КулЛиб - Классная библиотека! Скачать книги бесплатно 

Освой самостоятельно C++ по одному часу в день [Рао Сиддхартха] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
C++

О свой са м о сто я те льн о

по одному часу в день

ВОСЬМОЕ ИЗДАНИЕ

Sams Teach Yourself

C h—Ь

in One Hour a Day

EIGHTH EDITION

Siddhartha Rao

sAms
800 East 96th Street, Indianapolis, Indiana 46240

Освой самостоятельно

по одному часу в день

ВОСЬМОЕ ИЗДАНИЕ

Сиддхартха Рао

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

2017

ББК 32.973.26-018.2.75
Р22
УДК 681.3.07
Компьютерное издательство “Диалектика”
Зав. редакцией С. Я Тригуб
Перевод с английского и редакция канд. техн. наук И. В. Красикова
По общим вопросам обращайтесь в издательство “Диалектика” по адресу:
info@dialektika.com, http://www.dialektika.com
Рао, Сиддхартха.
Р22
Освой самостоятельно C++ по одному часу в день, 8-е и зд .: Пер. с англ. — С пБ.:
ООО “Альфа-книга”, 2017. — 752 с . : ил. — Парал. тит. англ.
ISBN 978-5-9909445-6-5 (рус.)
Б Б К 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответству­
ющих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни
было форме и какими бы то ни было средствами, будь то электронные или механические, включая фото­
копирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Sams
Publishing.
Authorized translation from the English language edition published by Sams Publishing, Copyright © 2017 by
Pearson Education, Inc.
All rights reserved. No part o f this book may be reproduced or transmitted in any form or by any means, electronic
or mechanical, including photocopying, recording, or by any information storage and retrieval system, without written
permission from the Publisher, except for the inclusion o f brief quotations in a review.
Russian language edition published by Dialektika Computer Books Publishing according to the Agreement with R&I
Enterprises International, Copyright © 2017.

Научно-популярное издание
Сиддхартха Рао
О с в о й с а м о с то я те л ь н о C + + п о о дн о м у ч а с у в д е н ь
8-е изда ни е
Л итературны й редактор
В ерстк а
Х удож ествен н ы й редактор
К орректор

Л.Н. Крас ножон
Л. В. Чернокозинская
Е.П. Дынник
Л.А. Гордиенко

Подписано в печать 28.08.2017. Формат 70x100/16.
Гарнитура Times.
Уел. печ. л. 47,0. Уч.-изд. л. 34,3.
Тираж 400 экз. Заказ № 5941
Отпечатано в АО «Первая Образцовая типография»
Филиал «Чеховский Печатный Двор»
142300, Московская область, г. Чехов, ул. Полиграфистов, д. 1
ООО “Альфа-книга”, 195027, Санкт-Петербург, Магнитогорская ул., д. 30

ISBN 978-5-9909445-6-5 (рус.)

© Компьютерное издательство “Диалектика”, 2017
перевод, оформление, макетирование

ISBN 978-0-7897-5774-6 (англ.)

© by Pearson Education, Inc., 2017

ВВЕДЕНИЕ

25

ЧАСТЬ I. ОСНОВЫ C++

29

зан я ти е

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

заня тие

2. Структура программы на C++

41

заня тие

з. Использование переменных и констант

55

ЗАНЯТИЕ 4. Массивы и строки

31

85

зан я ти е

5. Выражения, инструкции и операторы

105

зан я ти е

6 Управление потоком выполнения программы

129

зан я ти е

7. Организация кода с помощью функций

165

зан я ти е

8 Указатели и ссылки

191

часть

.

.

и. Объектно-ориентированное программирование на C++

227

зан я ти е

9. Классы и объекты

229

зан я ти е

ю . Реализация наследования

283

ЗАНЯТИЕ 11. Полиморфизм

315

зан я ти е

12. Типы операторов и их перегрузка

343

зан я ти е

13. Операторы приведения

381

зан я ти е

14. Введение в макросы и шаблоны

395

часть

ill. Стандартная библиотека шаблонов

425

заня тие

15. Введение в стандартную библиотеку шаблонов

427

заня тие

16. Класс строки библиотеки STL

439

зан я ти е

17. Классы динамических массивов библиотеки STL

457

ЗАНЯТИЕ 18. Классы list И forward_list

475

ЗАНЯТИЕ 19. Классы множеств STL

495

заня тие

20. Классы отображений библиотеки STL

513

ЧАСТЬ IV. Углубляемся в STL
ЗАНЯТИЕ 21. Понятие о функциональных объектах

535
537

з а н я ти е

22. Лямбда-выражения языка С++11

з а н я ти е

23. Алгоритмы библиотеки STL

567

з а н я ти е

24. Адаптивные контейнеры: стек и очередь

599

з а н я т и е 25.

ч асть

Работа с битовыми флагами при использовании библиотеки STL

V. Сложные концепции C++

553

615
625

з а н я ти е

26. Понятие интеллектуальных указателей

627

з а н я ти е

27. Применение потоков для ввода и вывода

641

з а н я ти е

28. Обработка исключений

663

з а н я ти е

29. Что дальше

677

ч а с ть

VI. Приложения

691

п р и л о ж ен и е

А. Двоичные и шестнадцатеричные числа

693

п р и л о ж ен и е

Б. Ключевые слова языка C++

699

прилож ение

в. Приоритет операторов

701

ПРИЛОЖЕНИЕ Г. Коды ASCII

703

ПРИЛОЖЕНИЕ Д. Ответы

707

ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ

747

Благодарности
Об авторе
Поддержка читателя
Ждем ваших отзывов!
ВВЕДЕНИЕ

23
23
24
24
25

Для кого написана эта книга
Структура книги
Соглашения, принятые в книге
Примеры кода
ЧАСТЬ I. ОСНОВЫ C++

.

25
25
26
27
29

1 Первые шаги
Краткий экскурс в историю языка C++
Связь с языком С
Преимущества языка C++
Развитие стандарта C++
Кто использует программы, написанные на C++
Создание приложения C++
Этапы создания выполнимого файла
Анализ и устранение ошибок
Интегрированные среды разработки
Создание первого приложения на C++
Построение и запуск вашего первого приложения C++
Понятие ошибок компиляции
Что нового в C++
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

31
32
32
32
33
33
33
34
34
34
35
36
38
38
39
39
40
40
40

2. Структура программы на C++
Части программы Hello World
Директива препроцессора #include
Тело программы — функция main ()
Возврат значения
Концепция пространств имен
Комментарии в коде C++
Функции в C++
Ввод-вывод с использованием потоков std::cin и std::cout
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

41
42
42
43
44
44
46
47
50
52
52
52
53
53

зан я ти е

з а н я ти е

з. Использование переменных и констант
Что такое переменная
Коротко о памяти и адресации
Объявление переменных для получения доступа и использования памяти
Объявление и инициализация нескольких переменных одного типа
Понятие области видимости переменной
Глобальные переменные
Соглашения об именовании
Распространенные типы переменных, поддерживаемые компилятором C++
Использование типа bool для хранения логических значений
Использование типа char для хранения символьных значений
Концепция знаковых и беззнаковых целых чисел
Знаковые целочисленные типы short, int, long и long long
Беззнаковые целочисленные типы unsigned short, unsigned int,
unsigned long и unsigned long long
Избегайте переполнения, выбирая подходящие типы
Типы с плавающей точкой float и double
Определение размера переменной с использованием оператора sizeof
Запрет сужающего преобразования при использовании инициализации списком
Автоматический вывод типа с использованием auto
Использование ключевого слова typedef для замены типа
Что такое константа
Литеральные константы
Объявление переменных как констант с использованием ключевого
слова const
Объявление констант с использованием ключевого слова constexpr
Перечисления
Определение констант с использованием директивы #def ine
Ключевые слова, недопустимые для использования
в качестве имен переменных и констант
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

55
56
56
56
58
59
61
62
63
64
64
65
66

4. Массивы и строки
Что такое массив
Необходимость в массивах
Объявление и инициализация статических массивов
Как данные хранятся в массиве
Доступ к данным, хранимым в массиве
Изменение данных в массиве
Многомерные массивы
Объявление и инициализация многомерных массивов
Доступ к элементам многомерного массива
Динамические массивы
Строки символов в стиле С

85
86
86
87
88
89
90
93
93
94
95
97

занятие

ЗАНЯТИ Е

66
67
69
69
71
72
73
74
74
75
76
78
80
80
81
82
84
84
84

Строки C++: использование s t d : : s t r i n g
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 5. Выражения, инструкции и операторы
Выражения
Составные инструкции, или блоки
Использование операторов
Оператор присваивания (=)
Понятие 1- и г-значений
Операторы сложения (+), вычитания (-), умножения (*),
деления (/) и деления по модулю (%)
Операторы инкремента (++) и декремента (—)
Что значит “постфиксный” и “префиксный”
Операторы равенства (==) и неравенства (! =)
Операторы сравнения
Логические операции НЕ, И, ИЛИ и ИСКЛЮЧАЮЩЕЕ ИЛИ
Использование логических операторов C++ !, && и | |
Побитовые операторы
|иА
Побитовые операторы сдвига вправо ( » ) и влево ( « )
Составные операторы присваивания
Использование оператора sizeof для определения объема памяти,
занимаемого переменной
Приоритет операторов
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
6, Управление потоком выполнения программы
Условное программирование с использованием конструкции i f . . . e ls e
Условное выполнение нескольких инструкций
Вложенные инструкции i f
Условная обработка с использованием конструкции sw itc h -c a se
Тернарный условный оператор (?: )
Выполнение кода в циклах
Рудиментарный цикл с использованием инструкции goto
Цикл w hile
Цикл do...while
Цикл fo r
Цикл fo r для диапазона
Изменение поведения цикла с использованием операторов co n tin u e и b reak
Бесконечные циклы, которые никогда не заканчиваются
Управление бесконечными циклами

занятие

100
101
102
103
103
103
105
106
107
107
107
107
108
109
109
111
111
114
115
119
121
122
124
125
127
127
128
128
128
129
131
133
134
138
141
142
143
145
146
148
151
153
154
154

Программирование вложенных циклов
Использование вложенных циклов для перебора многомерного массива
Использование вложенных циклов для вычисления чисел Фибоначчи
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
занятие

7. Организация кода с помощью функций

Потребность в функциях
Что такое прототип функции
Что такое определение функции
Что такое вызов функции и аргументы
Создание функций с несколькими параметрами
Создание функций без параметров и возвращаемых значений
Параметры функций со значениями по умолчанию
Рекурсия — функция, вызывающая сама себя
Функции с несколькими операторами r e tu r n
Использование функций для работы с данными различных видов
Перегрузка функций
Передача в функцию массива значений
Передача аргументов по ссылке
Как процессор обрабатывает вызовы функций
Встраиваемые функции
Автоматический вывод возвращаемого типа
Лямбда-функции
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
занятие

8. Указатели и ссылки

Что такое указатель
Объявление указателя
Определение адреса переменной с использованием оператора
получения адреса &
Использование указателей для хранения адресов
Доступ к данным с использованием оператора разыменования *
Значение s iz e o f () для указателя
Динамическое распределение памяти
Использование new и d e le te для выделения и освобождения памяти
Указатели и операции инкремента и декремента
Использование ключевого слова co n st с указателями
Передача указателей в функции
Сходство между массивами и указателями
Наиболее распространенные ошибки при использовании указателей
Утечки памяти

157
158
160
161
162
162
163
163
165
166
167
168
168
169
170
171
173
175
176
177
178
180
182
183
184
186
187
188
188
188

189
191
192
192
193
194
197
199
201
201
204
207
208
209
212
212

часть

Когда указатели указывают на недопустимые области памяти
Висячие (беспризорные, дикие) указатели
Проверка успешности запроса с использованием оператора new
Полезные советы по применению указателей
Что такое ссылка
Зачем нужны ссылки
Использование ключевого слова co n st со ссылками
Передача аргументов в функции по ссылке
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

213
214
215
218
218
220
221
221
223
223
225
225
225

II. Объектно-ориентированное программирование на C++

227

з а н я ти е

9. Классы и объекты

Концепция классов и объектов
Объявление класса
Объект как экземпляр класса
Доступ к членам класса с использованием оператора точки ( .)
Обращение к членам класса с использованием оператора указателя (->)
Ключевые слова p u b lic и p r iv a te
Абстракция данных с помощью ключевого слова p r iv a te
Конструкторы
Объявление и реализация конструктора
Когда и как использовать конструкторы
Перегрузка конструкторов
Класс без конструктора по умолчанию
Параметры конструктора со значениями по умолчанию
Конструкторы со списками инициализации
Деструктор
Объявление и реализация деструктора
Когда и как использовать деструкторы
Копирующий конструктор
Поверхностное копирование и связанные с ним проблемы
Глубокое копирование с использованием копирующего конструктора
Перемещающий конструктор улучшает производительность
Способы использования конструкторов и деструктора
Класс, который не разрешает себя копировать
Класс-синглтон, обеспечивающий наличие только одного экземпляра
Класс, запрещающий создание экземпляра в стеке
Применение конструкторов для преобразования типов
Указатель t h i s
Размер класса
Чем структура отличается от класса
Объявление друзей класса

229

230
230
231
232
232
234
236
237
237
238
240
242
243
244
246
246
247
250
250
252
257
258
258
259
262
264
266
267
269
270

Специальный механизм хранения данных — union
Объявление объединения
Где используется объединение
Агрегатная инициализация классов и структур
c o n stex p r с классами и объектами
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
з а н я ти е

ю . Реализация наследования

Основы наследования
Наследование и порождение
Синтаксис наследования C++
Модификатор доступа p ro te c te d
Инициализация базового класса — передача параметров базовому классу
Перекрытие методов базового класса в производном
Вызов перекрытых методов базового класса
Вызов методов базового класса в производном классе
Производный класс, скрывающий методы базового класса
Порядок конструирования
Порядок деструкции
Закрытое наследование
Защищенное наследование
Проблема срезки
Множественное наследование
Запрет наследования с помощью ключевого слова f i n a l
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 11. ПОЛИМОРФИЗМ

Основы полиморфизма
Потребность в полиморфном поведении
Полиморфное поведение, реализованное с помощью виртуальных функций
Необходимость виртуальных деструкторов
Как работают виртуальные функции. Понятие таблицы виртуальных функций
Абстрактные классы и чисто виртуальные функции
Использование виртуального наследования для решения проблемы ромба
Ключевое слово o v e rrid e для указания преднамеренного перекрытия
Использование ключевого слова f i n a l для предотвращения
перекрытия функции
Виртуальные копирующие конструкторы?
Резюме
Вопросы и ответы

272
272
273
275
278
279
279
280
280
281
283

284
284
286
288
290
293
295
296
298
300
300
303
305
308
309
311
313
313
313
314
314
315

316
316
318
320
324
328
330
335
336
336
340
340

Коллоквиум
Контрольные вопросы
Упражнения

341
341
342

12. Типы операторов и их перегрузка
Что такое операторы C++
Унарные операторы
Типы унарных операторов
Программирование унарного оператора инкремента или декремента
Создание операторов преобразования
Создание оператора разыменования (*) и оператора выбора члена (->)
Бинарные операторы
Типы бинарных операторов
Создание бинарных операторов сложения (а+b) ивычитания (а-Ь)
Реализация операторов сложения с присваиванием (+=)

343
344
345
345
345
348
351
353
353
354

и вычитания с присваиванием (-=)
Перегрузка операторов равенства ( = ) и неравенства (! =)
Перегрузка операторов , =
Перегрузка оператора копирующего присваивания (=)
Оператор индексации ([ ])
Оператор функции ()
Перемещающий конструктор и оператор перемещающего присваивания
Проблема излишнего копирования
Объявление перемещающих конструктора и оператора присваивания
Пользовательские литералы
Операторы, которые не могут быть перегружены
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

357
359
361
363
366
369
370
370
371
376
378
379
379
380
380
380

занятие

занятие

13. Операторы приведения

Потребность в приведении типов
Почему приведения в стиле С не нравятся некоторым программистам C++
Операторы приведения C++
Использование оператора s t a t i c c a s t
Использование оператора dynamic c a s t и идентификация типа
времени выполнения
Использование оператора r e i n t e r p r e t c a s t
Использование оператора co n st c a s t
Проблемы с операторами приведения C++
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

381
382
383
383
384
385
388
389
390
392
392
392
393
393

занятие

14. Введение в макросы и шаблоны

Препроцессор и компилятор
Использование #def ine для определения констант
Использование макроса для защиты от множественного включения
Использование директивы #def ine для написания макрофункции
Зачем все эти скобки?
Использование макроса assert для проверки выражений
Преимущества и недостатки использования макрофункций
Введение в шаблоны
Синтаксис объявления шаблона
Типы объявлений шаблонов
Шаблонные функции
Шаблоны и безопасность типов
Шаблонные классы
Объявление шаблонов с несколькими параметрами
Объявление шаблонов параметрами по умолчанию
Простой шаблон класса Holds Pair
Инстанцирование и специализация шаблона
Шаблонные классы и статические члены
Шаблоны с переменным количеством параметров (вариадические шаблоны)
Использование static assert для выполнения проверок

часть

395
396
396
399
400
402
402
404
405
406
406
407
409
409
410
411

411
413

415
416

времени компиляции
Использование шаблонов в практическом программировании на C++
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

420
421
422
422
423
423
423

ill. Стандартная библиотека шаблонов

425

15. Введение в стандартную библиотеку шаблонов
Контейнеры STL
Последовательные контейнеры
Ассоциативные контейнеры
Адаптеры контейнеров
Итераторы STL
Алгоритмы STL
Взаимодействие контейнеров и алгоритмов с использованием итераторов
Использование ключевого слова auto для определения типа
Выбор правильного контейнера
Классы строк библиотеки STL
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы

427
428
428
429
431
431
432
432
434
435
437
437
437
438
438

занятие

ЗАНЯТИЕ 16. Класс строки библиотеки STL

Потребность в классах обработки строк
Работа с классом строки STL
Создание экземпляров и копий строк STL
Доступ к символу в строке std::string
Конкатенация строк
Поиск символа или подстроки в строке
Усечение строк STL
Обращение строки
Смена регистра символов
Реализация строки на базе шаблона STL
Оператор ,,f,s в std::string в С++14
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 17. Классы динамических массивов библиотеки STL

Характеристики класса std::vector
Типичные операции с вектором
Создание экземпляра вектора
Вставка элементов в конец вектора с помощью push back ()
Инициализация списком
Вставка элементов в определенную позицию с помощью insert ()
Доступ к элементам вектора с использованием семантики массива
Доступ к элементам вектора с использованием семантики указателя
Удаление элементов из вектора
Концепции размера и емкости
Класс deque библиотеки STL
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 18. Классы l i s t И fo rw a rd _lis t

Характеристики класса std: : list
Основные операции со списком
Инстанцирование класса std: : list
Вставка элементов в начало и в конец списка
Вставка в середину списка
Удаление элементов из списка
Обращение списка и сортировка его элементов
Обращение элементов списка с помощью list :: reverse ()
Сортировка элементов
Сортировка и удаление элементов из списка, который содержит объекты класса
Шаблон класса std::forward list

439
440
441
441
443
445
446
448
450
451
453
453
454
455
455
455
455
457
458
458
458
460
461
461
464
465
466
468
470
473
473
474
474
474
475

476
476
476
478
479
482
483
484
485
487
490

Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

492
492
493
493
493

ЗАНЯТИЕ 19. Классы множеств STL

495
496
496
497
499
500
502
507

Введение в классы множеств STL
Фундаментальные операции с классами set и multiset
Инстанцирование объекта std::set
Вставка элементов в множество и мультимножество
Поиск элементов в множестве и мультимножестве
Удаление элементов из множества и мультимножества
Преимущества и недостатки использования множеств и мультимножеств
Реализация хеш-множеств std: :unordered_set
и s td ::u n o rd e re d _ m u ltis e t
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 20. Классы отображений библиотеки STL

Введение в классы отображений библиотеки STL
Фундаментальные операции с классами std: :map и std: :multimap
Инстанцирование классов std: :map и std: :multimap
Вставка элементов в map и multimap
Поиск элементов в отображении
Поиск элементов в мультиотображении STL
Удаление элементов из тар и multimap
Применение пользовательского предиката
Контейнер для пар “ключ-значение” на базе хеш-таблиц
Как работают хеш-таблицы
Использование unordered_map и unordered_multimap
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЧАСТЬ IV. Углубляемся в STL

21. Понятие о функциональных объектах
Концепция функциональных объектов и предикатов
Типичные приложения функциональных объектов
Унарные функции
Унарный предикат
Бинарные функции
Бинарный предикат

з а н я ти е

507
511
511
511
512
512
513
514
515
515
517
519
522
522
524
528
528
529
533
533
534
534
534
535

537
538
538
538
543
545
547

Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
зан я ти е

22. Лямбда-выражения языка С++11

Что такое лямбда-выражение
Как определить лямбда-выражение
Лямбда-выражение для унарной функции
Лямбда-выражение для унарного предиката
Лямбда-выражения с состоянием и списки захвата [ . . . ]
Обобщенный синтаксис лямбда-выражений
Лямбда-выражение для бинарной функции
Лямбда-выражение для бинарного предиката
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

Что такое алгоритмы STL
Классификация алгоритмов STL
Не изменяющие алгоритмы
Изменяющие алгоритмы
Использование алгоритмов STL
Поиск элементов по заданному значению или условию
Подсчет элементов с использованием значения или условия
Поиск элемента или диапазона в коллекции
Инициализация элементов в контейнере заданным значением
Использование алгоритма s t d : : g e n e ra te ()
для инициализации значениями, генерируемыми во время выполнения
Обработка элементов диапазона с использованием алгоритма for each ()
Выполнение преобразований с помощью алгоритма std :: transform ()
Операции копирования и удаления
Замена значений и элементов с использованием условия
Сортировка, поиск в отсортированной коллекции и удаление дубликатов
Разделение диапазона
Вставка элементов в отсортированную коллекцию
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения
24. Адаптивные контейнеры: стек и очередь
Поведенческие характеристики стеков и очередей
Стеки
Очереди

зан я ти е

550
550
550
550
551
553

554
555
555
557
558
560
561
563
565
565
566
566
566
567
568
568
568
569
571
571
573
575
577
579
581
583
585
588
590
592
594
597
597
598
598
598
599
600
600
600

18

|

Содержание

Использование класса STL s ta c k
Создание экземпляра стека
Функции-члены класса s ta c k
Вставка и извлечение с помощью методов push () и pop ()
Использование класса STL queue
Создание экземпляра очереди
Функции-члены класса queue
Вставка в конец и извлечение из начала очереди с использованием методов
push () и pop ()
Использование класса STL p r i o r i t y queue
Создание экземпляра очереди с приоритетами
Функции-члены класса p r i o r i t y queue
Вставка в конец и извлечение из начала очереди с приоритетами
с использованием методов push () и pop ()
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

601
601
602
603
605
605
606

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

Класс b i t s e t
Инстанцирование класса s td : : b i t s e t
Использование класса s td : : b i t s e t и его членов
Полезные операторы, предоставляемые классом s td : : b i t s e t
Методы класса s t d : :b i t s e t
Класс vector< bool>
Создание экземпляра класса vector< bool>
Функции и операторы класса vector< bool>
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

615
616
616
617
618
618
621
621
622
623
623
624
624
624

V. Сложные концепции C++

625

26. Понятие интеллектуальных указателей
Что такое интеллектуальный указатель
Проблемы обычных указателей
Чем могут помочь интеллектуальные указатели
Как реализованы интеллектуальные указатели
Типы интеллектуальных указателей
Глубокое копирование
Механизм копирования при записи
Интеллектуальные указатели со счетчиком ссылок
Интеллектуальный указатель со списком ссылок

627
628
628
628
629
630
631
633
633
634

ч асть

з а н я ти е

607
608
608
610
611
613
613
613
614
614

Деструктивное копирование
Использование интеллектуального указателя s td : :u n iq u e _ p tr
Популярные библиотеки интеллектуальных указателей
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

634
637
639
639
639
640
640
640

27. Применение потоков для ввода и вывода
Концепция потоков
Важнейшие классы и объекты потоков C++
Использование s t d : : cout для вывода форматированных данных на консоль
Изменение формата представления чисел
Выравнивание текста и установка ширины поля
Использование s t d :: c in для ввода
Использование s t d :: c in для ввода простых старых типов данных
Использование метода s t d : : c i n : : g e t () для ввода в буфер char*
Использование s t d : : c in для ввода в переменную типа s t d : : s t r i n g
Использование потока s t d : : f stream для работы с файлом
Открытие и закрытие файла с помощью методов open () и c lo s e ()
Создание и запись текстового файла с использованием метода open ()
и оператора «
Чтение текстового файла с использованием метода open () и оператора »
Запись и чтение из бинарного файла
Использование s t d :: s trin g s tre a m для преобразования строк
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

641

28. Обработка исключений
Что такое исключение
Что вызывает исключения
Реализация безопасности в отношении исключений
с помощью блоков t r y и c a tc h
Использование блока c a tc h ( . . . ) для обработки всех исключений
Обработка исключения конкретного типа
Генерация исключения с помощью оператора throw
Как работает обработка исключений
Класс s td : e x c e p tio n
Пользовательский класс исключения, производный от s t d : : e x c e p tio n
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы
Упражнения

663
664
664

з а н я ти е

зан я ти е

642
643
644
645
647
648
648
649
650
652
652
653
655
656
658
660
660
660
660
661

665
665
666
668
669
671
672
674
675
675
676
676

з а н я ти е

ч асть

29. Что дальше

677

Чем отличаются современные процессоры
Как лучше использовать несколько ядер
Что такое поток
Зачем создавать многопоточные приложения
Как потоки осуществляют транзакцию данных
Использование мьютексов и семафоров для синхронизации потоков
Проблемы, вызываемые многопоточностью
Как писать отличный код C++
C++17: что новенького
Инициализация в i f и sw itch
Гарантия устранения копирования
Устранение накладных расходов выделения памяти
с помощью s t d : : s t r i n g view
s t d :: v a r ia n t как безопасная с точки зрения типов альтернатива объединению
Условная компиляция с использованием i f co n stex p r
Усовершенствованные лямбда-выражения
Автоматический вывод типа для конструкторов
tem plate< auto>
Изучение C++ на этом не заканчивается
Документация в вебе
Сетевые сообщества и помощь
Резюме
Вопросы и ответы
Коллоквиум
Контрольные вопросы

678
679
679
680
681
682
682
683
684
684
685

VI. Приложения

691

А. Двоичные и шестнадцатеричные числа
Десятичная система счисления
Двоичная система счисления
Почему компьютеры используют двоичные числа
Что такое биты и байты
Сколько байтов в килобайте
Шестнадцатеричная система счисления
Зачем нужна шестнадцатеричная система
Преобразование в различные системы счисления
Обобщенный процесс преобразования
Преобразование десятичного числа в двоичное
Преобразование десятичного числа в шестнадцатеричное

693
694
694
695
695
695
696
696
697
697
697
698

п р и л о ж ен и е

686
686
687
688
688
688
688
688
689
689
689
690
690

п р и л о ж ен и е

Б. Ключевые слова языка C++

699

п р и л о ж ен и е

в. Приоритет операторов

701

ПРИЛОЖЕНИЕ Г. Коды ASCII

Таблица ASCII отображаемых символов

703
704

ПРИЛОЖЕНИЕ Д. Ответы

Ответы к занятию 1
Контрольные вопросы
Упражнения
Ответы к занятию 2
Контрольные вопросы
Упражнения
Ответы к занятию 3
Контрольные вопросы
Упражнения
Ответы к занятию 4
Контрольные вопросы
Упражнения
Ответы к занятию 5
Контрольные вопросы
Упражнения
Ответы к занятию 6
Контрольные вопросы
Упражнения
Ответы к занятию 7
Контрольные вопросы
Упражнения
Ответы к занятию 8
Контрольные вопросы
Упражнения
Ответы к занятию 9
Контрольные вопросы
Упражнения
Ответы к занятию 10
Контрольные вопросы
Упражнения
Ответы к занятию 11
Контрольные вопросы
Упражнения
Ответы к занятию 12
Контрольные вопросы
Упражнения
Ответы к занятию 13
Контрольные вопросы
Упражнения
Ответы к занятию 14
Контрольные вопросы
Упражнения
Ответы к занятию 15
Контрольные вопросы
Ответы к занятию 16
Контрольные вопросы
Упражнения

707
707
707
707
708
708
708
709
709
709
710
710
711
711
711
712
712
712
713
716
716
716
717
717
717
717
717
718
719
719
719
720
720
720
722
722
722
723
723
723
724
724
724
725
725
726
726
726

Ответы к занятию 17
Контрольные вопросы
Упражнения
Ответы к занятию 18
Контрольные вопросы
Упражнения
Ответы к занятию 19
Контрольные вопросы
Упражнения
Ответы к занятию 20
Контрольные вопросы
Упражнения
Ответы к занятию 21
Контрольные вопросы
Упражнения
Ответы к занятию 22
Контрольные вопросы
Упражнения
Ответы к занятию 23
Контрольные вопросы
Упражнения
Ответы к занятию 24
Контрольные вопросы
Упражнения
Ответы к занятию 25
Контрольные вопросы
Упражнения
Ответы к занятию 26
Контрольные вопросы
Упражнения
Ответы к занятию 27
Контрольные вопросы
Упражнения
Ответы к занятию 28
Контрольные вопросы
Упражнения
Ответы к занятию 29
Контрольные вопросы
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ

729
729
729
732
732
733
734
734
734
737
737
738
738
738
738
740
740
740
741
741
742
743
743
743
743
743
744
744
744
744
745
745
746
746
746
746
746
746
747

Памяти моего отца, который остается для меня источником вдохновения.

Благодарности
Я благодарен за огромную поддержку моей семье, и особенно жене Кларе, а также
всем сотрудникам редакции за активное участие в судьбе этой книги.

Об авторе
Сиддхартха Рао — вице-президент по вопросам безопасности в компании SAP
AG, ведущем мировом поставщике корпоративного программного обеспечения. По­
стоянная эволюция языка C++ постоянно убеждает Рао в том, что приложения на C++
можно создавать быстрее, проще и эффективнее.
Сиддхартха любит путешествовать и является страстным поклонником горного ве­
лосипеда. Он с нетерпением ждет ваших отзывов о своей работе!

24

|

Поддержка читателя

Поддержка читателя
Для доступа к исходному коду, файлам примеров, обновлениям и исправлениям,
когда они появятся, зарегистрируйте свою книгу на i n f o r m i t . с о т / r e g i s t e r .

Ждем ваших отзывов!
Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хо­
тим знать, что было сделано нами правильно, что можно было сделать лучше и что
еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в
наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бу­
мажное или электронное письмо либо просто посетить наш веб-сайт и оставить свои
замечания там. Одним словом, любым удобным для вас способом дайте нам знать,
нравится ли вам эта книга, а также выскажите свое мнение о том, как сделать наши
книги более интересными для вас.
Отправляя письмо или сообщ ение, не забудьте указать название книги и ее авто­
ров, а также свой обратный адрес. Мы внимательно ознакомимся с вашим мнением и
обязательно учтем его при отборе и подготовке к изданию новых книг.
Наши электронные адреса:
E-mail: i n f o @ d i a l e k t i k a . com
WWW: h t t p : / / w w w .d i a l e k t i k a .c o m
Наши почтовые адреса:
в России: 195027, Санкт-Петербург, Магнитогорская ул., д. 30, ящик 116
в Украине: 03150, Киев, а/я 152

Введение
2011 и 2014 годы были особенно важными для языка C++. В то время как новый
стандарт С ++11 внес в язык программирования кардинальные изменения, новые клю­
чевые слова и конструкции, повышающие эффективность программирования, C++14
скорее добавил завершающие штрихи к возможностям, внесенным в язык стандартом
С++11.
Эта книга поможет вам изучить язык C++11 маленькими шагами. Она специ­
ально разделена на отдельные занятия, на которых основные принципы этого языка
объектно-ориентированного программирования излагаются с практической точки зре­
ния. Вы сможете овладеть языком С ++11, уделяя каждому занятию всего один час.
Наилучший способ изучения языка программирования — его практическое при­
менение, поэтому в книге очень много разнообразных примеров кода, анализируя ко­
торые, вы улучшите свои знания языка программирования C++. Эти фрагменты кода
протестированы с использованием последних версий компиляторов, имеющихся на
момент написания книги, а именно — компиляторов Microsoft Visual C++ и GNU C++,
которые охватывают большинство возможностей С++14.

Для кого написана эта книга
Книга начинается с основ языка C++. Необходимы лишь желание изучить этот
язык и сообразительность, чтобы понять, как он работает. Наличие навыков програм­
мирования на языке C++ может быть преимуществом, но не является обязательным.
Кроме того, к этой книге имеет смысл обратиться, если вы уже знаете язык C++, но
хотите изучить дополнения, которые были внесены в него последними стандартами.
Если вы профессиональный программист, то часть III, “Стандартная библиотека ша­
блонов”, книги поможет узнать, как создавать более эффективные приложения C++.

ПРИМЕЧАНИЕ

Для доступа к исходному коду, файлам примеров, обновлениям и исправ­
лениям, когда они появятся, зарегистрируйте свою книгу на

in fo rm it.

c o m /re g iste r.

Структура книги
В зависимости от уровня своей квалификации вы можете начать изучение с лю бо­
го раздела. Концепции С ++11 и C++14 не выносятся в отдельные главы, а разбросаны
по всей книге. Книга состоит из пяти частей.

Соглашения, принятые в книге

26

■ Часть I, “Основы C++”, позволяет приступить к написанию простых приложений
C++. Одновременно она знакомит с ключевыми словами, которые вы чаще всего
видите в коде C++, а также с переменными, но не затрагивает безопасность типов.
■ Часть II, “Объектно-ориентированное программирование на C++”, знакомит с кон­
цепцией классов. Вы узнаете, как язык C++ поддерживает важнейшие принципы
объектно-ориентированного программирования, включая инкапсуляцию, абстракцию,
наследование и полиморфизм. Занятие 9, “Классы и объекты”, представляет такую
концепцию C++, как перемещающий конструктор, а занятие 12, “Типы операторов и
их перегрузка”, — оператор перемещающего присваивания. Эти эффективные сред­
ства помогают сократить ненужные и нежелательные этапы копирования, увеличи­
вая производительность приложения. Занятие 14, “Введение в макросы и шаблоны”,
является краеугольным камнем для написания мощного обобщенного кода на C++.
■ Часть III, “Стандартная библиотека шаблонов”, поможет писать эффективный код
C++, использующий класс STL s t d n s t r i n g и контейнеры. Вы узнаете, как класс
std : rstrin g упрощает операции конкатенации строк и позволяет избежать исполь­
зования символьных строк в стиле С. Вы сможете использовать динамические мас­
сивы и связанные списки библиотеки STL, а не создавать их самостоятельно.
■ Часть IV, “Углубляемся в STL”, посвящена алгоритмам. Вы узнаете, как, используя
итераторы, применить сортировку в таких контейнерах, как вектор. Здесь также из­
ложено, как ключевое слово C++11 auto позволяет существенно сократить длину
объявлений итератора. Занятие 22, “Лямбда-выражения языка С + + Н ”, представля­
ет мощное новое средство, позволяющее существенно сократить размеры кода при
использовании алгоритмов библиотеки STL.
■ Часть V, “Сложные концепции C++”, объясняет такие средства языка, как интел­
лектуальные указатели и обработка исключений, которые не являются необходи­
мостью в приложении C++, но вносят существенный вклад в увеличение его ста­
бильности и качества. Эта часть завершается полезными советами по написанию
приложений С ++11 и новыми возможностями, которые должны появиться в новей­
шем стандарте C++17.

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

ПРИМЕЧАНИЕ

Здесь приводится дополнительная информация, связанная с материалом
занятия.

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

СОВЕТ

В этой врезке приводятся практические советы по написанию программ
на C++.

П р и м ер ы кода

РЕКОМЕНДУЕТСЯ

27

НЕ РЕКОМЕНДУЕТСЯ

Используйте эти рекомендации для поиска

Не пропускайте важные замечания и преду­

краткого резюме фундаментальных концеп­

преждения, показанные в этом столбце.

ций, представленных на занятии.

В книге используются различные шрифты для того, чтобы подчеркнуть те или иные
моменты, с применением соглашений, общепринятых в компьютерной литературе.
■ Новые термины в тексте выделяются курсивом. Чтобы обратить внимание читателя
на отдельные фрагменты текста, также применяется курсив.
■ Текст программ, функций, переменных, URL веб-страниц и другой код представле­
ны моноширинным шрифтом.
■ Все, что придется вводить с клавиатуры, выделено полужирным шрифтом.
■ Знакоместо в описаниях синтаксиса выделено курсивом. Это указывает на необхо­
димость заменить знакоместо фактическим именем переменной, параметром или
другим элементом, который должен находиться на этом месте: c l a s s Производный:
Модификатор _ Доступа Базовы й .

■ Пункты меню и названия диалоговых окон представлены следующ им образом:
Пункт меню.
■ В листингах каждая строка имеет номер. Это сделано исключительно для удобства
описания. В реальном коде нумерация отсутствует.

Примеры кода
Примеры кода, приведенные в этой книге, доступны на веб-сайте издательства
http://www.williamspublishing.com/Books/978-5-9909445-6-5.html.

ЧАСТЬ I

Основы C++
В ЭТОЙ ЧАСТИ...

ЗАНЯТИЕ 1. Первые шаги
ЗАНЯТИЕ 2. Структура программы на C++
ЗАНЯТИЕ 3. Использование переменных и констант
ЗАНЯТИЕ 4. Массивы и строки
ЗАНЯТИЕ 5. Выражения, инструкции и операторы
ЗАНЯТИЕ 6. Управление потоком выполнения программы
ЗАНЯТИЕ 7. Организация кода с помощью функций
ЗАНЯТИЕ 8. Указатели и ссылки

ЗАНЯТИЕ 1

Первые шаги
Добро пожаловать на страницы книги Освой самостоя­
тельно C++ по одному часу в день! Сегодня начинается дол­
гий путь, который позволит вам достичь профессионального
уровня в программировании на языке C++.
На этом занятии...
■ Почему язык C++ стал стандартом в области разработки
программного продукта
■ Как набрать, откомпилировать и скомпоновать первую
рабочую программу C++
■ Что нового в C++

32

|

ЗАНЯТИЕ 1. Первые шаги

Краткий экскурс в историю языка C++
Задача языка программирования — упростить использование вычислительных
ресурсов. Язык C++ отнюдь не нов, но весьма популярен и продолжает совершен­
ствоваться. На момент написания этой книги его последняя версия, принятая М ежду­
народным комитетом по стандартам (ISO) и опубликованная в декабре 2014 года, на­
зывается среди программистов “C ++14”.

Связь с языком С
Первоначально разработанный Бьярне Страуструпом в 1979 году, язык C++ был за­
думан как преемник языка С. В противоположность языку программирования С язык
C++ был спроектирован как объектно-ориентированный язык, который реализует та­
кие концепции, как наследование, абстракция, полиморфизм и инкапсуляция. Классы
языка C++ используют для работы с данными данные-члены и методы-члены. Эти
методы работают с данными, хранящимися в данных-членах. В результате такой орга­
низации программист моделирует данные и действия, которые планирует выполнить
над ними. Многие популярные компиляторы C++ также традиционно продолжают
поддерживать программы на языке С.

ПРИМЕЧАНИЕ

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

Преимущества языка C++
C++ считается языком программирования среднего уровня. Это означает, что он
позволяет создавать как высокоуровневые приложения, так и низкоуровневые библио­
теки, работающие с аппаратными средствами. Длямногих программистов язык C++
представляет собой оптимальную комбинацию: являясь языком высокого уровня, он
позволяет любому создавать сложные приложения, тем самым сохраняя для разработ­
чика возможность достичь максимальной производительности за счет строгого кон­
троля над использованием ресурсов и их доступностью.
Несмотря на наличие более новых языков программирования, таких как Java, и
языков на платформе .NET, язык C++ остается популярным и продолжает развиваться.
Более новые языки предоставляют дополнительные средства, такие как управление
памятью за счет сбора “мусора”, реализованное в компоненте исполняющей среды,
которые нравятся некоторым программистам. Однако там, где нужны высокая произ­
водительность создаваемого приложения и уменьшенное потребление ресурсов ком­
пьютера, программисты все же выбирают язык C++. Многоуровневая архитектура,
когда веб-сервер создается на языке C++, а пользовательская часть приложения — на
HTML, Java или .NET, является в настоящее время достаточно распространенной.

Создание приложения C++

33

Развитие стандарта C++
В силу популярности языка годы развития сделали язык C++ доступным на многих
разных платформах, большинство из которых имеет собственные компиляторы C++.
Развитие языка привело к наличию определенных отклонений от стандарта в разных
компиляторах и, соответственно, к большому количеству проблем совместимости и
переносимости кода. В результате появилась насущная потребность в стандартизации
данного языка программирования.
В 1998 году первая стандартная версия языка C++ была ратифицирована М еж­
дународной организацией по стандартизации ISO Committee в виде стандарта ISO/
1ЕС 14882:1998. С тех пор стандарт претерпел множество изменений, которые повы­
сили удобство использования языка и расширили поддержку стандартной библиоте­
ки. На момент написания этой книги текущая ратифицированная версия стандарта —
ISO/IEC 14882:2014, неофициально именуемая С++14.
Зачастую текущий стандарт поддерживается популярными компилятора­
ми не сразу или не в полном объеме. Таким образом, хотя с академичес­
кой точки зрения знание новейших дополнений к стандарту оправданно
и безусловно верно, надо помнить, что зти дополнения не являются обя­
зательным условием для написания хороших многофункциональных при­
ложений на C++.

Кто использует программы, написанные на C++
Список приложений, операционных систем, драйверов устройств, офисных при­
ложений, веб-сервисов, баз данных и прочего программного обеспечения, созданного
с использованием C++, очень длинный. Независимо от того, кто вы или что вы делае­
те на компьютере, очень высоки шансы, что вы постоянно используете программное
обеспечение, написанное на C++. Этот язык программирования используют не только
разработчики программ; он часто выбирается в качестве рабочего языка программи­
рования для исследовательской работы физиками и математиками.

Создание приложения C++
Запуская Блокнот в W indows или терминал Linux, вы фактически указывае­
те процессору запустить выполнимый файл этой программы. Выполнимый файл
(executable) — это готовый продукт, который может быть выполнен на компьютере и
должен сделать то, чего намеревался достичь программист.

34

|

ЗАНЯТИЕ 1. Первые шаги

Этапы создания выполнимого файла
Написание программы C++ является первым этапом создания выполнимого фай­
ла, который в конечном счете может быть выполнен в вашей операционной системе.
Основные этапы создания приложений C++ приведены ниже.
1. Написание (программирование) кода C++ с использованием текстового редак­
тора.
2. Компиляция кода с помощью компилятора C++, который преобразовывает ис­
ходный текст в команды машинного языка и записывает их в объектный файл
(object file).
3. Компоновка результатов работы компилятора с помощью компоновщика и по­
лучение окончательного выполнимого файла ( . ех е в Windows, например).

Компиляция (compilation) представляет собой этап, на котором код C++, содержа­
щийся обычно в текстовых файлах с расширением . срр, преобразуется в бинарный
код, который может быть выполнен процессором. Компилятор (compiler) преобразует
по одному файлу кода за раз, создавая объектный файл с расширением .о или .o b j
и игнорируя связи, которые код в этом файле может иметь с кодом в другом файле.
Распознавание этих связей и объединение кода в одно целое является задачей ком­
поновщика (linker). Кроме объединения различных объектных файлов, он разрешает
имеющиеся связи и в случае успешной компоновки создает выполнимый файл, кото­
рый можно выполнять и в конечном счете распространять среди пользователей. Весь
процесс в целом называется построением выполнимого файла.

Анализ и устранение ошибок
Большинство приложений редко компилируются и начинают хорошо работать сра­
зу же. Большое или сложное приложение, написанное на любом языке (включая C++),
зачастую требует множества запусков для выполнения тестирования, анализа проблем
и обнаружения ошибок. Затем ошибки исправляются, программа перекомпилируется
и процесс тестирования продолжается. Таким образом, в дополнение к перечислен­
ным выше трем этапам (программирование, компиляция и компоновка) разработка
зачастую подразумевает этап отладки (debugging), на котором программист анали­
зирует ошибки в приложении и исправляет их. Хорошая интегрированная среда раз­
работки обеспечивает программиста инструментальными средствами отладки, облег­
чающими указанный процесс.

Интегрированные среды разработки
Большинство программистов предпочитают использовать интегрированную сре­
ду разработки (Integrated Developm ent Environment — IDE), объединяющую этапы
программирования, компиляции и компоновки в пределах единого пользовательского
интерфейса, предоставляющего также средства отладки, облегчающие обнаружение
ошибок и устранение проблем.

Создание приложения C++

СОВЕТ

35

Самым быстрым способом приступить к написанию, компиляции и выпол­
нению программ на C++ может быть использование удаленной интегриро­
ванной среды разработки, работающей через браузер. Один из таких ин­
струментов доступен по адресу h t t p : //w w w . tutorialspoint.сот/

compile__cpp_online.php.
Кроме того, вы можете установить на свой компьютер множество бесплатных
интегрированных сред разработки и компиляторов C++. Наиболее популяр­
ные из них - Microsoft Visual Studio Express для Windows и GNU C++ Compiler
(называемый также g++) для Linux. Если вы программируете на Linux, то мо­
жете установить бесплатную интегрированную среду разработки Eclipse для
разработки приложений C++ с использованием компилятора g++.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Сохраняйте свои файлы исходного кода в фай­

Не используйте расширение . с для файлов с

лах с расширением .с р р .

исходными текстами, поскольку большинство

Используйте для создания исходного кода про­
стой текстовый редактор либо интегрированную
среду разработки.

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

Создание первого приложения на C++
Теперь, когда вы знаете о том, какие есть инструментальные средства и как созда­
ются программы, пришло время создать первое приложение C++, которое по тради­
ции выводит на экран текст Hello World! (“Привет, мир!”).
Ели вы программируете в Linux, воспользуйтесь простым текстовым редактором
(я в Ubuntu использую gedit) для создания .срр-файла с содержимым, показанным
в листинге 1.1.
Если вы работаете под управлением операционной системы Windows и используе­
те IDE Microsoft Visual Studio, то можете следовать описанным ниже шагам.
1. Создайте проект, используя команду File^Create1^Project (Ф а й л^ С о з д а ть О
Проект).
2. Выберите тип проекта Win32 Console Application (Консольное приложение
Win32) и назовите свой проект Hello. Щелкните на кнопке ОК.
3. В окне настроек проекта снимите флажок Precompiled Headers (Предваритель­
но скомпилированный заголовок). Щелкните на кнопке Finish (Готово).

4. Замените автоматически созданное содержимое в файле H e llo . срр фрагментом
кода, представленным в листинге 1.1.

36

|

ЗАНЯТИЕ 1. Первые шаги

ЛИСТИНГ 1.1. Программа Hello World (файл H e llo . срр)
1:
2:
3:
4:
5:
6:
7:

#include
int main ()
{
std::cout «
return 0;
}

"Hello World!" «

std::endl;

Это простое приложение всего лишь выводит на экран строку, используя опера­
тор s t d : : co u t. Оператор s t d : : e n d l указывает объекту потока c o u t закончить вывод
строки переходом на новую строку, а оператор r e t u r n 0 обеспечивает завершение
работы приложения и возврат операционной системе кода 0.

ВНИМАНИЕ!

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

Построение и запуск вашего первого приложения C++
Работая под управлением Linux, откройте терминал и перейдите в каталог, содер­
жащий файл H e l l o . срр. Вызовите компилятор g++ и компоновщик, используя сле­
дующ ую командную строку:

д++ -о h e llo H e llo .срр
Эта команда приказывает компилятору g++ создать выполнимый файл h e l l o пу­
тем компиляции исходного файла C++ H e llo . срр.
Если вы используете Microsoft Visual Studio в Windows, нажмите для запуска про­
граммы непосредственно в интегрированной среде разработки комбинацию клавиш
. В результате программа будет откомпилирована, скомпонована и запущена
на выполнение. Эти же этапы можно пройти индивидуально.
1. Щелкните правой кнопкой мыши на проекте и в появившемся контекстном
меню выберите пункт Build, чтобы создать выполнимый файл.
2. Используя приглашение ко вводу команд, перейдите по пути выполнимого фай­
ла (обычно это каталог Debug папки проекта).
3. Запустите приложение, введя имя его выполнимого файла.
В среде разработки Microsoft Visual Studio ваша программа будет выглядеть при­
мерно так, как показано на рис. 1.1.

С о зд а н и е п р и л о ж е н и я C++

37

РИС. 1.1. П р и м е р п р о г р а м м ы C++ “ H e llo W o rld " в с р е д е р а з р а б о т к и M ic r o s o ft V is u a l
S tu d io

Запуск файла . / h e l l o в Linux или H e l l o . ех е на Windows даст следующий вывод:
Hello World!

Поздравляю! Вы начали свой путь к изучению одного из самых популярных и
мощных языков программирования!

Значение стандарта C++ ISO
Как можно заметить, соответствие стандарту позволяет компилировать и выполнять фрагмент
кода из листинга 1.1 на нескольких платформах или операционных системах - это преимущес­
тво соответствующих стандарту компиляторов C++. Таким образом, если необходимо создать
продукт, который способен выполняться в операционной системе как Windows, так и Linux, на­
пример, то совместимые со стандартом практики программирования (которые не подразуме­
вают использование семантики или компилятора, специфического для конкретной платформы)
предоставят недорогой способ завоевать более широкую аудиторию пользователей без необхо­
димости создавать специальную версию программы для каждой среды. Это, безусловно, пре­
красно подходит для приложений, которые не нуждаются в частом взаимодействии на уровне
операционной системы.

38

|

ЗАНЯТИЕ 1. Первые шаги

Понятие ошибок компиляции
Компиляторы крайне педантичны в своих требованиях, но, тем не менее, предпри­
нимают определенные усилия, чтобы оповестить вас о сделанных ошибках. Если вы
столкнулись с проблемой при компиляции приложения в листинге 1.1, то сообщение
об ошибке, вероятнее всего, будет похоже на следующее (автор преднамеренно убрал
точку с запятой в строке 5):
hello.срр(6): error С2143: syntax error : missing

before 'return'

Это сообщ ение об ошибке от компилятора Visual C++ весьма описательно: в нем
указываются имя файла, в котором содержится ошибка, номер строки (в данном слу­
чае — 6), в которой пропущена точка с запятой, и описание самой ошибки, предваряе­
мое номером ошибки (в данном случае — С2143). Хотя знак препинания был удален
из строки 5 кода примера, в сообщ ении об ошибке упоминается следующая строка,
поскольку для компилятора ошибка стала очевидной, только когда он проанализиро­
вал оператор r e t u r n и понял, что перед переходом к оператору r e t u r n предыдущая
инструкция должна была быть закончена. Можете попробовать добавить точку с за­
пятой в начале строки 6, и программа будет без проблем откомпилирована!

ПРИМЕЧАНИЕ

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

Что нового в C++
Если вы опытный программист C++, то, вероятно, уже обратили внимание на то,
что в примере программы C++ из листинга 1.1 за последние, пожалуй, десятилетия не
изменился ни один бит. Но хотя язык C++ остается полностью обратно совместимым
с предыдущими версиями языка, на самом деле не так давно было проделано очень
много работы, чтобы упростить его использование.
Последние крупные обновления языка были выпущены как часть стандарта ISO,
ратифицированного в 2011 году, который программисты называют “C++11”. C++14,
выпущенный в 2014 году, содержит в основном не столь значительные улучшения и
исправления С + +11.
Такое средство, как ключевое слово a u to , введенное в С++11, позволяет опреде­
лить переменную, тип которой компилятор выводит автоматически, позволяя компакт­
но записать многословные объявления (например, итераторов) и не нарушая безопас­
ность типов. С++14 добавляет эту возможность для возвращаемых значений функций.
Лямбда-функции — это функции без имени. Они позволяют писать компактные объек­
ты функций без длинных определений класса, значительно сокращая строки кода.
Стандарт C++ обещал программистам возможность писать переносимые, многопоточ­
ные и соответствующие стандарту приложения C++. Эти приложения при правильном

Резюме

|

39

построении поддерживают парадигму параллельного выполнения и хорошо позицио­
нируются как масштабируемые по производительности, когда пользователь наращива­
ет возможности своих аппаратных средств, увеличивая количество ядер процессора.
Это лишь некоторые из многих преимуществ языка C++, обсуждаемых в этой книге.
Новые возможности языка, ожидаемые в будущем стандарте, именуемом “C++17”,
рассматриваются на занятии 29, “Что дальше”.

Резюме
На этом занятии вы узнали, как написать, откомпилировать, скомпоновать и вы­
полнить свою первую программу C++. Здесь приведен также краткий обзор развития
языка C++ и продемонстрирована эффективность стандарта на примере того, как одна
и та же программа может быть откомпилирована с использованием разных компиля­
торов на разных операционных системах.

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

■ Чем интерпретируемый язык отличается от компилируемого?
Интерпретируемыми являются такие языки, как Windows Script. У них нет этапа
компиляции. Интерпретируемый язык использует интерпретатор, который читает
текстовый файл сценария (код) и выполняет желаемые действия. Поэтому на ма­
шине, на которой должен быть выполнен сценарий, необходимо установить интер­
претатор; следовательно, страдает производительность, поскольку интерпретатор
работает как транслятор времени выполнения, расположенный между написанным
кодом и микропроцессором.

■ Что такое ошибки времени выполнения и чем они отличаются от ошибок вре­
мени компиляции?
Ошибки, которые появляются при выполнении приложения, называются ошибками
времени выполнения (runtime error). Возможно, вам встречалось сообщение “A ccess
Violation” в старых версиях W indows, являющееся оповещением об ошибке вре­
мени выполнения программы. Сообщения об ошибках компиляции не доходят до
конечного пользователя и являются свидетельством синтаксических проблем; они
не позволяют программисту создать выполнимый файл.

ЗАНЯТИЕ 1. Первые шаги

40

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. В чем разница между интерпретатором и компилятором?
2. Что делает компоновщик?
3. Каковы этапы обычного цикла разработки?

Упражнения
1. Рассмотрите следующ ую программу и попытайтесь предположить, что она де­
лает, не запуская ее:
1: #include
2: int main()
3: {
4:
int x = 8;
5:
int у = 6;
6:
std::cout « std::endl;
7:
std::cout « x-y «
"" «
8:
std::cout « std::endl;
9:
return 0;

x*y «

" " «

x+y;

10: }

2. Введите программу из упражнения 1, а затем откомпилируйте и скомпонуйте
ее. Что она делает? Она делает то, что вы предполагали?
3. Где ошибка в следующей программе?
1: include
2: int main ()
3: {
4:
std::cout « "Hello
5:
return 0;

6:

Buggy World \n";

}

4. Исправьте ошибку в программе из упражнения 3, откомпилируйте, скомпонуй­
те и запустите ее. Что она делает?

ЗАНЯТИЕ 2

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

Части программы C + +

■ Взаимодействие частей
■ Что такое функция и что она делает
■ Простые операции ввода и вывода

42

|

ЗАНЯТИЕ 2. Структура программы на C++

Части программы Hello World
Ваша первая программа C++ (занятие 1, “Первые шаги”) всего лишь выводила на
экран простое приветствие H e llo World. Тем не менее в ней содержатся некоторые
из наиболее важных фундаментальных составляющих программы C++. Давайте вос­
пользуемся листингом 2.1 как отправной точкой для анализа компонентов, содержа­
щихся во всех программах C++.

ЛИСТИНГ 2.1. Ф а й л H e llo W o rld A n a ly sis . срр: а н а л и з

п ро сто й п р о гр а м м ы C++_______

1: // Директива препроцессора, подключающая заголовочный файл iostream
2: #include
3:

4: // Начало программы: блок функции main()
5: int main ()

6:

{

7:
8:
9:
10:
11:

12 :

/* Вьюод на экран */
std::cout « "Hello World"

« std::endl;

// Возврат значения операционной
return 0;

системе

}

Эту программу C++ можно грубо разделить на две части: директивы препро­
цессора, которые начинаются с символа #, и основную часть, которая начинается с

i n t m a in ().
Строки 1, 4, 7 и 10, начинающиеся с символов / / или /* , являются ком­
ментариями и игнорируются компилятором. Комментарии предназначены
для чтения людьми, а не компилятором.
Более подробная информация о комментариях приведена в следующем
разделе.

Директива препроцессора #include
Как и предполагает его название, препроцессор (preprocessor) — это инструмент,
запускающийся перед фактическим началом компиляции. Директивы препроцессора
(preprocessor directive) — это команды препроцессору, которые всегда начинаются со
знака “диез” (#). В строке 2 листинга 2.1 директива # in c lu d e требует
от препроцессора взять содержимое файла (в данном случае — io stre a m ) и включить
его вместо строки, в которой расположена директива, io s tr e a m — это стандартный
заголовочный файл, который включается потому, что он содержит определение объек­
та потока c o u t, используемого в строке 8 для вывода на экран слов H e llo World.
Другими словами, компилятор смог откомпилировать строку 8, содержащую выраже­
ние s t d : : co u t, только потому, что мы заставили препроцессор включать определение
объекта потока c o u t в строке 2.

Части программы Hello World

ПРИМЕЧАНИЕ

|

43

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

FileA ,

должен использоваться в файле

F ileB ,

то первый файл

необходимо включить в последний. Обычно для этого в файл

F ile B

по­

мещают директиву #in c lu d e :

#include "...путь к файлу FileA\FileA"
При включении самодельного заголовочного файла мы используем кавыч­
ки, а не угловые скобки. Угловые скобки ( о ) обычно используются при
включении стандартных заголовочных файлов.

Тело программы — функция main()
После директив препроцессора следует тело программы, расположенное в функ­
ции m ain ( ) . Выполнение программ на языке C++ всегда начинается с функции
main (). Согласно стандарту перед функцией m ain () указывается тип i n t . Тип i n t в
данном случае — это тип возвращаемого значения функции m ain ( ) .

ПРИМЕЧАНИЕ

Во многих приложениях C++ можно найти вариант функции
глядящий следующим образом:

m ain О,

вы­

int main(int argc, char* argv[])
Это объявление совместимо со стандартом и вполне приемлемо, поскольку
функция m ain () возвращает тип

in t,

а содержимое круглых скобок - это

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

program.exe /DoSomethingSpecific

/D oS om ethingS pecif i c

- это аргумент данной программы, переда­

ваемый операционной системой в качестве параметра для обработки в
функции

m ain ().

Рассмотрим строку 8, фактически выполняющую задачу этой программы.
std::cout «

"Hello World" «

std::endl;

c o u t (“console-out” (вывод на консоль); произносится как see-out (си-аут)), является
инструкцией, фактически выводящей на экран строку H e llo World, c o u t — это по­
ток, определенный в пространстве имен s t d (поэтому и s t d : : co u t), а то, что мы де­
лаем, — это помещение текстовой строки H e llo W orld в данный поток с использова­
нием оператора вывода (или вставки) в поток « . Выражение s t d : :e n d l используется
для завершения строки, а его вывод в поток эквивалентен вставке символа возврата
каретки. Обратите внимание: оператор вывода в поток (stream insertion operator) ис­
пользуется каждый раз, когда в поток нужно вывести новый элемент.

44

ЗАНЯТИЕ 2. Структура программы на C++

Преимущество потоков C++ заключается в одинаковой семантике, используемой
потоками разного типа. В результате различные операции, осуществляемые с одним
и тем же текстом, например вывод в файл, а не на консоль, выглядят одинаково и ис­
пользуют один и тот же оператор « , только для s t d : : fs tr e a m вместо s t d : :c o u t.
Таким образом, работа с потоками становится интуитивно понятной и, когда вы при­
выкаете к одному потоку (такому, как c o u t, выводящему текст на консоль), то без
проблем можете работать с другими (такими, как имеющие тип f stream , и записы­
вающие текстовые файлы на диск).
Более подробная информация о потоках рассматривается на занятии 27, “Примене­
ние потоков для ввода и вывода”.

ПРИМЕЧАНИЕ

Фактический текст, заключенный в кавычки

("H ello World"),

называет­

ся строковым литералом (string literal).

Возврат значения
Функции в языке C++ должны возвращать значение, если иное не указано явным
образом, m ain () — это функция, всегда и обязательно возвращающая целое число.
Это целочисленное значение возвращается операционной системе и, в зависимости от
характера вашего приложения, может быть очень полезным, поскольку большинство
операционных систем предусматривает для других приложений возможность обра­
титься к возвращенному значению. Не так уж и редко одно приложение запускает
другое, и родительскому приложению (запустившему дочернее) желательно знать,
закончило ли дочернее приложение свою задачу успешно. Программист может ис­
пользовать возвращаемое значение функции main () для передачи родительскому при­
ложению сообщения об успехе или неудаче.

ПРИМЕЧАНИЕ

Традиционно программисты возвращают значение 0 в случае успеха и -1
в случае ошибки. Однако тип

in t

(целое число) возвращаемого значения

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

ВНИМАНИЕ!

Язык C++ чувствителен к регистру. Поэтому готовьтесь к неудаче компиля­

In t
s t d : :c o u t.

ции, если напишете
вместо

вместо

i n t , Void

вместо

v o id

или

S td :: Cout

Концепция пространств имен
Причина использования в программе синтаксиса s t d : : cout, а не просто cou t, в том,
что используемый элемент (c o u t) находится в стандартном пространстве имен (std ).
Так что же такое пространство имен (namespace)?

К онцепция пространств имен

45

Предположим, вы не использовали спецификатор пространства имен и обрати­
лись к объекту c o u t , который объявлен в двух известных компилятору местах. Какой
из них компилятор должен использовать? Безусловно, это приведет к конфликту и
неудаче компиляции. Вот где оказываются полезными пространства имен. Простран­
ства имен — это имена, присвоенные частям кода, помогающие снизить вероятность
конфликтов имен. При вызове s t d : : c o u t вы указываете компилятору использовать
именно тот объект c o u t , который доступен в пространстве имен s t d .

ПРИМЕЧАНИЕ

Пространство имен s t d (произносится как “standard" (стандарт)) исполь­
зуется для вызова функций, потоков и утилит, которые были утверждены ISO
Standards Committee.

Многие программисты находят утомительным регулярный ввод при наборе исхо­
дного текста спецификатора s t d при использовании имени c o u t и других подобных
средств, содержащихся в том же пространстве имен. Объявление u s i n g n a m e s p a c e ,
представленное в листинге 2.2, позволит избежать этого повторения.
Л И С Т И Н Г 2 .2 . О б ъ я в л е н и е u s i n g

n a m e s p a c e _____________________________________________

1: // Директива препроцессора
2: #include
3 :

4: // Начало программы
5: int main()
6: {
7:
// Указать компилятору пространство имен для поиска
8:
using namespace std;
9 :

10:
11:

/* Вывод на экран с использованием std::cout */
cout « "Hello World" « endl;

12 :
13:
14:
15: }

// Возврат значения операционной системе
return 0;

Анализ
Обратите внимание на строку 8. Сообщив компилятору, что предполагается ис­
пользовать пространство имен s t d , можно не указывать пространство имен в стро­
ке 11 явно при использовании выражений s t d : : c o u t и s t d : : e n d l.
Листинг 2.3 содержит более ограничительный вариант кода листинга 2.2. Здесь
подключается не все пространство имен полностью, а только те его элементы, кото­
рые предстоит использовать.

46

|

ЗАНЯТИЕ 2. Структура программы на C++

ЛИСТИНГ 2.3. Другая демонстрация ключевого слова u s in g
1: // Директива препроцессора
2: #include

3:
4: // Начало программы
5: int main ()

6: {
7:
8:
9:
10:
11:
12 :
13:
14:
15: }

using std::cout;
using std::endl;
/* Вывод на экран с использованием cout */
cout « "Hello World" « endl;
// Возврат значения операционной системе
return 0;

Анализ
В листинге 2.3 строка 8 листинга 2.2 была заменена строками 7 и 8. Различие меж­
ду инструкциями u s in g nam espace s t d и u s in g s td : :c o u t заключается в том, что
первая позволяет использовать все элементы пространства имен s t d без явного ука­
зания квалификатора s t d : :. Удобство последней в том, что без необходимости устра­
нять неоднозначность пространств имен явно можно использовать только выражения
s t d ::c o u t и s td ::e n d l.

Комментарии в коде C++
Строки 1 ,4, 10 и 13 листинга 2.3 содержат текст на русском языке, но программа
все равно компилируется. Они никак не влияют и на вывод программы. Такие строки
называются комментариями (comment). Комментарии игнорируются компилятором
и обычно используются программистами для пояснений в коде. Следовательно, они
пишутся на человеческом языке (или профессиональном жаргоне).
■ Символ / / означает, что следующая далее строка — комментарий. Например:
/ / Это комментарий
■ Текст, содержащийся между символами /* и * /, также является комментарием,
даже если он занимает несколько строк:
/* Это комментарий,
занимающий две строки * /

Функции в C++

ПРИМЕЧАНИЕ

47

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

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Добавляйте комментарии, объясняющие ра­

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

боту сложных алгоритмов и частей вашей про­
граммы.
Оформляйте комментарии в стиле, принятом
вашим коллективом программистов.

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

Функции в C++
Функции (function) — это элементы, позволяющие разделить содержимое вашего
приложения на функциональные модули, которые могут быть вызваны по вашему вы­
бору. При вызове функция обычно возвращает значение вызывающей функции. Самая
известная функция, конечно, — i n t m ain (). Она распознается компилятором как
отправная точка приложения C++ и должна возвращать значение типа i n t (т.е. целое
число).
У программиста всегда есть возможность, а как правило, и необходимость, созда­
вать собственные функции. В листинге 2.4 приведено простое приложение, которое
использует функцию для отображения текста на экране, используя s t d : :c o u t с раз­
личными параметрами.
ЛИСТИНГ 2.4. Объявление, определение и вызов функции,
демонстрирующей возможности s t d : :c o u t______________
1
2

tinclude
using namespace std;

3
4

5
6
7

8

// Объявление функции
int DemoConsoleOutput();
int main()

48

|

9:
10:

ЗАНЯТИЕ 2. Структура программы на C++
// Вызов функции
DemoConsoleOutput();

11 :
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:

return 0;
}
// Определение, т.е. реализация объявленной ранее функции
int DemoConsoleOutput()
{
cout « "Простой строковый литерал" « endl;
cout « "Запись числа пять: " « 5 « endl;
cout « "Выполнение деления 10/5 = " « 10/5 « endl;
cout « "Пи примерно равно 22/7 = " « 22/7 « endl;
cout « "Более точно Пи равно 22/7 = " « 22.0/7 « endl;
return 0;
}

Результат
Простой строковый литерал
Запись числа пять: 5
Выполнение деления 10/5 = 2
Пи примерно равно 22/7 = 3
Более точно Пи равно 22/7 = 3.14286

Анализ
Интерес представляют строки 5 , 1 0 и 16-25. В строке 5 находится объявление функ­
ции (function declaration), которое в основном указывает компилятору, что вы хотите
создать функцию по имени D em oC onsoleO utput () , возвращающую значение типа
i n t (целое число). Именно из-за этого объявления компилятор соглашается отком­
пилировать строку 10, в которой функция DemoConsoleOutput () вызывается в функ­
ции m ain ( ). Компилятор считает, что где-то далее он встретит определение функции
(function definition), и действительно встречает его позже, в строках 16-25.
Фактически эта функция демонстрирует возможности потока c o u t. Обратите вни­
мание: она выводит не только текст, как в предыдущих примерах, но и результаты
простых арифметических вычислений. Две строки, 21 и 22, отображают результат
вычисления числа “пи” как 22/7, но последний результат немного точнее просто по­
тому, что при делении 22.0 на 7 вы указываете компилятору вычислить результат как
вещественное число (тип d o u b le в терминах C++), а не как целое значение.
Обратите внимание, что функция должна вернуть целое число, и она возвращает
значение 0. Точно так же функция m ain () тоже возвращает значение 0. Поскольку
функция main () делегирует все свои действия функции DemoConsoleOutput (), имело
бы смысл использовать возвращаемое ею значение для возврата значения из функции
m ain ( ) , как это сделано в листинге 2.5.

Ф у н к ц и и в C++

|

49

ЛИСТИНГ 2.5. Использование возвращаемого значения функции_______
1:
2:
3:
4:
5:

#include
using namespace std;

6:

{

// Объявление и определение функции
int DemoConsoleOutput()

7:
8:
9:
10:
11:

cout
cout
cout
cout
cout

«
«
«
«
«

"Простой строковый литерал" « endl;
"Запись числа пять: " « 5 « endl;
"Выполнение деления 10/5 = " « 10/5 « endl;
"Пи примерно равно 22/7 = " « 22/7 « endl;
"Более точно Пи равно 22/7 = " « 22.0/7 « endl;

12 :
13:
14: }
15:
16: int
17: {
18:
19:

20 :

return 0;

main()
// Вызов функции с возвратом результата при выходе
return DemoConsoleOutput();

}

Анализ
Вывод этого приложения такой же, как предыдущего. Небольшие изменения есть
только в способе его получения. Поскольку функция определена (т.е. реализована)
перед функцией main () в строке 5, ее дополнительное объявление уже не нужно. Со­
временные компиляторы C++ понимают это как одновременное объявление и опреде­
ление функции. Функция main () также немного короче. В строке 19 осуществляется
вызовов функции Dem oConsoleO utput () и одновременно возврат ее возвращаемого
значения при выходе из приложения.

ПРИМЕЧАНИЕ

В таких случаях, как здесь, когда функция не обязана принимать решение
или возвращать сообщение об успехе или отказе, можно объявить функцию
с типом возвращаемого значения void:

void DemoConsoleOutput()
Такая функция не может возвращать значение, и ее нельзя использовать
для принятия решения.

Функции могут получать параметры, могут быть рекурсивными, содержать не­
сколько операторов выхода, могут быть перегруженными, встраиваемыми и т.д. Эти
концепции вводятся далее, на занятии 7, “Организация кода с помощью функций”.

50

|

ЗАНЯТИЕ 2. Структура программы на C++

Ввод-вывод с использованием
ПОТОКОВ s t d : : c i n и s t d : : c o u t
Ваш компьютер позволяет взаимодействовать с выполняющимися на нем приложе­
ниями разными способами, а также позволяет этим приложениям взаимодействовать
с вами разными способами. Вы можете взаимодействовать с приложениями, исполь­
зуя клавиатуру или мышь. Информация может быть отображена на экране как текст
или в виде сложной графики, может быть напечатана с помощью принтера на бумаге
или просто сохранена в файловой системе для последующего использования. В этом
разделе рассматривается простейший ввод и вывод информации в языке C++ — ис­
пользование консоли для отображения и ввода информации.
Для работы с консолью используются потоки s t d : : c o u t (для вывода простой тек­
стовой информации) и s t d : : c i n (для чтения информации с консоли; как правило,
с клавиатуры). Вы уже встречались с потоком c o u t при выводе слов H e llo World на
экран в листинге 2.1:
8:

std::cout «

"Hello World" «

std::endl;

В этой инструкции после имени потока c o u t идет оператор вывода « (позволяю­
щий вставить данные в поток вывода) с последующим подлежащим выводу строковым
литералом " H e llo W orld" и символом новой строки в виде выражения s t d : :e n d l.
Применение потока c i n также очень простое; он работает в паре с переменной,
в которую следует поместить вводимые данные:
std::cin »

Переменная ;

Таким образом, за потоком c in следуют оператор извлечения значения » (данные
извлекаются из входного потока) и переменная, в которую следует поместить считы­
ваемые данные. Если вводимые данные разделены пробелом, и их следует сохранить
в двух разных переменных, можно использовать цепочку операторов:
std::cin »

Переменная1 »

Переменная2;

Обратите внимание на то, что поток c in применяется для ввода как текстовых, так
и числовых данных, как показано в листинге 2.6.

ЛИСТИНГ 2.в. Использование потоков c in и c o u t для отображения
числовых и текстовых данных, вводимых пользователем______________________________
1:
2:
3:
4:
5:

#include
#include
using namespace std;
int main()

6: {
7:
8:
9:

// Объявление переменной для хранения целого числа
int inputNumber;

Ввод-вывод с использованием потоков std::cin и std::cout
10:

cout «

51

"Введите целое число: ";

11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

// Сохранить введенное пользователем целое число
cin » inputNumber;
// Аналогично текстовымданным
cout « "Введите ваше имя:
";
string inputName;
cin » inputName;
cout «

inputName «

" ввел " «

inputNumber «

endl;

21 :
22:
23: }

return 0;

Результат
Введите целое число: 2017
Введите ваше имя: Siddhartha
Siddhartha ввел 2017

Анализ
В строке 8 переменная inputN um ber объявляется как способная хранить данные
типа i n t . В строке 10 пользователя просят ввести число, используя поток c o u t, а
введенное значение сохраняется в целочисленной переменной с использованием по­
тока c in в строке 13. То же самое повторяется при сохранении имени пользователя,
которое, конечно, не может содержаться в целочисленной переменной. Для этого ис­
пользуется другой тип — s t r i n g , как видно из строк 17 и 18. Именно поэтому, чтобы
можно было использовать тип s t r i n g в функции m ain (), в строке 2 была включена
директива # in c lu d e < s trin g > . И наконец в строке 20 поток c o u t используется для
отображения введенных имени и числа с промежуточным текстом, чтобы получить
вывод S id d h a rth a ввел 2017.
Это очень простой пример ввода и вывода в C++. Не волнуйтесь, если концеп­
ция переменных пока что вам непонятна: подробно мы рассмотрим ее на следующем
занятии.

ПРИМЕЧАНИЕ

Если я введу пару слов в качестве имени (например,
при выполнении листинга 2.6, то поток
только первое слово -

S id d h a rth a .

c in

S id d h a rth a Rao)

все равно сохранит в строке

Чтобы вводить и сохранять строки

целиком, следует использовать функцию

g e tlin e

() (которая будет рас­

смотрена на занятии 4, “Массивы и строки”, в листинге 4.7.

52

|

ЗАНЯТИЕ 2. Структура программы на C++

Резюме
Это занятие знакомит с основными частями простых программ C++. Здесь проде­
монстрировано, что такое функция m ain (), изложено введение в пространства имен
и основы ввода и вывода на консоль. Вы будете использовать многие из них в каждой
программе, которую пишете.

Вопросы и ответы
■ Что делает директива #include?
Это директива препроцессора, которая выполняется при вызове компилятора. Дан­
ная конкретная директива требует включить содержимое файла, имя которого ука­
зано в угловых скобках о после нее, вместо текущей строки, как если бы оно было
введено в этом месте исходного текста.

■ В чем разница между комментариями / / и /*?
Комментарий после двойной косой черты ( / / ) завершается в конце строки. Ком­
ментарий после косой черты со звездочкой ( / * ) продолжается до тех пор, пока не
встретится завершающий знак комментария (*/). Комментарии двойной косой чер­
ты называют также однострочными комментариями, а косой черты со звездоч­
кой — многострочными комментариями. Помните, что даже конец функции не за­
вершает многострочный комментарий; его необходимо закрыть явно, в противном
случае произойдет ошибка при компиляции.

■ Зачем программе нужны аргументы командной строки?
Чтобы дать пользователю возможность изменять поведение программы. Например,
команда I s в Linux или d i r в Windows позволяет просматривать содержимое теку­
щего каталога или папки. Чтобы просмотреть файлы в другом каталоге, вы можете
указать путь к ним, используя аргументы командной строки, как, например, в вы­
зове I s / и л и d i r \.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления полученных
знаний, а также упражнения, которые помогут применить на практике полученные на­
выки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить задания, а
потом сверьте полученные результаты с ответами в приложении Д, “Ответы”. Если
остались неясными хотя бы некоторые из предложенных ниже вопросов, не присту­
пайте к изучению материала следующего занятия.

Коллоквиум

|

53

Контрольные вопросы
1. Что неправильно в объявлении I n t main () ?
2. Могут ли комментарии быть длиннее одной строки?

Упражнения
1. Отладка. Введите исходный текст программы и откомпилируйте ее. Почему
она не компилируется? Как можно ее исправить?
1: #include
2: void main()
3: {
4:
std::Cout « Is there a bug here?";

5: }
2. Исправьте ошибки в программе из упражнения 1 и, перекомпилировав, запус­
тите ее снова.
3. Измените листинг 2.4 так, чтобы продемонстрировать вычитание (используя
оператор - ) и умножение (используя оператор *).

ЗАНЯТИЕ 3

Использование
переменных
и констант
Переменны е (variable) представляют собой средство, по­
зволяющее программисту временно хранить данные в тече­
ние некоторого конечного времени. Константа (constant) —
это средство, позволяющее программисту определить эле­
мент, который не должен изменяться в процессе выполнения
программы.
На этом занятии...

■ Как объявить и определить переменные и константы
■ Как присвоить значения переменным и манипулировать ими
■ Как вывести значение переменной на экран
■ Как использовать ключевые слова a u to и c o n s te x p r

56

|

ЗАНЯТИЕ 3. Использование переменных и констант

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

Коротко о памяти и адресации
Все компьютеры, смартфоны и другие программируемые устройства имеют микро­
процессор и определенный объем памяти для временного хранения, называемый опе­
ративной памятью (Random Access Memory — RAM). Кроме того, многие устройства
позволяют сохранять данные на долгосрочном запоминающем устройстве, таком как
жесткий диск. Микропроцессор выполняет ваше приложение и использует при этом
оперативную память для загрузки его бинарного кода, а также связанных с ним дан­
ных, включая те, которые отображаются на экране и вводятся пользователем.
Саму оперативную память, являющуюся областью хранения, можно сравнить с
рядом шкафчиков в общежитии, каждый из которых имеет свой номер, т.е. адрес. Что­
бы получить доступ к области памяти, скажем, к ее ячейке 578, процессор нужно с
помощью специальной инструкции попросить выбрать оттуда значение или записать
в нее значение.

Объявление переменных для получения
доступа и использования памяти
Приведенные ниже примеры помогут понять, что такое переменные. Предполо­
жим, вы пишете программу для умножения двух чисел, предоставляемых пользовате­
лем. Пользователя просят ввести два значения — множитель и множимое, и каждое
из этих значений необходимо хранить до момента умножения. В зависимости от того,
что вы хотите делать с результатом умножения, вам может понадобиться хранить эти
значения для более позднего использования в программе. Было бы слишком медлен­
но (и программисты часто ошибались бы), если бы для хранения чисел нужно было
помнить и записывать явные адреса областей памяти (такой, как номер ячейки 578),
поскольку при этом приходилось бы постоянно помнить о том, где какие данные нахо­
дятся, и заботиться о том, чтобы случайно не перезаписать уже хранящиеся в ячейках
памяти данные другими.
При программировании на таких языках, как C++, для хранения значений опреде­
ляют переменные. Определить переменную очень просто по такому шаблону:

Тип_переменной Имя_переменной;
ИЛИ

Тип_переменной Имя_переменной = Начальное_значение ;
Атрибут типа переменной указывает компилятору характер данных, которые могут
храниться в этой переменной, и то, какое количество памяти компилятор должен за­
резервировать для этого. Выбранное программистом имя переменной является более

Что такое переменная

|

57

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

ЛИСТИНГ 3.1. Использование переменных для хранения чисел
и результата их умножения
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
using namespace std;
int main()
{
cout «

"Программа для умножения двух чисел" «

endl;

cout « "Введите первое число: ";
int firstNumber = 0;
cin » firstNumber;

11 :
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24: }

cout « "Введите второе число: ";
int secondNumber = 0;
cin » secondNumber;
// Умножение двух чисел, сохранение результата в переменной
int multiplicationResult =firstNumber*secondNumber;
// Вывод результата
cout « firstNumber « " x " « secondNumber;
cout « " = " « multiplicationResult « endl;
return 0;

Результат
Программа для умножения двух чисел
Введите первое число: 51
Введите второе число: 24
51 х 24 = 1224

Анализ
Это приложение просит пользователя ввести два числа, результат умножения ко­
торых затем выводит. Чтобы использовать введенные пользователем числа, следует
сохранить их в памяти. Переменные firs tN u m b e r и secondN um ber, объявленные
в строках 9 и 13, решают задачу временного хранения введенных пользователем

58

|

ЗАНЯТИЕ 3. Использование переменных и констант

целочисленных значений. Поток s t d : : c i n в строках 10 и 14 используется для по­
лучения введенных пользователем значений и их сохранения в двух целочисленных
переменных. Поток c o u t в строке 21 используется для вывода результата на консоль.
Давайте проанализируем объявление переменной подробнее:
9:

int firstNumber = 0;

Эта строка объявляет переменную типа i n t , который означает целое число, с име­
нем firstN u m b e r. В качестве начального значения переменной присваивается нуле­
вое значение.
Компилятор выполняет задачу по отображению имени этой (и прочих объявлен­
ных в программе) переменной на область памяти и вместо вас заботится о сохране­
нии соответствующей информации об адресах памяти. Таким образом, программист
работает с понятными человеку именами, предоставляя компилятору работу с непо­
средственными адресами памяти и создание команд для работы микропроцессора с
оперативной памятью.

ВНИМАНИЕ!

Хорошие имена переменных важны для написания хорошего, понятного и
удобного в сопровождении кода.
Имена переменных в C++ могут состоять из букв и цифр, но не могут на­
чинаться с цифр, а также содержать пробелы и арифметические операторы
(такие, как +, - и т.п.).
Именами переменных не могут быть зарезервированные ключевые слова.
Например, переменная по имени r e t u r n приведет к ошибке при компи­
ляции.
В именах переменных можно использовать символ подчеркивания, кото­
рый позволяет создавать более понятные, самодокументируемые имена
переменных.

Объявление и инициализация нескольких
переменных одного типа
Переменные f i r s t N u m b e r , s e c o n d N u m b e r и m u l t i p l i c a t i o n R e s u l t в листинге 3.1
имеют одинаковый тип (целое число), но объявляются в трех отдельных строках. При
желании можно было бы уплотнить объявление этих трех переменных до одной стро­
ки кода, которая выглядела бы следующим образом:
int firstNumber = 0, secondNumber = 0, multiplicationResult = 0;

Как видите, язык C++ позволяет объявлять сразу несколько переменных
одного типа, а также объявлять переменные в начале функции. Но все же
объявление переменной непосредственно перед ее первым применением
зачастую оказывается более удобным, поскольку делает код более удобо­
читаемым и вам не требуется долго искать тип переменной - он указан
возле места ее первого применения.

Что такое переменная

ВНИМАНИЕ!

|

59

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

Понятие области видимости переменной
У обычных переменных, подобных рассмотренным выше, есть точно определен­
ная область видимости (scope), в пределах которой к ним можно обращаться и ис­
пользовать хранящиеся в них данные. При использовании вне области видимости имя
переменной не будет распознано компилятором, и ваша программа не будет скомпили­
рована. Вне своей области видимости переменная представляет собой неопознанный
объект, о котором компилятор ничего не знает.
Чтобы лучше понять концепцию области видимости переменной, реорганизуем
программу в листинге 3.1 в функцию M ultiplyN um bers (), которая умножает два чис­
ла и возвращает результат (листинг 3.2).
ЛИСТИНГ 3.2, Демонстрация области видимости переменных_________________________
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:

#include
using namespace std;

20 :

}

void MultiplyNumbers()
{
cout « "Введите первое число: ";
int firstNumber = 0;
cin » firstNumber;
cout « "Введите второе число: ";
int secondNumber = 0;
cin » secondNumber;
// Умножение двух чисел, сохранение результата в переменной
int multiplicationResult =firstNumber
* secondNumber;
// Вьюод результата
cout « firstNumber « " x " « secondNumber;
cout « " = " « multiplicationResult « endl;

21: int main()
22: {
23:
cout « "Программа для умножения двух чисел" «
24:
25:
// Вызов функции, выполняющей всюработу
26:
MultiplyNumbers();

endl;

ЗАНЯТИЕ 3. Использование переменных и констант

60
27:
28:
29:
30:
31:
32: }

// cout «
// cout «

firstNumber « " х " « secondNumber;
" = " « multiplicationResult « endl;

return 0;

Результат
Программа для умножения двух чисел
Введите первое число: 51
Введите второе число: 24
51 х 24 = 1224

Анализ
Код из листинга 3.2 выполняет те же действия, что и код из листинга 3.1, генерируя
тот же вывод. Единственное различие состоит в том, что все действия перенесены в
функцию M ultiplyN um bers (), вызываемую функцией main (). Обратите внимание на
то, что переменные firstN u m b e r и secondNumber не могут использоваться за преде­
лами функции M ultiplyN um bers ( ) . Если убрать комментарий из строки 28 или 29 в
функции m ain ( ) , то компиляция потерпит неудачу с наиболее вероятной причиной
u n d e c la re d i d e n t i f i e r (необъявленный идентификатор).
Дело в том, что переменные firstN u m b e r и secondNumber имеют локальную об­
ласть видимости, а значит, она ограничивается той функцией, в которой они объяв­
лены, в данном случае — функцией M u ltip ly N u m b ers () . Локальная переменная
(local variable) может использоваться в функции от места объявления переменной
до конца функции. Фигурная скобка (}), означающая конец функции, означает также
конец области видимости объявленных в ней переменных. Когда функция закан­
чивается, все ее локальные переменные уничтожаются, а занимаемая ими память
освобождается.
При компиляции объявленные в пределах функции M u ltip ly N u m b ers () пере­
менные уничтожаются по завершении функции и, если они используются в функции
m ain ( ) , происходит ошибка, поскольку эти переменные в ней не были объявлены.

ВНИМАНИЕ!

Если в функции

m ain

() объявить другой набор переменных с теми же

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

M ultiplyN um bers ().
main () как незави­

Компилятор рассматривает переменные в функции

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

Что такое переменная

|

61

Глобальные переменные
Если бы переменные, используемые в функции M ultiplyN um bers () листинга 3.2,
были объявлены не в ней, а за ее пределами, то они были бы пригодны для исполь­
зования и в функции m ain ( ) , и в функции M ultiplyN um bers ( ) . Листинг 3.3 демон­
стрирует глобальные переменные (global variable), имеющие самую широкую область
видимости в программе.
ЛИСТИНГ 3.3. Использование глобальных переменных_______________________________
1:
2:
3:
4:
5:
6:
7:

#include
using namespace std;
// Три глобальные целочисленные переменные
int firstNumber = 0;
int secondNumber = 0;
int multiplicationResult = 0;

8:
9: void MultiplyNumbers()

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:

{

cout « "Введите первое число: ";
cin » firstNumber;
cout « "Введите второе число: ";
cin » secondNumber;
// Умножение двух чисел, сохранение результата в переменной
multiplicationResult = firstNumber * secondNumber;
// Вывод результата
cout « "Вывод из MultiplyNumbers(): ";
cout « firstNumber « " х " « secondNumber;
cout « " = " « multiplicationResult « endl;
}
int main()
{
cout «

"Программа для умножения двух чисел" «

// Вызов функции, выполняющей всю работу
MultiplyNumbers();
cout «

"Вывод из main():

";

// Теперь эта строка компилируется и работает!
cout « firstNumber « " х " « secondNumber;
cout « " = " « multiplicationResult « endl;
return 0;
}

endl;

62

|

ЗАНЯТИЕ 3. Использование переменных и констант

Результат
Программа для умножения двух чисел
Введите первое число: 51
Введите второе число: 19
Вьюод из MultiplyNumbers(): 51 х 19 = 969
Вывод из main(): 51 х 19 = 969

Анализ
Листинг 3.3 выводит результат умножения в двух функциях, причем переменные
firstN u m b e r, secondNumber и m u l t i p l i c a t i o n R e s u l t объявлены за их пределами.
Эти переменные глобальны (global), поскольку были объявлены в строках 5 -7 , вне
области видимости всех функций. Обратите внимание на строки 22 и 35, которые
используют эти переменные и отображают их значения. Обратите особое внимание
на то, что переменная m u l t i p l i c a t i o n R e s u l t сначала получает значение в функции
M ultiplyN um bers (), которое потом повторно используется в функции main ().

ВНИМАНИЕ!

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

m ain

() без применения глобальных переменных - использовать

возврат результата умножения функцией

M ultiplyN um bers ().

Соглашения об именовании
Если вы еще не обратили на это внимания, заметьте, что мы дали функции имя

M ultiplyN um bers (), в котором каждое слово начинается с прописной буквы (стиль
языка программирования Pascal), в то время как переменные firstN u m b e r, seco n d ­
Number и m u l t i p l i c a t i o n R e s u l t имеют имена, первое слово которых начинается с
буквы в нижнем регистре (так называемый “верблюжий стиль”). Именно этому со­
глашению мы следуем дальше в этой книге — в именах переменных используется
“верблюжий стиль”, в то время как другие объекты, такие как имена функций, следу­
ют стилю Pascal.
Вы можете встретить код C++, в котором имя переменной предваряется символа­
ми, описывающими тип переменной. Это соглашение называется венгерской нота­
цией и часто используется в программировании в Windows. Так, переменная f i r s t ­
Number в венгерской нотации имела бы имя iF irstN u m b e r, где префикс i означает
тип i n t . Глобальная переменная имела бы имя g iF irstN u m b e r. Венгерская нотация
в последние годы теряет популярность, частично из-за улучшения интегрированных
сред разработки, при необходимости отображающих тип переменной, например при
наведении на них указателя мыши.

Распространенныетипы переменных, поддерживаемые компилятором C++

|

63

Ниже приводятся часто встречающиеся примеры плохих имен переменных:
int i = 0;
bool b = false;

Имя переменной должно указывать ее предназначение, так что эти переменные
было бы лучше объявить следующим образом:
int totalCash = 0;
bool isLampOn = false;

ВНИМАНИЕ!

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

Распространенные типы переменных,
поддерживаемые компилятором C++
В большинстве примеров до сих пор определялись переменные типа i n t , т.е. це­
лые числа. Однако в распоряжении программистов C++ есть множество других фун­
даментальных типов переменных, непосредственно поддерживаемых компилятором.
Выбор правильного типа переменной так же важен, как и выбор правильных инстру­
ментов для работы! Крестообразная отвертка не подойдет для работы с шурупом под
плоскую, точно так же целое число без знака не может использоваться для хранения
отрицательного значения! Типы переменных и характер данных, которые они могут
содержать, приведены в табл. 3.1. Эта информация очень важна при написании эффек­
тивных и надежных программ C++.

ТАБЛИЦА 3.1. Типы переменных
Тип

Типичный диапазон значений

b ool
ch a r
u n sig n ed s h o r t i n t
sh o rt in t
u n sig n ed long i n t
long i n t
u n sig n ed long lo n g
long lo n g
i n t (16 бит)

t r u e (истина) или f a l s e (ложь)
256 символьных значений
ОтОдо 65 535
От -32 768 до 32 767
О тО д о 4294967 295
От -2 147 483 648 до 2 147 483 647
ОтОдо 18446 744073 709 551 615
От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807
О т -32 768 до 32 767

64

|

ЗАНЯТИЕ 3. Использование переменных и констант
Окончание табл. 3.1

Тип

Типичный диапазон значений

i n t (32 бита)
u n sig n e d i n t (16 бит)
u n sig n e d i n t (32 бита)
flo a t
d ou b le

От -2 147 483 648 до 2 147 483 647
ОтО до 65 535
От 0 до 4 294 967 295
От 1.2е-38 до 3.4е38
От 2.2е-308 до 1.8е308

Более подробная информация о важнейших типах приведена в следующих раз­
делах.

Использование типа b o o l
для хранения логических значений
Язык C++ предоставляет тип, специально созданный для хранения логических
значений tr u e или f a l s e (оба являются зарезервированными ключевыми словами
C++). Этот тип особенно полезен при хранении настроек и флагов, которые могут
быть установлены или сброшены, существовать или отсутствовать, быть доступными
или недоступными.
Типичное объявление инициализированной логической переменной имеет следую­
щий вид:
bool alwaysOnTop = false;

Выражение, вычисляющее значение логического типа, может иметь следующий вид:
bool deleteFile = (userSelection == "yes");
// Истинно, если переменная userSelection содержит "yes",
/ / в противном случае - ложно

Условные выражения рассматриваются на занятии 5, “Выражения, инструкции и
операторы”.

Использование типа c h a r
для хранения символьных значений
Тип ch a r используется для хранения одного символа. Типичное объявление по­
казано ниже.
char userlnput = 'У; // Инициализация символом 1Y 1

Обратите внимание, что память состоит из битов и байтов. Биты могут содержать
значения 0 и 1, а байты могут хранить числовые представления, использующие эти
биты. Таким образом, работая или присваивая символьные данные, как показано в
примере, компилятор преобразует символы в числовое представление, которое мо­
жет быть помещено в память. Числовое представление латинских символов A -Z , a-z,

Распространенныетипы переменных, поддерживаемые компилятором C++

65

чисел 0 -9 , некоторых специальных клавиш (например, ) и специальных симво­
лов (таких, как “забой”), было стандартизовано в американском коде обмена информа­
цией (American Standard Code for Information Interchange), именуемом также ASCII.
Вы можете открыть таблицу в приложении Г, “Коды ASCII”, и увидеть, что символ
’ Y1, присвоенный переменной u s e r In p u t, имеет согласно стандарту ASCII десятич­
ное значение 89. Таким образом, компилятор просто сохраняет значение 89 в области
памяти, зарезервированной для переменной u s e r ln p u t.

Концепция знаковых и беззнаковых целых чисел
Знак (sign) делает число положительным или отрицательным. Все числа, с которы­
ми работает компьютер, хранятся в памяти просто как биты и байты. Область памя­
ти размером 1 байт содержит 8 битов. Каждый бит может содержать значение 0 или
1 (т.е. хранить только одно из этих двух значений). Таким образом, область памяти
размером I байт может хранить одно из 2 в степени 8, т.е. 256 разных значений. Ана­
логично область памяти размером 16 битов может хранить одно из 2 в степени 16
разных значений, т.е. одно из 65536 уникальных значений.
Если эти значения должны быть беззнаковыми, т.е. память может содержать только
положительные значения, то один байт мог бы содержать целочисленные значения
в пределах от 0 до 255, а два байта будут содержать значения в пределах от 0 до
65535 соответственно. Загляните в табл. 3.1 и обратите внимание на то, что тип un­
sig n e d s h o r t i n t , который поддерживает этот диапазон, занимает в памяти 16 бит.
Таким образом, положительные значения в битах и байтах очень просто представить
схематически (рис. 3.1).
Бит 15

.........

Бит О

11111111111111

= 65535

16 битов, хранящих значение

РИС. 3.1. Организация битов в 16-разрядном
коротком беззнаковом целом числе
Но как же представить в этой же области отрицательные числа? Один из сп осо­
бов — “пожертвовать” одним из разрядов для хранения знака, который указывал бы,
положительное или отрицательное значение содержится в других битах (рис. 3.2).
Такой знаковый разряд имеет смысл делать самым старшим битом (Most-SignificantBit — M SB) для согласованности знаковых и беззнаковых чисел, например чтобы у
обоих типов самый младший бит (Least-Significant-Bit — LSB) указывал нечетность
числа. Если старший бит содержит информацию о знаке, предполагается, что значе­
ние 0 означает положительное число, а значение 1 — отрицательное. Другие биты
содержат абсолютное значение числа.

66

|

ЗАНЯТИЕ 3. Использование переменных и констант
Бит 15

......................

Бит О

1 11111111111111
15 битов содержат абсолютное значение


Бит знака
0 означает положительное целое число
1 означает отрицательное целое число

РИС- 3.2. Организация битов в 16-битовом
коротком знаковом целом числе
Таким образом, занимающее 8 битов знаковое число может содержать значения в
пределах от -1 2 8 до 127, а занимающее 16 битов — значения в пределах от -3 2 768
до 32 767. Еще раз посмотрите на табл. 3.1 и обратите внимание на то, что тип sh o r t
i n t (знаковый) поддерживает положительные и отрицательные целочисленные значе­
ния в 16-разрядном пространстве1.

Знаковые целочисленные типы
s h o r t , i n t , lo n g И lo n g lo n g
Эти типы различаются своими размерами, а следовательно, и диапазоном значе­
ний, которые могут содержать. Тип i n t (самый популярный целочисленный тип) у
большинства современных компиляторов имеет размер 32 бита. Используйте под­
ходящий тип в зависимости от максимального значения, которое предположительно
будет содержать определенная переменная.
Объявление переменной знакового типа очень простое:
short
int
int
long
long long

smallNumber
= -100;
largerNumber
= -70000;
possiblyLargerThanlnt =
-70000;
largerThanlnt
= -70000000000;

Беззнаковые целочисленные типы u n s ig n e d s h o r t ,
u n s ig n e d i n t , u n s ig n e d lo n g и u n s ig n e d lo n g lo n g
В отличие от знаковых аналогов, беззнаковые целочисленные типы не могут со­
держать информацию о знаке, зато могут содержать вдвое большие положительные
значения.

1
Здесь автор изложил не более чем наброски использования знакового бита. На самом деле
ситуация гораздо сложнее, и существует несколько вариантов представления отрицательных
чисел в компьютерах. Заинтересованному читателю предлагается ознакомиться с этой темой в
Интернете или соответствующей литературе; язык C++ на битовом уровне со знаковыми чис­
лами фактически не работает. — П рим еч. р ед.

Распространенныетипы переменных, поддерживаемые компилятором C++

|

67

Объявление переменной беззнакового типа тоже очень простое:
unsigned
unsigned
unsigned
unsigned

short int
int
long
long long

ПРИМЕЧАНИЕ

smallNumber
largerNumber
possiblyLargerThanlnt
largerThanlnt

=
=
=
=

255;
70000;
70000;
70000000000;

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

in t;

воспользуйтесь типом

u n sig n e d

in t.

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

ВНИМАНИЕ!

Беззнаковый тип нельзя использовать в банковском приложении для пе­
ременной, хранящей остаток на счете, поскольку банки обычно допускают
отрицательные значения на счету, предоставляя кредит своим клиентам.
Пример, демонстрирующий разницу между знаковыми и беззнаковыми
типами, вы найдете в листинге 5.3 занятия 5, “Выражения, инструкции и
операторы”.

Избегайте переполнения,
выбирая подходящие типы
Типы данных, такие как s h o r t, i n t , long, u n s ig n e d s h o r t, u n s ig n e d i n t , un­
s ig n e d lo n g и другие, имеют ограниченную емкость и могут содержать числа, не
превышающие некоторые пределы. Превысив предел для выбранного типа, вы по­
лучаете переполнение.
Возьмем в качестве примера u n sig n e d s h o r t. Этот тип данных обычно содержит
16 бит, а потому может содержать значения от 0 до 65 535. Если вы прибавите 1 к
65535 в переменной типа u n s ig n e d s h o r t, циклический переход приведет к значе­
нию 0. Это напоминает счетчик пробега автомобиля, у которого происходит меха­
ническое переполнение, если он может показывать только пять цифр, а автомобиль
проехал 99999 км.
В этом случае тип u n s ig n e d s h o r t явно не является правильным выбором для
такого счетчика. Программист должен выбирать тип u n sig n e d i n t , если ему необхо­
димо работать с числами, большими, чем 65535.
В случае типа s ig n e d s h o r t, диапазон которого — от -3 2 7 6 8 до 32767, прибавле­
ние 1 к 32767 может привести к наибольшему по модулю представимому отрицатель­
ному значению. В случае знаковых чисел стандарт не определяет конкретное поведе­
ние, позволяя принять решение конкретному компилятору (и его разработчикам).
В листинге 3.4 продемонстрированы циклический переход и ошибка переполне­
ния, которые вы можете непреднамеренно получить при выполнении арифметических
операций.

|

68

ЗАНЯТИЕ 3. Использование переменных и констант

ЛИСТИНГ 3.4. Демонстрация переполнения знаковых
и беззнаковых целых чисел
0: #include
1: using namespace std;
2:

3: int main()
4: {
5:
unsigned short uShortValue = 65535;
6:
cout « "Увеличение unsigned short " «
7:
cout « ++uShortValue « endl;

uShortValue «

";

8:
9:
10:
11:

short signedShort = 32767;
cout « "Увеличение signed short " «
cout « ++signedShort « endl;

signedShort «

": ";

12 :
13:
14: }

return 0;

Результат
Увеличение unsigned short 65535: 0
Увеличение signed short 32767: -32768

Анализ
Выходные данные показывают, что непреднамеренное переполнение приводит к
непредсказуемому и непонятному на интуитивном уровне поведению приложения.
Строки 7 и 11 увеличивают переменные типа u n sig n e d s h o r t и s ig n e d s h o r t, ко­
торые были инициализированы их максимальными представимыми значениями —
65535 и 32767 соответственно. Вывод программы показывает значения, которые они
получают после операции увеличения, а именно — циклический переход 65535 до
нуля в случае u n s ig n e d s h o r t и переполнение 32767 до -3 2 7 6 8 в случае s ig n e d
s h o r t. Вряд ли вы ожидаете, что в результате операции увеличения рассматриваемое
значение уменьшается, но именно это и происходит при переполнении целочисленно­
го типа. Если вы используете такие значения для выделения памяти, то можете запро­
сить у операционной системы нуль байт, в то время как на самом деле вам требуется
64 Кбайта — 65 536 байт.

ПРИМЕЧАНИЕ

Операции

++uShortV alue

и

+ + sig n ed S h o rt,

показанные в листинге

3.4 в строках 7 и И , являются операциями префиксного инкремента. Они
подробно рассматриваются на занятии 5, “Выражения, инструкции и опе­
раторы”.

Определение размера переменной с использованием оператора sizeof

69

Типы с плавающей точкой f l o a t и d o u b le
Числа с плавающей точкой вы, вероятно, изучали в школе как вещественные чис­
ла. Они могут быть положительными и отрицательными, а также содержать десятич­
ные значения. Так, если в переменной C++ необходимо сохранить значение числа л
(3,141592... или, приближенно, 22/7), можете использовать для нее тип с плавающей
точкой.
Объявление переменных этих типов следует тому же шаблону, что и тип i n t в лис­
тинге 3.1. Так, переменная типа f l o a t , позволяющая хранить десятичные значения,
могла бы быть объявлена следующим образом:
float pi = 3.1415926;

Переменная вещественного типа с двойной точностью (типа d ou b le) определяется
аналогично:
double morePrecisePi = 3.1415926;
Стандарт C++14 добавляет возможность использовать при записи числа
разделитель разрядов в виде одинарной кавычки, что повышает удобочитаемость кода:

int moneylnBank
long populationChange
long long countryGDPChange
double pi

ПРИМЕЧАНИЕ

=
=
=
=

-70'000;
-85'000;
-70'000'ООО'000;
3.141'592'653'59;

//
//
//
//

-70000
-85000
-70 млрд.
3.14159265359

Рассматривавшиеся до этого момента типы данных зачастую называют

простыми старыми данными

(Plain Old Data - POD). К этой категории от­

носятся также объединения этих типов, такие как структуры, перечисления
или объединения.

Определение размера переменной
с использованием оператора s i z e o f
Размер представляет собой объем памяти, резервируемый компилятором при
объявлении программистом переменной для хранения присваиваемых ей данных.
Размер переменной зависит от ее типа, и в языке C++ есть очень удобный оператор
s i z e o f , который возвращает размер переменной или типа в байтах.
Применение оператора s i z e o f очень простое. Чтобы определить размер целого
числа, вызовите оператор s i z e o f с параметром в виде типа i n t , как показано в лис­
тинге 3.5.
cout «

"Размер int: " «

sizeof(int);

70

|

ЗАНЯТИЕ 3. Использование переменных и констант

ЛИСТИНГ 3.5. Поиск размера стандартных типов языка C++
1

iinclude

2

3

int main()

4
5

{
using namespace std;
cout « "Размеры некоторых встроенных типов C++" «

6

endl;

7

8

cout«"bool
:
cout«"char
:
cout «"unsigned short int:
cout«"short int
:
cout «"unsigned long int :
cout«"long
:
cout«"int
:
cout«"unsigned long long:
cout«"long long
:
cout«"unsigned int
:
cout«"float
:
cout«"double
:

9

10
11

12
13
14
15
16
17
18
19

20
21
22
23
24

cout «

"«sizeof(bool)
«endl;
"«sizeof (char)
«endl;
"«sizeof (unsigned short)
«endl;
"«sizeof(short)
«endl;
"«sizeof (unsigned long)
«endl;
"«sizeof(long)
«endl;
"«sizeof(int)
«endl;
"«sizeof (unsigned long long) «endl;
"«sizeof (long long)
«endl;
"«sizeof (unsigned int)
«endl;
"«sizeof(float)
«endl;
"«sizeof(double)
«endl;

"Вывод зависит от компилятора, компьютера и ОС" «endl;

return 0;

}

Результат
Размеры некоторых встроенных типов C++
bool
: 1
char
: 1
unsigned short int: 2
short int
: 2
unsigned long int : 4
long
: 4
int
: 4
unsigned long long: 8
long long
: 8
unsigned int
: 4
float
: 4
double
: 8
Вывод зависит от компилятора, компьютера и ОС

Анализ
Вывод листинга 3.5 демонстрирует размеры различных типов в байтах (которые
зависят от конкретной платформы: компилятора, операционной системы и аппарат­
ных средств). Данный конкретный вывод — это результат выполнения программы в

Определение размера переменной с использованием оператора sizeof

|

71

32-битовом режиме (32-битовый компилятор) в 64-битовой операционной системе.
Обратите внимание, что 64-битовый компилятор, возможно, даст другие результаты,
а 32-битовый компилятор автор выбрал потому, что должен был иметь возможность
запускать приложение как на 32-, так и на 64-битовых системах. Вывод оператора
s iz e o f свидетельствует о том, что размер переменной знакового и беззнакового типов
одинаков; единственным различием этих двух типов является знаковый старший бит.
Все размеры в выводе приведены в байтах. Размер типа - важный па­
раметр при выделении памяти для переменной, особенно для типов, ис­
может хранить числа из

меньшего диапазона, чем

не можете использовать

тип

СОВЕТ

sh o rt in t
lo n g long. Так что вы

пользуемых для хранения чисел. Тип

sh o rt in t

Стандарт C++И

для хранения, например, численности населения страны.

вводит целочисленные типы фиксированного размера,

позволяющие выбрать тип с точным количеством битов. Это

u in t8 _ t

in t8 _ t

и

для 8-битовых знаковых и беззнаковых целых. Имеются также

целочисленные типы размером 16 бит
ром 32 бита

( in t3 2 _ t

и

u in t3 2 _ t)

( i n t l 6 _ t и u in t l6 _ t ) , разме­
( in t6 4 _ t и u in t6 4 _ t).

и 64 бита

Для их использования требуется включить в программу заголовочный файл

< c s td in t> .

Запрет сужающего преобразования
при использовании инициализации списком
При инициализации переменной меньшего целочисленного типа (скажем, s h o r t)
значением переменной большего типа (скажем, i n t ) вы рискуете получить ошибку
сужающего преобразования, при которой компилятор должен преобразовать значение,
хранящееся в типе, который потенциально может содержать гораздо большие числа,
в тип, который имеет меньшие размеры, например:
int largeNum
= 5000000;
short smallNum = largeNum; // Компилируется, но возможна ошибка

Сужение не ограничивается преобразованиями только между целочисленными типа­
ми. С этой ошибкой можно столкнуться при инициализации f l o a t с помощью double,
инициализации f l o a t (или do u b le) с использованием i n t или i n t с помощью f l o a t .
Некоторые компиляторы могут предупреждать о возможной ошибке, но это предупре­
ждение не является критичной ошибкой, которая приведет к прекращению компиляции.
В таких случаях вы можете столкнуться с ошибками, которые трудно выловить, так как
это ошибки времени выполнения, которые могут происходить достаточно редко.
Чтобы избежать этой проблемы, C++11 рекомендует инициализацию списком, ко­
торая предотвращает сужение. Для использования этой возможности поместите зна­
чения или переменные инициализации в фигурные скобки {}. Синтаксис инициали­
зации списком выглядит следующим образом:

72

|

ЗАНЯТИЕ 3. Использование переменных и констант

int largeNum = 5000000;
short anotherNum{ largeNum };
int anotherNum{ largeNum };
float someFloat{ largeNum };
float someFloat{ 5000000 };

//
//
//
//

Ошибка сужения!
OK!
Ошибка! Возможно сужение
OK! 5000000 помещается в float

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

Автоматический вывод типа
с использованием auto
В ряде случаев тип переменной очевиден — по присваиваемому при инициализа­
ции значению. Например, если переменная инициализируется значением t r u e , сле­
дует ожидать, что, скорее всего, типом переменной будет b o o l. Компиляторы с под­
держкой C++11 и выше дают возможность определять тип неявно, с использованием
вместо типа переменной его ключевого слова au to :
auto coinFlippedHeads = true;

Здесь задача определения конкретного типа переменной coin F lip p ed H ead s остав­
лена компилятору. Компилятор просто проверяет природу значения, которым инициа­
лизируется переменная, а затем выбирает тип, наилучшим образом подходящий для
этой переменной. В данном случае для инициализирующего значения t r u e лучше
всего подходит тип b o o l. Таким образом, компилятор определяет тип b o o l как наи­
лучший для переменной co in F lip p e d H ea d s и внутренне рассматривает ее как имею­
щую тип b o o l, что и демонстрирует листинг 3.6.
ЛИСТИНГ 3-6- Использование ключевого слова a u to

для выведения типов компилятором
1:
2:
3:
4:
5:
6:
7:

#include
using namespace std;
int main()
{
auto coinFlippedHeads = true;
auto largeNumber = 2500000000000;

8:
9:
10:

11 :
12:
13:
14:
15:
16:
17: }

cout
cout

«
«
«
cout «
cout «
«

"coinFlippedHeads = " « coinFlippedHeads;
" , sizeof(coinFlippedHeads) = ”
sizeof(coinFlippedHeads) « endl;
"largeNumber = " « largeNumber;
" , sizeof(largeNumber) = "
sizeof(largeNumber) « endl;

return 0;

Использование ключевого слова typedef для замены типа

|

73

Результат
coinFlippedHeads = 1 , sizeof(coinFlippedHeads) = 1
largeNumber = 2500000000000 , sizeof(largeNumber) = 8

Анализ
Как можно заметить, вместо явного указания типа b o o l для переменной c o i n ­
F lip p ed H ead s и типа lo n g lo n g для переменной largeN um ber в строках 6 и 7, где
они объявляются, было использовано ключевое слово a u to . Это ключевое слово д е­
легирует принятие решения о типе переменных компилятору, который использует для
этого инициализирующее значение. Чтобы проверить, создал ли компилятор факти­
чески предполагаемые типы, используется оператор s iz e o f , позволяющий убедиться,
что это действительно так.
Использование ключевого слова

a u to

требует инициализации перемен­

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

a u to

приведет к ошибке при компиляции.

Хотя, на первый взгляд, ключевое слово a u to кажется не особенно полезным, оно
существенно упрощает программирование в тех случаях, когда тип переменной сло­
жен. Роль ключевого слова a u to в написании более простых, но безопасных с точки
зрения использования типов программ рассматривается на занятиях с 15, “Введение в
стандартную библиотеку шаблонов”, и далее.

Использование ключевого слова
typedef для замены типа
Язык C++ позволяет переименовывать типы переменных так, как вам кажется бо­
лее удобным. Для этого используется ключевое слово ty p e d e f. Например, програм­
мист хочет назначить типу u n sig n e d i n t более описательное имя STRICTLY_POSI-

TIVE_INTEGER.
typedef unsigned int STRICTLY_POSITIVE_INTEGER;
STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;

При компиляции первая строка указывает компилятору, что STRICLY_POSITIVE_
INTEGER — это не что иное, как тип u n sig n e d i n t . Впоследствии, когда компилятор
встречает уже определенный тип STRICLY_POSITIVE_INTEGER, он заменяет его типом
u n sig n ed i n t и продолжает компиляцию.

74

|

ЗАНЯТИЕ 3. Использование переменных и констант

ПРИМЕЧАНИЕ

ty p e d e f или подстановка типа особенно удобна при работе со сложными
типами, у которых может быть громоздкий синтаксис, например при ис­
пользовании шаблонов. Шаблоны рассматриваются на занятии 14, “Вве­
дение в макросы и шаблоны”.

Что такое константа
Предположим, вы пишете программу для вычисления площади и периметра круга.
Формулы таковы:
Площадь = pi * Радиус * Радиус;
Периметр = 2 * pi * Радиус

В данных формулах p i — это константа со значением 3 ,1 41592... Вы хотите из­
бежать случайного изменения значения p i где-нибудь в вашей программе. Вы также
не хотите случайно присвоить p i неправильное значение, скажем, при небрежном
копировании и вставке или при контекстном поиске и замене. Язык C++ позволяет
определить p i как константу, которая не может быть изменена после объявления. Дру­
гими словами, после того как значение константы определено, оно не может быть из­
менено. Попытки присваивания значения константе в языке C++ приводят к ошибке
при компиляции.
Таким образом, в C++ константы похожи на переменные, за исключением того,
что они не могут быть изменены. П одобно переменной, константа также занимает
пространство в памяти и имеет имя для идентификации адреса выделенной для нее
области. Однако содержимое этой области не может быть перезаписано. В языке C++
возможны следующие константы.
■ Литеральные константы.
■ Константы, объявленные с использованием ключевого слова c o n st.
■ Константные выражения, использующие ключевое слово c o n ste x p r (нововведение
С++11).
■ Константы перечислений, использующие ключевое слово enum.
■ Константы, определенные с помощью макроопределений, использование которых
не рекомендуется и осуждается.

Литеральные константы
Литеральные константы могут быть многих типов — целочисленные, строки и т.д.
В нашей первой программе в листинге 1.1 строка " H e llo W orld" выводится с помо­
щью следующей инструкции:
std::cout «

"Hello World" «

std::endl;

Здесь "H ello W orld" — это константа строкового литерала (string literal). Когда
вы объявляете целое число наподобие
int someNumber = 10;

Что такое константа

|

75

целочисленной переменной so m e N u m b e r присваивается начальное значение, равное
10. Здесь 10 — это часть кода, компилируемая в приложение, которая является неиз­
менной и тоже является литеральной константой (literal constant). Вы можете ини­
циализировать целочисленную переменную в восьмеричной записи:
int someNumber = 012; // Восьмеричное 12 равно десятичному 10

Начиная с С++14 можно использовать бинарные литералы:
int someNumber = 0Ы010; // Двоичное 1010 равно десятичному 10

СОВЕТ

C++ позволяет определять собственные литералы, например температуру
0 . 0_С, расстояние 1 0 _ km и т.д.
Эти суффиксы (наподобие _С, _km) называются пользовательскими литера­
лами. О них речь пойдет на занятии 12, “Типы операторов и их перегрузка”.

Объявление переменных как констант
с использованием ключевого слова c o n s t
Самый важный тип констант C++ с практической и программной точек зрения
объявляется с помощью ключевого слова c o n s t , расположенного перед типом пере­
менной. В общем виде объявление выглядит следующим образом:
const имя_типа имя_константы = значение ;

Давайте рассмотрим простое приложение, которое отображает значение константы
по имени p i (листинг 3.7).
ЛИСТИНГ 3,7. О б ъ я в л е н и е к о н ста н ты p i __________________________________________________
1: #include
2:
3: int main()
4:

{

5:

using namespace std;

6:

7:
8:
9:
10:
11:

const double pi = 3.1415926;
cout « "Значение pi равно: " «

pi «

endl;

// Удаление следующего комментария ниже ведет к ошибке:
// pi = 345;

12 :
13:
14: }

return 0;

Результат
Значение pi равно: 3.1415926

76

|

ЗАНЯТИЕ 3. Использование переменных и констант

Анализ
Обратите внимание на объявление константы p i в строке 7. Ключевое слово c o n st
позволяет указать компилятору, что p i — это константа типа d o u b le. Если убрать
комментарий со строки 11, в которой предпринимается попытка присвоить значение
переменной, которую вы определили как константу, произойдет ошибка при компиля­
ции примерно с таким сообщением: You cannot assign to a variable that is const (Вы не
можете присвоить значение переменной, которая является константой). Таким обра­
зом, константы — это прекрасное средство гарантировать неизменность определен­
ных данных.
Хорошей практикой программирования является определение перемен­
ных, значения которых предполагаются неизменными, как констант. При­
менение ключевого слова

c o n s t указывает, что программист позаботился

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

Константы полезны при объявлении массивов постоянной длины, которые не­
изменны во время компиляции. В листинге 4.2 из занятия 4, “Массивы и строки”,
содержится пример использования синтаксиса c o n s t i n t при определении длины
массива.

Объявление констант с использованием
ключевого слова c o n s te x p r
Ключевое слово c o n s te x p r позволяет объявлять константы подобно функциям:
constexpr double GetPi() {return 3.1415926;}

Одно c o n s te x p r -выражение может использовать другое:
constexpr double TwicePiO

{return 2 * GetPi();}

c o n s te x p r может выглядеть как функция, однако обеспечивает возможности опти­
мизации с точки зрения компилятора и приложения. До тех пор, пока компилятор в
состоянии вычислять константноевыражение как конкретное значение, оно может
использоваться в инструкциях и выражениях везде, где ожидается константа. В пред­
ыдущем примере T w icePi () является выражением c o n s te x p r , использующим кон­
стантное выражение G e tP i ( ) . Скорее всего, это приведет к оптимизации во время
компиляции, при которой компилятор каждый вызов Tw icePi () просто заменяет чис­
лом 6 . 2831852, а не кодом, который будет вызывать G etP i () и умножать возвращен­
ное им значение на 2.
В листинге 3.8 показан пример использования c o n ste x p r .

Что такое константа

|

77

ЛИСТИНГ 3.8. И с п о л ь з о в а н и е c o n s t e x p r для в ы ч и сл е н и я p i _______________
1:
2:
3:
4:
5:
6:

7:
8:
9:
10:
11:
12:
13:
14:

#include
constexpr double GetPi() { return 3.141593; }
constexpr double TwicePiO { return 2 * GetPiO;

}

int main ()
{

using namespace std;
const double pi = 3.141593;
cout « "Константа pi равна " « pi « endl;
cout « "constexpr GetPiO возвращает " « GetPiO « endl;
cout « "constexpr TwicePiO возвращает " « TwicePiO « endl;
return 0;
}

Результат
Константа pi равна 3.141593
constexpr GetPiO возвращает 3.141593
constexpr TwicePiO возвращает 6.283186

Анализ
Программа демонстрирует два способа получения значения числа п: как констант­
ной переменной p i , объявленной в строке 8, и как константного выражения G e t P i ( ) ,
объявленного в строке 2. G e t P i () и T w i c e P i () могут казаться функциями, но это не
совсем функции. Дело в том, что функции вызываются во время выполнения програм­
мы. Эти же константные выражения компилятор заменяет числом 3 .1 4 1 5 9 3 при каж­
дом использовании G e t P i () и числом 6 .2 8 3 1 8 6 при использовании T w i c e P i (). Такое
разрешение T w i c e P i () в константу увеличивает скорость выполнения программы по
сравнению выполнением вычисления, содержащегося в функции.

СОВЕТ

Константные выражения должны содержать простые вычисления, возвра­
щающие простые типы, такие как i n t , d o u b le и т.п. C++14 позволяет
c o n s t e x p r -выражениям содержать инструкции принятия решения, такие
как i f и s w i t c h . Подробно эти условные инструкции рассматриваются на
занятии 6, “Управление потоком выполнения программы”.
Использование c o n s t e x p r не гарантирует оптимизацию времени компи­
ляции, например если вы используете выражение c o n s t e x p r для удвое­
ния числа, предоставляемого пользователем. Компилятор не в состоянии
получить результат вычисления такого выражения во время компиляции,
так что он может игнорировать модификатор c o n s t e x p r и компилировать
выражение, как обычную функцию.
Как константное выражение используется там, где компилятор ожидает
константу, показано в листинге 4.2 занятия 4, "Массивы и строки”.

78

|

ЗАНЯТИЕ 3. Использование переменных и констант

СОВЕТ

В предыдущих примерах мы определяли собственную константу p i просто
как пример синтаксиса объявления констант и использования ключевого
слова c o n s t e x p r . Но большинство популярных компиляторов C++ со­
держат более точное значение числа

тс в

константе М _Р1, которую можно

использовать в своих программах после включения заголовочного файла
< cm ath> .

Перечисления
Иногда некая переменная должна принимать значения только из определенного
набора. Например, вы не хотите, чтобы среди цветов радуги случайно оказался би­
рюзовый или среди направлений компаса оказалось направление влево. В обоих этих
случаях необходим тип переменной, значения которой ограничиваются определенным
вами набором. Перечисления (enumerations) — это именно то, что необходимо в дан­
ной ситуации. Перечисления объявляются с помощью ключевого слова enum.
Вот пример перечисления, которое определяет цвета радуги:
enum RainbowColors

{
Violet = О,
Indigo,
Blue,
Green,
Yellow,
Orange,
Red

};
А вот другой пример — направления компаса:
enum CardinalDirections

{
North,
South,
East,
West

};
Перечисления используются как пользовательские. Переменные этого типа могут
принимать значения, ограниченные объявленными ранее значениями перечисления.
Так, при определении переменной, которая содержит цвет радуги, вы объявляете ее
следующим образом:
RainbowColors MyWorldsColor = Blue; // Начальное значение

В приведенной выше строке кода объявляется переменная M y W o r ld s C o lo r , имею­
щая тип перечисления R a in b o w C o lo r s . Эта переменная может содержать только один
из семи цветов радуги и не может хранить никакие другие значения.

Что такое константа

|

79

При объявлении перечисления компилятор преобразует его константы, та­
кие как V i o l e t и другие, в целые числа. Каждое последующее значение
перечисления на единицу больше предыдущего. Начальное значение вы
можете задать сами, но если вы этого не сделаете, компилятор начнет счет
с 0. Так, значению N o r t h соответствует числовое значение 0.
По желанию можно также явно определить числовое значение напротив
каждой из перечисляемых констант при их инициализации.

Листинг 3.9 демонстрирует использование перечисления для хранения четырех на­
правлений с инициализацией первого значения.
ЛИСТИНГ 3.9. Использование перечислений для указания направлений ветра
1
2

#include
using namespace std;

3
4
5

enum CardinalDirections

{

6

North = 25,
South,
East,
West

7

8
9

10
11
12
13
14
15
16
17
18
19
20

};
int main()

{
cout
cout
cout
cout
cout

"Направления и их значения" «
"North: " « North « endl;
"South: " « South « endl;
"East: " « East « endl;
"West: " « West « endl;

endl;

CardinalDirections windDirection = South;
cout « "windDirection = " « windDirection «

21
22
23
24

«
«
«
«
«

return 0;

}

Результат
Направления и их значения
North: 25
South: 26
East: 27
West: 28
windDirection = 2 6

endl;

ЗАНЯТИЕ 3. Использование переменных и констант

80

Анализ
Обратите внимание на то, что у нас определены четыре константы перечисления,
но первая константа N o r t h получила значение 2 5 (см. строку 6). Это автоматически
гарантирует, что следующим константам будут соответствовать значения 26, 2 7 и 28,
что и видно из вывода программы. В строке 2 0 создается переменная типа C a r d i n a l
D i r e c t i o n s , которой присваивается начальное значение S o u t h . При выводе на экран
в строке 21 компилятор отображает целочисленное значение, назначенное константе
S o u t h , которое равно 26.

СОВЕТ

Имеет смысл обратиться к листингам 6.4 и 6.5 занятия 6, “Управление по­
током выполнения программы”. В них перечисление используется для дней
недели, а условное выражение позволяет указать выбранный пользовате­
лем день.

Определение констант с использованием
директивы # d e fin e
Первое и главное: не используйте этот способ при написании новых программ.
Единственная причина упоминания определения констант с использованием дирек­
тивы # d e f i n e в этой книге — помочь вам понять некоторые устаревшие программы,
в которых для определения числа к мог бы использоваться такой синтаксис:
# d e f in e p i

3 .1 4 1 5 9 3

Это макрокоманда препроцессора, предписывающая компилятору заменять все
упоминания p i значением 3 .1 4 1 5 9 3 . Обратите внимание: это текстовая (читай: не­
интеллектуальная) замена, осуществляемая препроцессором. Компилятор не знает
фактический тип рассматриваемой константы и не заботится о нем.

ВНИМАНИЕ!

Определение

констант с

использованием

директивы

препроцессора

# d e f i n e считается устаревшим и не рекомендуется.

Ключевые слова, недопустимые
для использования в качестве
имен переменных и констант
Некоторые слова зарезервированы языком C++, и их нельзя использовать в качес­
тве имен переменных. У этих ключевых слов есть специальное значение с точки
зрения компилятора C++. К ключевым относятся такие слова, как i f , w h i l e , f o r и
m a in . Список ключевых слов языка C++ приведен в табл. 3.2, а также в приложении Б,
“Ключевые слова языка C++”. У вашего компилятора могут быть дополнительные
зарезервированные слова, поэтому для полноты списка проверьте его документацию.

Резю м е

ТАБЛИЦА 3.2. Ключевые слова языка C++
asm
dynamic c a s t
namespace
new
auto
e ls e
enum
bool
op erator
break
e x p lic it
p r iv a te
case
exp ort
p r o te c te d
ex tern
catch
p u b lic
char
fa ls e
r e g is t e r
c la s s
flo a t
r e in te r p r e t c a s t
fo r
co n st
retu rn
con stexpr
fr ie n d
sh o rt
goto
c o n st_ c a st
sig n ed
con tin ue
if
s iz e o f
d e fa u lt
in lin e
s ta tic
d e le te
in t
s ta tic _ c a s t
long
do
s tr u c t
sw itch
double
mutable
Кроме того, зарезервированы следующие слова:
b it o r
and
not_eq
and_eq
compl
or
b itan d
not
or_eq

РЕКОМЕНДУЕТСЯ

81

tem p late
th is
throw
tru e
tr y
ty p ed ef
ty p e id
typename
union
unsign ed
u sin g
v ir t u a l
void
v o la tile
wchar t
w h ile

xor
xor_eq

НЕ РЕКОМЕНДУЕТСЯ

Присваивайте переменным осмысленные име­

Не присваивайте переменным имена, которые

на, даже если они становятся длинными.

слишком коротки или содержат только один

Инициализируйте переменные и используйте

символ.

инициализацию списком, чтобы избежать оши­

Не присваивайте переменным имена, которые

бок сужающего преобразования.

используют экзотические сокращения, понят­

Удостоверьтесь, что имя переменной объясня­

ные только вам.

ет ее назначение.

Не присваивайте переменным имена, совпа­

Поставьте себя на место того, кто еще не видел

дающие с зарезервированными ключевыми

ваш код, и подумайте, ясен ли смысл применяе­
мых в нем имен.

дет компилироваться.

словами языка C++, поскольку такой код не бу­

Используйте в своей группе соглашение об име­
новании и строго придерживайтесь его.

Резюме
На этом занятии речь шла об использовании памяти для временного хранения зна­
чений в переменных и константах. Вы узнали, что тип переменных определяет их
размер и что оператор s i z e o f позволяет выяснить этот размер. Вы также узнали о су­
ществовании различных типов переменных, таких как bool, i n t и другие, и о том, что

82

|

ЗАНЯТИЕ 3. Использование переменных и констант

они должны использоваться для содержания данных различных типов. Правильный
выбор типа переменной важен для эффективности программы; если для переменной
выбран слишком маленький тип, это может закончиться циклическим переходом или
переполнением. Вы познакомились с ключевым словом au to , которое позволяет ком­
пилятору самостоятельно вывести тип данных на основе значения, инициализирую­
щего переменную.
Кроме того, мы рассмотрели различные типы констант и применение самых важ­
ных из них с использованием ключевых слов c o n s t и enum.

Вопросы и ответы
■ Зачем вообще определять константы, если вместо них можно использовать
обычные переменные?
Константы, особенн о те, в объявлении которых используется ключевое слово
c o n s t, являются средством для указания компилятору того, что значение опреде­
ленной переменной должно быть постоянным (не должно изменяться во время вы­
полнения программы). Таким образом, компилятор гарантирует, что переменной,
объявленной константной, никогда не будет присвоено другое значение, даже если
другой программист, исправляя вашу работу, по неосторожности попытается пере­
записать значение этой переменной. Если вы знаете, что значение переменной в
программе не должно изменяться, ее следует объявить как константу, что увеличи­
вает качество и надежность вашего приложения.

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

i n t m yFavoriteNum ber = 0;
записывает в область памяти, выделенной для переменной m yFavoriteN um ber,
исходное значение по вашему выбору, в данном случае — 0. Распространены си­
туации, когда некоторые действия осуществляется по-разному в зависимости от
значения переменной (как правило, выполняется проверка на отличие ее значения
от нуля). Без инициализации такая логика работает ненадежно, поскольку вновь
выделенная область памяти содержит то, что в ней было раньше, т.е. случайное
значение, которое невозможно предсказать2.

■ Почему язык C++ позволяет использовать для целых чисел разные типы:
sh ort in t, i n t и long in t? Почему бы не использовать всегда только тот тип,
который позволяет хранить наибольшие значения?
Язык программирования C++ используется для разработки множества приложений,
некоторые из которых выполняются на устройствах с небольшими вычислительны­
ми возможностями и ресурсами памяти. (Простой старый сотовый телефон — один
из примеров таких устройств.) В таком случае программист может сэкономить

2 Если только это не глобальная переменная. — П рим еч.

р ед .

Вопросы и ответы

|

83

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

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

■ Язык C++ позволяет объявлять беззнаковые целочисленные переменные, ко­
торые, как предполагается, способны содержать только положительные цело­
численные значения и нуль. Что случится при уменьшении нулевого значения
переменной типа unsigned in t?
Произойдет циклический переход (wrapping). Уменьшение значения 0 беззнаковой
целочисленной переменной на 1 превратит его в самое большое значение, которое
она способна содержать! Просмотрите табл. 3.1 и убедитесь, что переменная типа
u n sig n ed s h o r t способна содержать значения от 0 до 65535. Итак, объявим пере­
менную типа un signed s h o rt, осуществим декремент и увидим нечто неожиданное:
unsigned short myShortlnt = 0 ;
// Исходное значение
myShortlnt = myShortlnt - 1;
// Уменьшение на 1
std::cout « myShortlnt « std::endl; // Вывод: 65535!

Обратите внимание: это проблема не типа u n sig n e d s h o rt, а способа ее примене­
ния. Целочисленный тип без знака (не важно, короткий или длинный) не должен
использоваться там, где ожидается наличие отрицательных значений. Если содер­
жимое переменной m y S h o rtln t должно использоваться для количества динамичес­
ки резервируемых байтов, то небольшая ошибка, допустившая декремент нулево­
го значения, привела бы к выделению 64 Кбайт! Хуже того, если бы переменная
m y S h o rtln t использовалась как индекс при доступе к памяти, ваше приложение,
вероятнее всего, обратилось бы за пределы выделенной области памяти и аварийно
завершилось бы!

84

|

ЗАНЯТИЕ 3. Использование переменных и констант

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. В чем разница между знаковым и беззнаковым целыми числами?
2. Почему не стоит использовать директиву # d e f i n e при объявлении константы?
3. Зачем инициализировать переменную?
4. Рассмотрите перечисление ниже. Каково значение константы QUEEN?
enum YOURCARDS

{АСЕ,

JA C K ,

Q U EEN ,

K IN G };

5. Что не так с именем этой переменной?
in t

I n t e g e r = 0;

Упражнения
1.

Измените перечисление YOURCARDS контрольного вопроса 4 так, чтобы значени­
ем константы QUEEN стало 4 5 .

2. Напишите программу, демонстрирующ ую , что размер беззнакового целого
числа и обычного целого числа одинаков и что размер их обоих не превышает
размер длинного целого числа.
3. Напишите программу для вычисления площади и периметра круга, радиус ко­
торого вводится пользователем.
4. Что будет, если в коде упражнения 3 площадь и периметр хранить в целочис­
ленных переменных, а результат — в переменной любого другого типа?
5. Отладка. Что неверно в следующей инициализации?
a u to

In te g e r;

ЗАНЯТИЕ 4

Массивы и строки
На предыдущих занятиях мы объявляли переменные для
хранения одиночного значения типа i n t , c h a r или s t r i n g ,
даже если использовалось несколько их экземпляров. Одна­
ко можно объявить коллекцию объектов, например 20 целых
чисел или строку символов для хранения имени.
На этом занятии...

■ Что такое массивы, как их объявлять и использовать
■ Что такое строки и как использовать для их создания
символьные массивы
■ Краткое введение в тип s t d : : s t r i n g

86

|

ЗАНЯТИЕ 4. Массивы и строки

Что такое массив
Определение слова массив (array) в словаре довольно близко к тому, что мы хотим
понять. Согласно словарю Вебстера массив — это “группа элементов, формирующих
единое целое, например массив солнечных панелей”.
Ниже приведены характеристики массива.
■ Массив — это коллекция элементов.
■ Все содержащиеся в массиве элементы — одного типа.
■ Такая коллекция формирует полный набор.
В языке C++ массивы позволяют сохранить в памяти элементы данных некоторого
типа в последовательном упорядоченном виде.

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

firstNumber
secondNumber
thirdNuinber
fourthNumber
fifthNumber

=
=
=
=
=

0;
0;
0;
0;
0;

Но если бы пользователю понадобилось хранить и впоследствии отображать 500 и
более целых чисел, то пришлось бы объявить 500 таких целочисленных переменных,
используя приведенную выше систему. Требуются огромная работа и терпение для
ее выполнения. А что делать, если пользователь попросит обеспечить 500000 целых
чисел вместо 5?
Правильнее объявить массив из пяти целых чисел, каждое из которых инициализи­
ровалось бы нулевым значением:
int myNumbers[5] = {0};

Таким образом, если бы вас попросили обеспечить 500000 целых чисел, то ваш
массив без проблем можно было бы увеличить:
int manyNumbers[500000] = {0};

Массив из пяти символов можно определить следующим образом:
char myCharacters[5];

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

Что такое массив

|

87

Объявление и инициализация статических массивов
В приведенных выше строках кода мы объявили массив myNumbers, который со­
держит пять элементов типа i n t (т.е. целых чисел), инициализированных значени­
ем 0. Таким образом, для объявления массива в языке C++ используется следующий
синтаксис:
Т и п_элем ен т а И м я _ м а сси ва [К о л и ч е с т в о _ э л е м е н т о в ] =
{ Н е о б я за т е л ь н ы е и с х о д н ы е з н а ч е н и я };

Можно даже объявить массив и инициализировать содержимое всех его элементов.
Так, целочисленный массив из пяти целых чисел можно инициализировать пятью раз­
ными целочисленными значениями:
int myNumbers[5] = {34, 56, -21, 5002, 365};

Все элементы массива можно инициализировать нулем (значение по умолчанию,
предоставляемое компилятором):
int myNumbers[5] = {0}; // Инициализировать все элементы нулем

Вы можете также инициализировать только часть элементов массива:
int myNumbers[5] = {34, 56}; // инициализировать первые два
// элемента значениями 34 и 56, прочие элементы равны нулю

Вы можете определить длину массива (т.е. указать количество элементов в нем)
как константу и использовать ее при определении массива:
const int ARRAY_LENGTH = 5;
int myNumbers[ARRAYJLENGTH] = {34, 56, -21, 5002, 365};

Это особенно полезно, когда необходимо иметь доступ и использовать длину мас­
сива в нескольких местах, например при переборе всех элементов массива. В таком
случае при изменении длины массива достаточно будет исправить лишь одно значе­
ние, объявленное как c o n s t i n t .
Если исходное количество элементов в массиве неизвестно, его можно не указывать:
int myNumbers[] = {2017, 2052, -525};

Приведенный выше код создает массив из трех целых чисел со значениями 2017,
2052 и -5 2 5 .

ПРИМЕЧАНИЕ

Массивы, которые мы объявляли до сих пор, называются статическими,
поскольку их длина фиксирована во время компиляции. Такой массив не
может принять больше данных, чем указано программистом. Он не может
использовать и меньшее количество памяти, если она используется только
наполовину или вообще не используется. Массивы, размер которых опреде­
ляется во время выполнения, называются динамическими. Динамические
массивы бегло затрагиваются на данном занятии и подробно обсуждаются
на занятии 17, “Классы динамических массивов библиотеки STL”.

88

ЗАНЯТИЕ 4. Массивы и строки

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

РИС. 4.1. Книги на полке: одномерный массив
Здесь нет ошибки, мы действительно начинаем нумеровать книги с нуля — имен­
но так, как вы узнаете позже, нумеруются индексы в языке C++. Массив myNumbers,
содержащий пять целых чисел и показанный на рис. 4.2, выглядит очень похожим на
пять книг на полке.

РИС. 4.2. Организация массива myNumbers из пяти целых чисел
Обратите внимание, что занятая массивом область памяти состоит из пяти бло­
ков равного размера, определяемого типом хранимых массивом данных, в данном

Что такое массив

89

случае — типом i n t . Если вы помните, мы рассматривали размер целочисленных
типов на занятии 3, “Использование переменных и констант”. Таким образом, объ­
ем памяти, выделенной компилятором для массиваmyNumbers, равен s i z e o f ( i n t ) *5
байт. В общем виде объем памяти в байтах, резервируемой компилятором для масси­
ва, составляет

Байты массива = sizeof {Тип элемента) * Количество элементов

Доступ к данным, хранимым в массиве
Для обращения к элементам массива можно использовать индексы (index), или но­
мер элемента в массиве. Первый элемент массива имеет индекс 0. Так, первое цело­
численное значение, хранимое в массиве myNumbers, — это myNumbers [0], второе —
myNumbers [1] и т.д. Пятый элемент массива — myNumbers [ 4] . Другими словами,
индекс последнего элемента в массиве всегда на единицу меньше его длины.
Когда запрашивается доступ к элементу с индексом N, компилятор использует
адрес первого элемента (позиция элемента с нулевым индексом) в качестве отправной
точки, а затем пропускает N элементов, добавляя к этому адресу смещение, вычисляе­
мое как N * s i z e o f (тип__элемента), чтобы получить адрес N+1-го элемента. Компи­
лятор C++ не проверяет, находится ли индекс в пределах фактически определенных
границ массива. Вы можете попытаться выбрать элемент с индексом 1001 в массиве,
содержащем только 10 элементов, поставив тем самым под угрозу безопасность и
стабильность своей программы. Ответственность за предотвращение обращений к
элементам за пределами массива лежит исключительно на программисте.

ВНИМАНИЕ!

Результат доступа к массиву за его пределами непредсказуем. Как прави­
ло, такое обращение ведет к аварийному завершению программы1. Этого
нужно избегать любой ценой.

В листинге 4.1 демонстрируются объявление массива целых чисел, инициализация
его элементов целочисленными значениями и обращение к ним для отображения на
экране.
ЛИСТИНГ 4,1. Объявление массива целых чисел и доступ к его элементам_____________
0: tinclude

1:
2: using namespace std;
3:
4: int main()
5: {
6:
int myNumbers[5] = {34, 56, -21, 5002, 365};
7:

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

ЗАНЯТИЕ 4. Массивы и строки

90

8
9
10
11
12
13
14
15

cout
cout
cout
cout
cout

«
«
«
«
«

"Элемент
"Элемент
"Элемент
"Элемент
"Элемент

с
с
с
с
с

индексом
индексом
индексом
индексом
индексом

0
1
2
3
4

"
"
"
"
"

«
«
«
«
«

myNumbers[0]
myNumbers[1]
myNumbers[2]
myNumbers[3]
myNumbers[4]

«
«
«
«
«

endl;
endl ;
endl;
endl;
endl;

return 0;

Результат
Элемент
Элемент
Элемент
Элемент
Элемент

с
с
с
с
с

индексом
индексом
индексом
индексом
индексом

0:
1:
2:
3:
4:

34
56
-21
5002
365

Анализ
В строке 6 объявлен массив из пяти целых чисел с определенными для каждого
элемента исходными значениями. П оследующ ие строки просто отображают целые
числа, используя поток c o u t и переменную типа массива myNumbers с соответствую­
щим индексом.

ПРИМЕЧАНИЕ

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

Изменение данных в массиве
В коде предыдущего листинга пользовательские данные в массив не вводились.
Синтаксис присваивания целого числа элементу в этом массиве очень похож на син­
таксис присваивания значения целочисленной переменной.
Например, присваивание значения 2017 целочисленной переменной выглядит так:
int thisYear;
thisYear = 2017;

Присваивание значения 2017 четвертому элементу в рассматриваемом массиве вы­
глядит аналогично:
myNumbers[3] = 2017; // Присваивание 2017 четвертому элементу

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

Что такое массив

91

ЛИСТИНГ 4.2. Присваивание значений элементам массива
0
1
2

#include
using namespace std;
constexpr int Square(int number)

{ return number*number;

}

3
4
5

int main()

{

6

const int ARRAY_LENGTH = 5;

7

8

// Инициализированный массив из 5 целых чисел
int myNumbers[ARRAY_LENGTH] = {5, 10, 0, -101, 20};

9

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

// Использование constexpr для массива из 25 целых чисел
int moreNumbers[Square(ARRAY_LENGTH)];
cout « "Введите индекс изменяемого элемента: ";
int elementIndex = 0;
cin » elementIndex;
cout « "Введите новое значение: ";
int newValue = 0;
cin » newValue;
myNumbers[elementIndex] = newValue;
moreNumbers[elementIndex] = newValue;
cout «
cout «

"Элемент " « elementlndex « " myNumbers равен: ";
myNumbers[elementlndex] « endl;

cout «
cout «

"Элемент " « elementlndex « " moreNumbers равен: ";
moreNumbers[elementlndex] « endl;

return 0;

}

Результат
Введите
Введите
Элемент
Элемент

индекс изменяемого элемента: 3
новое значение: 101
3 myNumbers равен: 101
3 moreNumbers равен: 101

Анализ
Длина массива должна быть константным целочисленным значением. Это значение
может быть задано как константа ARRAY_LENGTH (строка 9) или как константное выраже­
ние Square () в строке 12. Таким образом, массив myNumbers объявлен как имеющий 5
элементов, в то время как массив moreNumbers содержит 25 элементов. В строках 14-20

92

|

ЗА Н Я ТИ Е 4.

Массивы и строки

пользователю предлагается ввести индекс элемента массива, который он хочет изме­
нить, и новое значение, которое будет храниться в элементе с этим индексом. В стро­
ках 22 и 23 показано, как изменить конкретный элемент массива с использованием ин­
декса. Обращение к элементам массива с помощью индекса показано в строках 26-29.
Обратите внимание, что на изменение элемента с индексом 3 изменяет четвертый эле­
мент массива, так как индексы отсчитываются от нуля. Вы должны привыкнуть к этому.

ПРИМЕЧАНИЕ

Многие новички в программировании на языке C++ присваивают значе­
ние пятому элементу, используя индекс 5 в массиве из пяти целых чисел.
Обратите внимание: этот элемент находится уже за пределами массива, и
откомпилированный код на самом деле пытается обратиться к шестому эле­
менту массива из пяти элементов.
Этот вид ошибки иногда называется ошибкой поста охраны (fence-post
error). Это название связано с тем фактом, что количество постов охраны
на один больше количества охраняемых участков.

ВНИМАНИЕ!

В листинге 4.2 отсутствует кое-что очень важное: проверка введенного
пользователем индекса на соответствие границам массива. Предыдущая
программа должна на самом деле проверять, находится ли значение пере­

e le m e n tln d e x в пределах от 0 до 4 для массива myNumbers и
0 до 24 для массива moreNumbers, и отбрасывать все остальные зна­

менной
от

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

И сп ол ь зов ан и е циклов для доступ а
к э л е м е н т а м м а с си в а ___________________________________
При последовательной работе с элементами массива для обращения к ним (их перебора) используют­
ся циклы. Чтобы быстро научиться эффективно работать с элементами массива, используя цикл

for,

обратитесь к листингу 6.10 занятия 6, “Управление потоком выполнения программы".

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Всегда инициализируйте массивы, иначе они

Никогда не обращайтесь к элементу с номе­

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

ром N, используя индекс N, в массиве из N эле­

Всегда проверяйте, не выходят ли используе­

используйте индекс N-1.

мые индексы за границы массива.

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

Многомерные массивы

93

Многомерные массивы
Массивы, которые мы рассматривали до сих пор, напоминали книги, стоящие на
полке. На более длинной полке может быть больше книг, на более короткой — мень­
ше. Таким образом, длина полки — единственная размерность, определяющая ее ем­
кость, т.е. полка одномерна. Но что если нам нужно использовать массив для модели­
рования солнечных панелей, показанных на рис. 4.3? Солнечные панели, в отличие от
книжных полок, распространяются в двух размерностях: по длине и по ширине.
Столбец 0

Столбец 1

Ряд О

Панель
0

Панель
1

Ряд 1

Панель
3

Панель
4

Столбец 2
Панель
2

Панель
5

Р И С - 4 -3 - М а с с и в с о л н е ч н ы х п а н е л е й на к р ы ш е

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

Объявление и инициализация многомерных массивов
Язык C++ позволяет объявлять многомерные массивы, указывая количество эле­
ментов, которое необходимо выделить в каждой размерности. Таким образом, дву­
мерный массив целых чисел, представляющий солнечные панели на рис. 4.3, можно
объявить так:
int solarPanellDs[2][3];

Обратите внимание, что на рис. 4.3 каждой из шести панелей присвоен также иден­
тификатор в диапазоне от 0 до 5. Если мы инициализируем целочисленный массив в
том же порядке, то эта инициализация будет иметь следующий вид:
int solarPanellDs[2][3] = {{0, 1, 2}, {3, 4, 5}};

Как видите, синтаксис инициализации подобен синтаксису, используемому при
инициализации двух одномерных массивов. Если бы массив состоял из трех строк и
трех столбцов, его объявления и инициализация выглядели бы следующим образом:
int threeRowsThreeColumns[3][3] =
{{-501, 206, 2017}, {989, 101, 206}, {303, 456, 596}};

94

|

ЗАНЯТИЕ 4. Массивы и строки

ПРИМЕЧАНИЕ

Несмотря на то что язык C++ позволяет использовать модель многомерных
массивов, в памяти такие массивы все равно хранятся как одномерные.
Компилятор отображает многомерный массив на область памяти, которая
расширяется только в одном направлении.
Если хотите, можете инициализировать тот же массив s o l a r P a n e l l D s
следующим образом - результат при этом оказывается тем же:
in t

s o la r P a n e llD s [2 ][3 ]

= {0,

1,

2,

3,

4,

5};

Однако предыдущий способ нагляднее, поскольку так проще представить и
понять, что многомерный массив - это массив массивов.

Доступ к элементам многомерного массива
Рассматривайте многомерный массив как массив массивов. Поскольку рассмотрен­
ный ранее двумерный массив включал три строки и три столбца, содержащих целые
числа, вы можете рассматривать его как массив, состоящий из трех элементов, каж­
дый из которых является массивом, состоящим из трех целых чисел.
Поэтому, когда необходимо получить доступ к целому числу в этом массиве, следу­
ет использовать первый индекс для указания номера массива, хранящего целые числа,
а второй индекс — для указания номера целого числа в этом массиве. Рассмотрим
следующий массив:
int threeRowsThreeColumns[3][3] =
{{-501, 206, 2017}, {989, 101, 206}, {303, 456, 596}};

Он инициализирован так, что его можно рассматривать как три массива, каждый
из которых содержит три целых числа. Здесь целочисленный элемент со значением
206 находится в позиции [0] [ 1] , а элемент со значением 456 — в позиции [2] [1].
В листинге 4.3 демонстрируется, как можно обращаться к целочисленным элементам
этого массива.
ЛИСТИНГ 4,3. Доступ к элементам многомерного массива
0
1

#include
using namespace std;

2

3

int main()

4
5

{

6

int threeRowsThreeColumns[3][3] =
{{-501, 206, 2016}, {989, 101, 206}, {303, 456, 596}};

7
8

cout «

"Row 0: " «
«
«

threeRowsThreeColumns[0][0] «
threeRowsThreeColumns[0][1] «
threeRowsThreeColumns[0][2] «

" "
" "
endl;

cout «

"Row 1: " «
«

threeRowsThreeColumns[1][0] «
threeRowsThreeColumns[1][1] «

»» »i

9

10
11
12
13
14

" "

Динамические массивы
15
16
17
18
19
20
21
22

cout «

«

threeRowsThreeColumns[1][2] «

endl ;

"Row 2: " «
«
«

threeRowsThreeColumns[2][0] «
threeRowsThreeColumns[2][1] «
threeRowsThreeColumns[2][2] «

endl ;

|

95

return 0;
}

Результат
Row 0: -501 206 2016
Row 1: 989 101 206
Row 2: 303 456 596

Анализ
Обратите внимание на метод построчного обращения к элементам массива, начиная
с массива Row 0 (первая строка, с индексом 0 ) и заканчивая массивом Row 2 (третья
строка, с индексом 2). Поскольку каждая из строк — это массив, синтаксис обращения к
третьему элементу (индекс 2) в первой строке (индекс 0) такой, как показано в строке 10.

ПРИМЕЧАНИЕ

Длина кода в листинге 4.3 существенно увеличивается при увеличении ко­
личества элементов в массиве или его размерностей. При профессиональ­
ной разработке такой код неприемлем.
Более эффективный способ обращения к элементам многомерного масси­
ва показан в листинге 6.15 занятия 6, “Управление потоком выполнения
программы". Там для доступа ко всем элементам подобного массива ис­
пользуется вложенный цикл f o r . Код с применением цикла f o r суще­
ственно короче и меньше склонен к ошибкам, а кроме того, на его длину не
влияет изменение количества элементов в массиве.

Динамические массивы
Рассмотрим приложение, которое хранит медицинские записи больницы. Програм­
мист никак не может заранее знать, сколько записей должно хранить и обрабатывать
его приложение. Он может сделать предположение о разумном пределе количества
записей для маленькой больницы и превысить его, допустив ошибку в безопасном на­
правлении. Но в этом случае он бессмысленно резервирует огромные объемы памяти
и уменьшает производительность системы.
В таком случае нужно использовать не статические массивы, которые мы рассмо­
трели только что, а динамические, которые оптимизируют использование памяти и
при необходимости увеличивают размер занимаемых ими ресурсов и памяти во время
выполнения. Язык C++ предоставляет очень удобный в работе динамический массив
в форме типа s t d : : v e c t o r , как показано в листинге 4.4.

96

|

ЗАНЯТИЕ 4. Массивы и строки

ЛИСТИНГ 4.4. Создание динамического массива целых чисел
и его заполнение значениями
0: #include
1: #include
2:
3: using namespace std;

4:
5: int main()

6:

{

vector DynArrNums(3); // Динамический массив int'oB

7:

8:
9:
10:
11:

dynArrNums[0] = 365;
dynArrNums[1] = -421;
dynArrNums[2]= 789;

12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25: }

cout «

"Чисел в массиве: " «

DynArrNums.size() «

endl;

cout « "Введите новое число для вставки в массив: ";
int anotherNum = 0;
cin » AnotherNum;
dynArrNums .push_back(anotherNum) ;
cout « "Чисел в массиве: " « dynArrNums.size() «
cout « "Последний элемент массива: ";
cout « dynArrNums[dynArrNums.size() - 1] « endl;

endl;

return 0;

Результат
Чисел в массиве: 3
Введите новое число для вставки в массив: 2017
Чисел в массиве: 4
Последний элемент массива: 2017

Анализ
Не волнуйтесь о синтаксисе с векторами и шаблонами в листинге 4.4, они пока
еще не были объяснены. Попробуйте просто просмотреть вывод и соотнести его с
кодом. Согласно выводу начальный размер массива составляет три элемента, что со­
гласуется с объявлением вектора в строке 7. Зная это, вы все же можете в строке 15
попросить пользователя ввести четвертое число и, что интереснее всего, в строке 18
добавить его в конец массива, используя метод push_back (). v e c to r динамически из­
менит свои размеры так, чтобы приспособиться к хранению большего объема данных.
Это заметно по последующему увеличению размера массива до 4. Обратите внимание
на использование знакомого по статическим массивам синтаксиса доступа к данным

Строки символов в стиле С

97

в векторе. В строке 22 осуществляется доступ к последнему элементу (каким бы он
ни был по счету, поскольку его позиция вычисляется во время выполнения) с помо­
щью индекса, который для последнего элемента имеет значение s i z e () - 1 . Функция
s i z e () возвращает общее количество элементов, содержащихся в векторе.

ПРИМЕЧАНИЕ

Для использования класса динамического массива s t d : : v e c t o r в код
необходимо включить заголовочный файл v e c t o r , как зто сделано в стро­
ке 1 листинга 4.4.
# in c l u d e

< v e c to r>

Более подробно о векторах пойдет речь на занятии 17, “Классы динамичес­
ких массивов библиотеки STL”.

Строки символов в стиле С
Строки в стиле С (C-style string) — это частный случай массива символов. Вы уже
видели несколько примеров таких строк в виде строковых литералов, когда писали код:
std::cout «

"Hello World";

Это эквивалентно такому объявлению массива:
char sayHello[] = {'Н*,'е','1’, ’1','о',1 ','W’,'о', 'г',’1','d
std::cout « sayHello « std::endl;

\ 0 1 };

Обратите внимание: последний символ в массиве — нулевой символ 1\0 ’ . Он так­
же называется завершающим нулевым символом (string-terminating character), посколь­
ку указывает компилятору, что строка на этом заканчивается. Такие строки в стиле
С — это частный случай символьных массивов, последним символом которых всегда
является нулевой символ 1\ 0 1. Когда вы используете в коде строковый литерал, ком­
пилятор сам добавляет после него символ ' \ 0 1.
Если вставить символ 1\ 0 1 в середину массива, то это не изменит его размер; од­
нако обработка строки, хранящейся в данном массиве, остановится на этой точке. Это
демонстрируется в листинге 4.5.

ПРИМЕЧАНИЕ

Символ 1\ 0 1 может показаться двумя символами (в самом деле, для его
ввода следует нажать на клавиатуре две клавиши). Однако обратная ко­
сая черта - это просто специальный управляющий код, который понимает
компилятор, и потому воспринимает последовательность \ 0 как нуль, т.е.
это способ указать компилятору на необходимость вставить нулевой символ
(символ со значением нуль).
Вы не можете записать нулевой символ непосредственно, поскольку лите­
рал 10 1 будет воспринят, как символ нуля с кодом ASCII 48, а не 0.
Чтобы увидеть этот и другие значения кодов ASCII, обратитесь к таблице в
приложении Г, “Коды ASCII”.

98

|

ЗАНЯТИЕ 4. Массивы и строки

ЛИСТИНГ 4,5. Анализ строки в стиле С с завершающим нулевым символом
0:
1:
2:
3:
4:
5:

#include
using namespace std;
int main()
{
char sayHello[] = {,H ,, ,e',,l ,, ,l ,,,o ,,!

6:
7:
8:
9:
10:
12:
12:
13:
14:
15: }

' W \ 'o', *r', '1', fd f, '\0' };
cout « sayHello « endl;
cout « "Размер массива: " « sizeof(sayHello) « endl;
cout « "Замена пробела нулем" « endl;
sayHello[5] = 1\01;
cout « sayHello « endl;
cout « "Размер массива: " « sizeof(sayHello) « endl;

return 0;

Результат
Hello World
Размер массива: 12
Замена пробела нулем
Hello
Размер массива: 12

Анализ
Код в строке 10 заменяет пробел в строке " H e l l o W o r ld " нулевым символом. Те­
перь у массива есть два нулевых завершающих символа, но при выводе используется
первый, что и создает получаемый результат. Когда пробел заменяется нулевым сим­
волом, отображаемая строка усекается до части " H e l l o " . Метод s i z e o f () в строках 8
и 12 указывает, что размер массива не изменился, несмотря на изменение отображае­
мых данных.

ВНИМАНИЕ!

Если при объявлении и инициализации символьного массива в строках 5
и 6 листинга 4.5 вы забудете добавить символ ! \ 0 \ то после вывода
" H e l l o W o r ld " на консоль будет выведен случайный набор символов.
Дело в том, что оператор s t d : : c o u t не остановится по окончании масси­
ва и будет продолжать вывод, пока не встретит нулевой символ, даже если
для этого придется перейти границы массива.
Эта ошибкаможет привести вашу программу к аварийному останову, а в
некоторых случаях поставить под угрозу стабильность системы.

Строки в стиле С чреваты опасностями. В листинге 4.6 демонстрируются риски,
связанные с их применением.

Строки символов в стиле С

|

99

ЛИСТИНГ 4.6. Риски использования строк в стиле С и пользовательского ввода
0
1
2
3



tinclude
#include
using namespace std;
int main()
/
i

5
с0
7
8
о
У
10
11
12
13

cout «

"Введите слово не длиннее 20 символов: ";

char userlnput[21] = {1\0'};
cin » userlnput;
cout «

"Длина ввода: " «

strlen(userlnput) «

endl;

return 0;
}

Результат
Введите слово не длиннее 20 символов: D o n 1tUseThisProgram
Длина ввода: 19

Анализ
Опасность видна в выводе. Программа просит пользователя не вводить больше
двадцати символов. Дело в том, что объявленный в строке 7 символьный буфер, пред­
назначенный для хранения пользовательского ввода, имеет фиксированную статически
длину 21 символ. Поскольку последний символ в строке должен быть нулевым, т\ 0 f,
максимальная длина текста, хранимого буфером, ограничивается двадцатью символа­
ми. Обратите внимание на применение функции s t r l e n в строке 10 для вычисления
длины строки. Она проходит по символьному буферу и подсчитывает количество сим­
волов, пока не достигает нулевого, который означает конец строки. Этот символ был
вставлен в конец введенных пользователем данных потоком c in . Подобное поведение
опасно, поскольку так можно легко пересечь границы символьного массива при вводе
пользователем текста длиннее упомянутого предела. Чтобы узнать, как реализовать
проверку, гарантирующую, что запись в массив не выйдет за его пределы, обратитесь
к листингу 6.2 занятия 6, “Управление потоком выполнения программы”.

ВНИМАНИЕ!

Приложения, написанные на языке С (или на языке C++ программистами
с большим опытом в языке С), зачастую используют в своем коде функции

s tr c p y , функции конкатенации, такие как
s t r c a t , и определения длины строк, такие как s t r le n .

копирования строк, такие как

Эти функции используют строки в стиле С и потому опасны, так как ищут
завершающий нулевой символ и могут легко выйти за границы символь­
ного массива, если программист не гарантировал наличие завершающего
нулевого символа.

100

|

ЗАНЯТИЕ 4. Массивы и строки

Строки C++: использование

s t d : :s t r in g

Стандартная строка C++ — самое эффективное средство работы и с текстовым
вводом, и при работе со строками, например, при их конкатенации.
Язык C++ предоставляет мощное и в то же время безопасное средство работы со
строками — класс s t d : : s t r i n g . Класс s t d : : s t r i n g не является статическим мас­
сивом элементов типа ch ar неизменного размера, как строки в стиле С, и допускает
увеличение размера, когда в нем необходимо сохранить больше данных. Применение
s t d : : s t r i n g для работы со строковыми данными показано в листинге 4.7
ЛИСТИНГ 4.7. Использование s t d : : s t r i n g для инициализации
и хранения пользовательского ввода, а также для копирования,
конкатенации и определения длины строки___________________________________________
0: #include
1: #include

2:
3: using namespace std;
4:
5: int main ()

6: {
7:
8:

string greetString("Hello std::string!");
cout « greetString « endl;

9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

cout « "Введите текстовую строку: " «
string firstLine;
getline(cin, firstLine);
cout « "Введите другую строку: " «
string secondLine;
getline(cin, secondLine);

endl;

endl;

cout « "Результат конкатенации: " « endl;
string concatString = firstLine + " " + secondLine;
cout « concatString « endl;

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30: )

cout « "Копия полученной строки: " «
string aCopy;
aCopy = concatString;
cout « aCopy « endl;
cout «

"Длина строки: " «

return 0;

endl;

concatString.length() «

endl;

Резю ме

101

Результат
Hello std::string!
Введите текстовую строку:
I love
Введите другую строку:
C++ strings
Результат конкатенации:
I love C++ strings
Копия полученной строки:
I love C++ strings
Длина строки: 18

Анализ
Постарайтесь понять вывод и связать его с соответствующими элементами в коде.
Не беспокойтесь пока что о новых синтаксических возможностях. Программа начи­
нается с отображения инициализированной в строке 7 строки "H ello s t d : : s t r i n g ! ”
Затем, в строках 12 и 16, она считывает введенные пользователем строки текста, кото­
рые сохраняются в переменных f i r s t L i n e и secondL ine. Фактически конкатенация
очень проста и выглядит в строке 19 как арифметическая сумма, в которой к первой
строке к тому же добавлен пробел. Копирование выглядит как простое присваивание
в строке 24. Определение длины строки осуществляется с помощью вызова функции
le n g th () в строке 27.

ПРИМЕЧАНИЕ

Для использования строк C++ в код необходимо включить заголовочный
файл

s trin g :

ti n c l u d e < s trin g >
Это было сделано в строке 1 листинга 4.7.

Чтобы подробнее изучить различные функции класса s t d : : s t r i n g , обратитесь к
занятию 16, “Класс строки библиотеки STL”. Поскольку вы еще не изучали классы и
шаблоны, игнорируйте пока соответствующие разделы и уделите внимание сути при­
меров.

Резюме
На этом занятии вы познакомились с азами массивов и способами их примене­
ния. Вы научились объявлять и инициализировать их элементы, получать доступ к
значениям элементов массива и записывать их. Вы узнали, как важно не выходить за
границы массива. Это явление называется переполнением буфера (buffer overflow),
и проверка ввода перед его использованием для индексации элементов позволяет га­
рантировать отсутствие пересечения границ массива.

102

ЗАНЯТИЕ 4. Массивы и строки

Динамические массивы позволяют программисту не беспокоиться об установке
максимальной длины массива во время компиляции, а также обеспечивают лучшее
управление памятью в случае, если размер массива меньше ожидаемого максимума.
Вы также узнали, что строки в стиле С — это частный случай символьного масси­
ва, в котором конец строки отмечается завершающим нулевым символом 1\ 0 1. Кроме
того, вы узнали, что язык C++ обеспечивает намного лучшую возможность — класс
s t d : : s t r i n g , — предоставляющую удобные вспомогательные функции и позволяю­
щую определять длину строк, объединять их и выполнять иные действия с ними.

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

■ Следует ли инициализировать элементы динамического массива по причинам,
упомянутым в первом вопросе?
Вообще-то, нет. Динамический массив весьма интеллектуален. Нет необходимости
инициализировать элементы динамического массива значениями по умолчанию,
если для этого нет причин, связанных с приложением, которому нужно иметь в
массиве определенные исходные значения.

■ Когда имеет смысл использовать строки в стиле С с завершающим нулевым
символом?
Только когда кто-то приставил пистолет к вашей голове. Язык C++ предоставляет
намного более безопасное средство — класс s t d : : s t r i n g , позволяющий любому
программисту избежать использования строк в стиле С.

■ Включает ли длина строки завершающий нулевой символ?
Нет, не включает. Длина строки " H e llo W orld" составляет 11 символов, включая
пробел, но исключая завершающий нулевой символ.

■ Хорошо, но я все же хочу использовать строки в стиле С в символьных масси­
вах, определенных мною. Каким должен быть размер используемого массива?
Здесь вы столкнетесь с одной из сложностей использования строк в стиле С. Раз­
мер массива должен быть на единицу больше размера наибольшей строки, которую
он будет когда-либо содержать. Это место необходимо для завершающего нулевого
символа в конце самой длинной строки. Если бы строка "H ello World" была наи­
большей, которую предстоит содержать символьному массиву, то размер массива
должен был бы быть 11+1 символ, т.е. всего 12 символов.

Коллоквиум

103

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Посмотрите на myNumbers в листинге 4.1. Каковы индексы первого и последне­
го его элементов?

2. Если необходимо дать возможность пользователю вводить строки, использова­
ли бы вы строки в стиле С?
3. Сколько символов в строке "\0" насчитает компилятор?
4. Вы забываете завершить строку в стиле С нулевым символом. Что может слу­
читься при ее использовании?
5. Просмотрите объявление вектора в листинге 4.4 и попытайтесь создать дина­
мический массив, содержащий элементы типа char.

Упражнения
1. Объявите массив, представляющий клетки на шахматной доске; типом массива
должно быть перечисление, определяющее фигуры на доске.

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

2. Отладка. Что не так в приведенном фрагменте кода?
int myNumbers[5] = {0};
myNumbers[5] = 450; // Присваивание значения 450 пятому элементу

3. Отладка. Что не так в приведенном фрагменте кода?
int myNumbers[5];
cout « myNumbers[3];

ЗАНЯТИЕ 5

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


Что такое выражения

■ Что такое блоки, или составные выражения
■ Что такое операторы
■ Как выполнять простые арифметические и логические
операции

106

|

ЗАНЯТИЕ 5. Выражения, инструкции и операторы

Выражения
Языки программирования состоят из инструкций (statement), которые следуют
одна задругой. Давайте проанализируем первую инструкцию, которую вы изучили:
cout «

"Hello World" «

endl;

Эта инструкция использует поток c o u t для вывода текста на консоль (т.е. на
экран). Все инструкции в языке C++ заканчиваются точкой с запятой (;), определяю­
щей границу инструкции. Эта точка с запятой подобна точке, которую вы добавляете
в конце предложения разговорного языка. Следующая инструкция может начинаться
непосредственно после точки с запятой, но для удобства и удобочитаемости програм­
мисты, как правило, записывают инструкции с новой строки. Вот, например, две ин­
струкции в одной строке:
cout « "Hello World" « endl; cout «
// Одна строка, две инструкции

ПРИМЕЧАНИЕ

"Another hello" «

endl;

Пробельные символы обычно не воспринимаются компилятором. К ним от­
носятся пробелы, символы табуляции, символы новой строки и т.д. Тем не
менее пробельные символы в составе строковых литералов отображаются
при выводе.

Поэтому следующий код недопустим:
cout « "Hello
World" « endl;
// Символ новой строки в строковом литерале недопустим

Такой код обычно заканчивается сообщ ением об ошибке, указывающим, что ком­
пилятор не обнаружил в первой строке закрывающую кавычку (") и завершающую
инструкцию точку с запятой (;). Если по каким-то причинам необходимо распростра­
нить инструкцию на несколько строк, достаточно добавить последним символом сим­
вол обратной косой черты (\):
cout « "Hello \
World" « endl; // Разделение строки на две вполне допустимо

Еще один способ разместить приведенную выше инструкцию в двух строках —
это использовать два строковых литерала вместо одного:
cout « "Hello "
"World" « endl; // Два строковых литерала подряд вполне допустимы

Встретив такой код, компилятор обратит внимание на два соседних строковых ли­
терала и сам объединит их.

Составные инструкции, или блоки

107

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

Составные инструкции, или блоки
Сгруппировав инструкции в фигурных скобках { . . . } , вы создаете составную ин­

струкцию (compound statement), или блок (block).
{
int Number = 365;
cout « "Этот блок содержит две инструкции" «

endl;

}
Как правило, блок объединяет несколько связных инструкций. Блоки особенно по­
лезны при применении условной инструкции i f и циклов, которые рассматриваются
на занятии 6, “Управление потоком выполнения программы”.

Использование операторов
Операторы (operator) в C++ представляют собой инструменты, предоставляемые
языком для работы с данными, их преобразования, обработки и принятия решений на
их основе.

Оператор присваивания (=)
Оператор присваивания (assignment operator) мы уже использовали в этой книге.
Он вполне интуитивно понятен:
int daysInYear = 365;

Приведенное выше выражение использует оператор присваивания для инициа­
лизации целочисленной переменной значением 365. Оператор присваивания заме­
няет значение, содержащееся в операнде слева от оператора присваивания (назы­
ваемого 1-значением (1-value)), значением операнда справа (называемого г-значением
(r-value)).

Понятие I- и г-значений
Зачастую 1-значения называют областями памяти. Такая переменная, как d ays In
Year, из приведенного выше примера фактически является дескриптором области па­
мяти и, соответственно, 1-значением. С другой стороны, r-значения могут быть самим
содержимым области памяти.

ЗАНЯТИЕ 5. Выражения, инструкции и операторы

108

В се 1-значения могут быть r-значениями, но не все r-значения могут быть
1-значениями. Чтобы понять это лучше, рассмотрим следующий пример, который не
имеет никакого смысла, а потому не будет компилироваться:
365 = daysInYear;

Операторы сложения (+), вычитания (-),
умножения (*), деления (/) и деления по модулю (%)
Вы можете выполнять арифметические операции между двумя операндами, ис­
пользуя оператор + для сложения, оператор - для вычитания, оператор * для умноже­
ния, оператор / для деления и оператор %для деления по модулю:
int
int
int
int
int
int
int

numl = 22;
num2 = 5;
addNums
subtractNums
multiplyNums
divideNums
moduloNums

= numl
= numl
= numl
= numl
= numl

+ num2;
- num2;
* num2;
/ num2;
% num2;

/ / 27
/ / 17
/ / 110
4
//
2
//

Оператор деления ( / ) возвращает результат деления двух операндов. Однако в случае целых чисел результат не содержит дробной части, поскольку целые числа по
определению не могут ее содержать. Оператор деления по модулю (%) возвращает
остаток от деления и применим только к целочисленным значениям. В листинге 5.1
содержится простая программа, демонстрирующая выполнение арифметических дей­
ствий с двумя введенными пользователем числами.
ЛИСТИНГ 5.1. Демонстрация арифметических операторов
с введенными пользователем целыми числами
#include
using namespace std;
int main()

{
cout « "Введите два целых числа:
int numl = 0, num2 = 0;
cin » numl;
cin » num2;
10
11
12
13
14
15
16
17

cout
cout
cout
cout
cout

«
«
«
«
«

numl
numl
numl
numl
numl

return 0;

«
«
«
«
«

if
и
ii
и
и

+ и «
- и «
* и «
и «
/
ii «
%

num2
num2
num2
num2
num2

«
«
«
«
«

и
и
и
и
и

_
_
_
_
_

и «
и «
и «
ii «
и «

numl+num2
numl-num2
numl*num2
numl /num2
numl%num2

«
«
«
«
«

endl;
endl;
endl ;
endl;
endl;

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

|

109

Результат
Введите два целых числа: 365 25
365 + 25 = 390
365 - 25 = 340
365 * 25 = 9125
365 / 25 = 14
365 % 25 = 15

Анализ
Большая часть программы говорит сама за себя. Интереснее всего, вероятно, стро­
ка, использующая оператор деления по модулю %. Она возвращает остаток деления
значения переменной numl (365) на значение переменной num2 (25).

Операторы инкремента (++) и декремента (— )
Иногда в программе необходим инкремент (increment), т.е. простое увеличение
значения переменной на единицу. Это особенно важно для переменных, контроли­
рующих циклы, в которых значение переменной должно увеличиваться или умень­
шаться на единицу при каждом выполнении цикла.
Для сокращения записей наподобие num=num+1 или num=num-1 язык C++ предо­
ставляет операторы ++ (инкремента) и — (декремента).
Синтаксис их использования следующий:
int
int
int
int
int

numl
num2
num2
num2
num2

=

101 ;

=
=
=
=

numl++;
++numl;
numl—;
—numl;

//
//
//
//

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

Пример кода демонстрирует два разных способа применения операторов инкре­
мента и декремента: до и после операнда. Операторы, которые располагаются перед
операндом, называются префиксными (prefix) операторами инкремента или декремен­
та, а те, которые располагаются после, — постфиксными (postfix).

Что значит “постфиксный” и “префиксный”
Сначала следует понять различие между префиксными и постфиксными оператора­
ми, а затем использовать тот, который нужен вам в каждом конкретном случае. Резуль­
тат выполнения постфиксных операторов заключается в том, что сначала 1-значению
присваивается r-значение, а потом r-значение увеличивается или уменьшается. Это
значит, что во всех случаях использования постфиксного оператора значением пере­
менной num2 будет прежнее значение переменной numl (т.е. то значение, которое она
имела до операции инкремента или декремента).
Действие префиксных операторов прямо противоположно: сначала изменяется
r-значение, а затем оно присваивается 1-значению. В этих случаях переменные num2 и
numl имеют одинаковые значения. Листинг 5.2 демонстрирует результат выполнения
префиксных и постфиксных операторов инкремента и декремента для определенного
целого числа.

110

|

ЗАНЯТИЕ 5. Выражения, инструкции и операторы

ЛИСТИНГ 5.2, Различия между постфиксными и префиксными операторами
0:
1:
2:
3:
4:
5:
6:

#include
using namespace std;
int main()
{
int startValue = 101;
cout « "Начальное значение: ,f «

startValue «

endl;

7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28: }

int postfixlncrement = startValue++;
cout « "Постфиксный ++ = " « postfixlncrement « endl;
cout « "После постфиксного ++ startValue = "
« startValue «
endl;
startValue = 101;
// Сброс
int prefixlncrement = ++startValue;
cout « "Префиксный ++ = " « prefixlncrement « endl;
cout « "После префиксного ++ startValue = "
« startValue «
endl;
startValue = 101;
// Сброс
int postfixDecrement = startValue— ;
cout « "Постфиксный — = " « postfixDecrement « endl;
cout « "После постфиксного — startValue = "
« startValue «
endl;
startValue = 101;
// Сброс
int prefixDecrement = — startValue;
cout « "Префиксный — = " « prefixDecrement « endl;
cout « "После префиксного — startValue = "
« startValue «
endl;
return 0;

Результат
Начальное значение: 101
Префиксный ++ = 101
После постфиксного ++ startValue = 102
Постфиксный ++ = 102
После префиксного ++ startValue = 102
Постфиксный — = 101
После постфиксного — startValue = 100
Префиксный — = 100
После префиксного — startValue = 100

Анализ
Результаты показывают, чем постфиксные операторы отличаются от префиксных.
При использовании постфиксных операторов в строках 8 и 18 1-значения содержат

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

111

исходные значения целого числа, — какими они были до операций инкремента или
декремента. Использование префиксных операторов в строках 13 и 23, напротив, при­
сваивает результат инкремента или декремента. Это самое важное различие, о кото­
ром следует помнить, выбирая правильный тип оператора.
В следующих выражениях префиксные или постфиксные операторы никак не вли­
яют на результат:
startValue++;
++startValue;

// То же, что и...

Дело в том, что здесь нет присваивания исходного значения и конечный результат
в обоих случаях — увеличенное на единицу значение переменной s ta r tV a lu e .

ПРИМЕЧАНИЕ

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

+ + sta r tV a lu e предпочтительнее, чем

sta r tV a lu e + + .
Это правда, по крайней мере теоретически, поскольку при постфиксных
операторах компилятор должен временно хранить исходное значение на
случай его присваивания. Влияние на производительность в случае целых
чисел незначительно, но в случае некоторых классов этот аргумент мог бы
иметь смысл. Интеллектуальные компиляторы могут полностью устранить
различия, оптимизируя код.

Операторы равенства (=) и неравенства (!=)
Зачастую необходимо проверить выполнение или не выполнение определенного
условия прежде, чем предпринять некое действие. Операторы равенства == (операнды
равны) и неравенства ! = (операнды не равны) позволяют сделать именно это.
Результат проверки равенства имеет логический тип b o o l, т.е. tr u e (истина) или
f a l s e (ложь).
int personAge = 20;
bool checkEquality
bool checklnequality
bool checkEqualityAgain
bool checklnequalityAgain =

(personAge
(personAge
(personAge
(personAge

==
!=
==
!=

20);
100);
200);
20);

//
//
//
//

true
true
false
false

Операторы сравнения
Кроме проверки на равенство и неравенство, может возникнуть необходимость в
сравнении, значение какой переменной больше, а какой меньше. Для этого язык C++
предоставляет операторы сравнения, приведенные в табл. 5.1.

112

|

ЗАНЯТИЕ 5. Выражения, инструкции и операторы

ТАБЛИЦА 5.1. Операторы сравнения
Оператор

Назначение

Меньше ()
Меньше или равно (=)

Как свидетельствует табл. 5.1, результатом операции сравнения всегда является
значение tr u e или f a l s e , другими словами, значение типа b o o l. Следующий пример
кода демонстрирует применение операторов сравнения, приведенных в табл. 5.1:
int personAge = 20;
bool checkLessThan
bool checkGreaterThan
bool checkLessThanEqualTo
bool checkGreaterThanEqualTo
bool checkGreaterThanEqualToAgain =

(personAge
(personAge
(personAge
(personAge
(personAge

< 100);
> 100);
= 20);
>= 100)

//
//
//
//
//

true
false
true
true
false

Код листинга 5.3 демонстрирует использование этих операторов при отображении
результата на экране.
ЛИСТИНГ 5,3. Операторы равенства и сравнения_____________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:

#include
using namespace std;
int main()
{
cout « "Введите два
целых числа:" «
int numl = 0, num2
= 0;
cin » numl;
cin » num2;
bool Equality = (numl == num2);
cout « "Проверка равенства: " «

endl;

Equality «

endl;

12 :
13:
14:
15:
16:
17:
18:
19:
20:

bool Inequality = (numl != num2);
cout « "Проверка неравенства: " «

Inequality « endl;

bool GreaterThan = (numl > num2);
cout « "Результат сравнения " « numl
cout « ": " « GreaterThan « endl;
bool LessThan = (numl < num2);

«

" >" «

num2;

Использование операторов
cout
cout «

V
V

V
V

21
22
23
24
25
26
27
28
29
30
31
32
33

"Результат сравнения "
numl «
": " « LessThan « endl;

?i

^

и

«

num2;

bool GreaterThanEquals = (numl >= num2) t
cout « "Результат сравнения " « numl « " >= " «
cout « ": " « GreaterThanEquals « endl;

num2

bool LessThanEquals = (numl -24: 1
< -24: О
>= -24: 1
101: 0
< 101: 0
>= 101: 1
= и DoSomething();
// Раскомментируйте, чтобы убедиться в неработоспособности
// delete myDB; // Закрытыйдеструктор не вызываем
// Использование статического метода дляудаления
MonsterDB::Destroylnstance(myDB);
return 0;

Этот фрагмент кода не имеет вывода.

Анализ
Код предназначен только для демонстрации класса, который запрещает создание
экземпляра в стеке. Ключевым является закрытый деструктор, показанный в строке 6.
Статическая функция D e s t r o y l n s t a n c e () в строках 9 -1 2 требуется для освобож де­
ния памяти, поскольку функция m a in () не может вызвать d e l e t e для уничтожения
myDB. Вы можете убедиться в этом, раскомментировав строку 23.

264

|

ЗАНЯТИЕ 9. Классы и объекты

П рименение конструкторов для п реобразован и я типов
Ранее на этом занятии вы узнали, что конструкторы могут быть перегружены,
т.е. могут принимать один или несколько параметров. Эта возможность часто ис­
пользуется для выполнения преобразования данных одного типа в другой. Рассмо­
трим класс Human, который имеет перегруженный конструктор, принимающий целое
число.
class Human {
int age;
public:
Human(int humansAge): age(humansAge)

{}

};
// Функция, принимающая Human в качестве параметра
void DoSomething(Human person) {
cout « "Работаем c Human" « endl;
return;

Такой конструктор позволяет выполнить следующее преобразование:
Human kid(10); // Преобразование int в Human
DoSomething(kid);

ВНИМАНИЕ!

Такой преобразующий конструктор допускает выполнение неявных преоб­
разований:

Human anotherKid =11;
DoSomething(.10);

// int преобразуется в Human
// 10 преобразуется в Human!

Мы объявили D o S o m e t h in g (Human

p e r s o n ) как функцию, которая

принимает параметр типа Human, а не i n t ! Так почему же эта строка ком­
пилируется? Компилятор знает, что класс Human имеет конструктор, кото­
рый принимает целое число, и выполняет неявное преобразование вместо
вас, создавая объект типа Human из переданного целого числа и переда­
вая его в качестве аргумента в функцию.
Чтобы избежать неявных преобразований, при объявлении конструктора
следует указывать ключевое слово e x p l i c i t :

class Human
{
int age;
public:
explicit Human(int humansAge): age(humansAge)

{}

};
Применение ключевого слова e x p l i c i t не является необходимым, но во
многих случаях является хорошей практикой программирования. В следую­
щем примере в листинге 9.12 показана версия класса Human, который не
допускает неявных преобразований.

Способы использования конструкторов и деструктора

|

265

ЛИСТИНГ 9.12. Использование ключевого слова e x p l i c i t
для блокирования неявного преобразования типов
0

#include

1

using namespace std;

2
3

class Human

4

{
int age;

5
6

public:
// explicit конструктор блокирует неявные преобразования

7

8

explicit Human(int humansAge)

: age(humansAge)

{}

9

10
11
12

void DoSomething(Human person)

{

13

cout «

14

return;

15
16
17

"Работа c Human" «

endl;

int main()

18
20

Human k i d (10);
// Явное преобразование, OK
Human anotherKid = Human(11); // Явное преобразование, OK

21

DoSomething(kid);

19

// OK

22
24

// Human anotherKid2 = 1 1 ;
// DoSomething(10);

25
26

return 0;

23

// Ошибка: неявное преобразование
// Неявное преобразование

27

Результат
Работа с Human

Анализ
Строки кода, которые не выполняют никакого вывода, по меньшей мере так же
важны, как и те, которые вывод выполняют. Функция main () в строках 17-27 исполь­
зует разные варианты создания объекта класса Human, объявленного с e x p l i c i t кон­
структором в строке 8. Успешно компилируемые строки представляют собой попытки
явного преобразования, где in t использован для создания объекта типа Human. Строки
23 и 24 представляют собой варианты, включающие неявные преобразования. Эти
строки закомментированы, но если их раскомментировать, то они будут компилиро­
ваться, только когда мы удалим ключевое слово e x p l i c i t в строке 8. Таким образом,
этот пример демонстрирует, как ключевое слово e x p l i c i t защищает нас от неявных
преобразований.

266

ЗАНЯТИЕ 9. Классы и объекты

СОВЕТ____

Проблема неявных преобразований и их устранения с помощью ключево­
го слова e x p l i c i t относится и к операторам. Не забудьте использовать
ключевое слово e x p l i c i t при программировании операторов преобра­
зования, с которыми вы познакомитесь на занятии 12, “Типы операторов
и их перегрузка".

Указатель this
Указатель t h i s — это важнейшая концепция языка C++; зарезервированное клю­
чевое слово t h i s применимо в рамках класса и содержит адрес текущего объекта.
Другими словами, значение указателя t h i s — это &ob j e c t . В пределах метода класса,
когда вы вызываете другой метод, компилятор неявно передает ему при вызове указа­
тель t h i s как невидимый параметр:
class Human
{
private:
// ... Объявления закрытых членов
void Talk(string Statement)

{
cout «

Statement;

public:
void IntroduceSelf()

{
Talk("Bla blan); // To же, что Talk(this,"Bla bla")

};
Здесь представлен метод I n t r o d u c e S e l f ( ) , использующий закрытый член
() для вывода строки на экран. В действительности компилятор передает ука­
затель t h i s в вызов метода T a l k ( ) , который вызывается, как если бы имел вид
T a lk

T a l k (t h i s , " B l a

b la " ) .

С точки зрения программирования у указателя t h i s не слишком много областей при­
менения, но иногда он оказывается удобным. Например, в коде функции S e tA g e () в ли­
стинге 9.2 для доступа к переменной-члену а д е может использоваться такое выражение:
void SetAge(int HumansAge)

{
this->age = HumansAge; // To же, что и age = HumansAge

}

ПРИМЕЧАНИЕ

Указатель t h i s не передается статическим методам класса, так как ста­
тические функции не связаны с экземпляром класса. Статические методы
используются всеми экземплярами.
Чтобы использовать переменные экземпляра в статической функции, требу­
ется явно объявить параметр для передачи статической функции в качестве
аргумента указателя t h i s .

Размер класса

|

267

Размер класса
Вы изучили основные принципы определения собственного типа с использовани­
ем ключевого слова c l a s s , позволяющие инкапсулировать атрибуты данных и мето­
ды, работающие с этими данными. Оператор s i z e o f (), описанный на занятии 3, “Ис­
пользование переменных и констант”, используется для определения объема памяти,
занимаемого переменной определенного типа, в байтах. Этот оператор применим и
для классов, и сообщает сумму количества байтов, занимаемых каждым атрибутом
данных, содержащимся в объявлении класса. В зависимости от используемого компи­
лятора оператор s i z e o f () может включать или не включать для некоторых атрибутов
дополнения до границ слова. Функции-члены и их локальные переменные в опреде­
лении размера класса не участвуют. Рассмотрим листинг 9.13.
ЛИСТИНГ 9.13. Результат применения оператора s i z e o f ()
к классам и их экземплярам
0:
1:
2:
3:

tinclude
#include
using namespace std;
class MyString

4: {
5:
private:
6:
char* buffer;
7:
8:
public:
9:
MyString(const char* initString) // Конструктор по умолчанию

10:
11:
12:
13:
14:
15:
16:
17:
18:
19:

20 :
21:
22:
23:
24:
25:
26:
27:
28:
29:
30
31

{

buffer = nullptr;
if(initString != nullptr)
{
buffer = new char[strlen(initString)
strcpy(buffer, initString);
}

+1];

}
MyString(const MyString& copySource) // Копирующий конструктор
{

buffer = nullptr;
if(copySource.buffer != nullptr)
{
buffer = new char [strlen (copySource.buffer) + lbstrcpy(buffer, copySource.buffer);
}
}
-MyString()
delete[] buffer;

268
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:

ЗАНЯТИЕ 9. Классы и объекты
}
int GetLengthO
{ return strlen(buffer); )
const char* GetStringO
{ return buffer; )
};
class Human
{
private:
int age;
bool gender;
MyString name;
public:
Human(const MyStrings InputName, int InputAge, bool g)
: name(InputName), age(InputAge), gender(g) {)
int GetAge()
{ return age; }
};
int main()
{
MyString mansName ("Adam”);
MyString womansName("Eve");
cout « "sizeof(MyString)
= " « sizeof(MyString)
« endl;
cout « "sizeof(mansName)
= " « sizeof(mansName)
« endl;
cout « "sizeof(womansName) = " « sizeof(womansName) « endl;
Human firstMan(mansName, 25, true);
Human firstWoman(womansName, 18, false);
cout « "sizeof(Human) = " « sizeof(Human) « endl;
cout « "sizeof(firstMan)
= " « sizeof(firstMan)
« endl;
cout « "sizeof(firstWoman) = " « sizeof(firstWoman) « endl;
return 0;
}

Результат для 32-разрядного компилятора
sizeof(MyString) = 4
sizeof(mansName) = 4
sizeof(womansName) = 4

Чем структура отличается от класса

269

sizeof(Human) = 12
sizeof(firstMan) = 12
sizeof(firstWoman) = 12

Результат для 64-разрядного компилятора
sizeof(MyString) =
sizeof(mansName) =
sizeof(womansName)
sizeof(Human) = 16
sizeof(firstMan) =
sizeof(firstWoman)

8
8
= 8
16
= 16

Анализ
Пример несколько длинноват, поскольку содержит класс M y S t r i n g и вариант клас­
са Human, который использует тип M y S t r i n g для хранения имени (паш е), а также име­
ет новый параметр типа b o o l для пола ( g e n d e r ) .
Приступим к анализу вывода. Как можно заметить, результат выполнения опера­
тора s i z e o f () для класса совпадает с таковым для объекта класса. Следовательно,
s i z e o f ( M y S t r in g ) — то же самое, что и s i z e o f (m a n s N a m e ) , поскольку количество
байтов, использованных классом, по существу, фиксируется во время компиляции.
Не удивляйтесь, что размер в байтах объектов f i r s t M a n и f ir s t W o m a n одинаков, не­
смотря на то, что один содержит имя Adam , а другой E v e , поскольку они хранятся в
переменной M y S t r i n g : : b u f f e r , которая фактически является указателем типа c h a r * ,
размер которого составляет 4 байта (на моей 32-разрядной системе) и не зависит от
объема данных, на которые указывает.
При вычислении размера типа Hum an получается 12 байт. Строки 4 4 -4 6 свиде­
тельствуют, что класс Hum an содержит атрибуты типа i n t , b o o l и M y S t r i n g . Чтобы
освежить в памяти размер в байтах используемых встроенных типов, обратитесь к
листингу 3.5. Тип i n t использует 4 байта, тип b o o l — 1 байт, тип M y S t r in g — 4 байта
на системе автора, что в итоге не равно значению 12, которое выведено программой.
Дело в том, что на результат оператора s i z e o f () влияет дополнение до границ слова
и другие факторы.

Чем структура отличается от класса
Ключевое слово s t r u c t осталось со времен языка С и во всех практических целях
обрабатывается компилятором C++ почти так же, как ключевое слово c l a s s . Различия
кроются лишь в заданном по умолчанию модификаторе доступа ( p u b lic или p r i ­
v a te ), когда разработчик не указывает никакого модификатора. По умолчанию, если
ничего не указано, члены структуры являются открытыми ( p u b lic ) , а члены клас­
с а — закрытыми (p r iv a te ), и если не определено иное, то члены структуры остаются
открытыми при наследовании базовой структуры, а члены класса — закрытыми. На­
следование рассматривается на занятии 10, “Реализация наследования”.

ЗАНЯТИЕ 9 . Классы и объекты

270

Вариант структуры класса Human из листинга 9.13 имел бы следующий вид:
struct Human

{
// Конструктор, открытый по умолчанию (поскольку
// никакой модификатор доступа не упомянут)
Human(const MyString& inputName, int inputAge, bool inputGender)
: name(inputName), age(inputAge), gender(inputGender) {}
int GetAge()

{
return age;

private:
int age;
bool gender;
MyString name;

};
Как можно заметить, структура Human очень похожа на класс Human, и создание
экземпляра объекта структуры очень похоже на таковое для класса:
Human firstMan(MAdam", 25, true); // Экземпляр структуры Human

Объявление друзей класса
Класс не разрешает доступ извне к своим закрытым переменным-членам и ме­
тодам. Это правило не относится к тем классам и функциям, которые с помощью
ключевого слова f r i e n d объявлены дружественными (friend), как показано в лис­
тинге 9.14.
ЛИСТИНГ 9 .1 4 . Использование ключевого слова f r ie n d _____________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

tinclude
#include
using namespace std;
class Human
{
private:
friend void DisplayAge(const Human& person);
string name;
int age;

10:
11:
12:
13:
14

public:
Human(string humansName, int humansAge)
{
name = humansName;

Объявление друзей класса

271

15:
age = humansAge;
16:
}
17: };
18:
19: void DisplayAge(const Human& person)

20 :

{

21:

cout «

22 :

person.age «

endl;

}

23:
24: int main()
25: {
26:
Human firstMan("Adam”, 25);
27:
cout « "Доступ друга к закрытым членам:
28:
DisplayAge(firstMan);
29:
30:
return 0;
31: }

Результат
Доступ друга к закрытым членам: 25

Анализ
Строка 7 содержит объявление, указывающее компилятору, что функция D i s ­
() из глобальной области видимости является другом, а значит, ей разрешен
специальный доступ к закрытым членам класса Human. Закомментировав строку 7, вы
сразу получите ошибку компиляции в строке 22.
Как и функции, внешние классы также могут быть объявлены дружественными,
как показано в листинге 9.15.
p la y A g e

ЛИСТИНГ 9 .1 5 . Объявление класса другом___________________________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include
#include
using namespace std;
class Human
{
private:
friend class
string name;
int age;

Utility;

10:
11:
12:
13
14
15

public:
Human (string

humansName, int humansAge)

name = humansName;
age = humansAge;

ЗАНЯТИЕ 9. Классы и объекты

272

16:
}
17: };
18:
19: class Utility

20 :

{

21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:

public:
static void DisplayAge(const Humans person)
{
cout « person.age « endl;
}
};
int main()
{
Human firstMan(f,Adam", 25);
cout « "Доступ друга к закрытым членам:
Utility::DisplayAge(firstMan);
return 0;
}

Результат
Доступ друга к закрытым членам: 25

Анализ
Строка 7 объявляет класс U t i l i t y дружественным классу Human. Это объявление
позволяет всем методам класса U t i l i t y обращаться даже к закрытым переменнымчленам и методам класса Human.

Специальный механизм
хранения данных — u n io n
Объединение ( u n io n ) представляет собой особый тип класса, в котором в каждый
момент времени активен только один из нестатических членов-данных. Таким обра­
зом, объединение может принимать несколько членов-данных, как и класс, но с тем
отличием, что использоваться в каждый момент времени может только один из них.

Объявление объединения
Объединение объявляется с помощью ключевого слова u n io n , за которым следуют
имя объединения и его члены в фигурных скобках:
union Имя_Объединения {
Тип1 член !;

Специальный механизм хранения данных — union

|

273

Тип2 член2 ;
ТипЫ членЫ;
};
Вы можете создавать объекты и использовать объединения следующим образом:
UnionName unionObject;
unionObject.member2 = value; // member2 выбран как активный член

ПРИМЕЧАНИЕ

s t r u c t , члены u n io n по умолчанию открыты. Однако, в отличие
s t r u c t , объединения не могут использоваться в иерархиях наследо­

Подобно
от

вания.
Кроме того, при применении

s i z e o f () к объединению всегда возвраща­

ется размер наибольшего содержащегося в нем члена, даже если зтот член
в данном объекте в настоящее время неактивен.

Где используется объединение
Часто объединение используется в качестве члена s t r u c t для моделирования
сложного типа данных. В некоторых реализациях возможность объединения интер­
претировать фиксированное пространство памяти как другой тип используется для
преобразования типов или иной интерпретации памяти — практика, которая как ми­
нимум является очень спорной и не рекомендуемой к применению.
В листинге 9.16 демонстрируются объявление и применение объединений.
Л И С Т И Н Г 9 .1 6 . О б ъ я в л е н и е и и н с т а н ц и р о в а н и е о б ъ е д и н е н и я и п р и м е н е н и е

0:
1:
2:
3:
4:
5:
6:
7:

tinclude
using namespace std;
union SimpleUnion
{
int num;
char alphabet;
};

8:
9: struct ComplexType

10:
11:
12:
13:
14:
15:
16:
17:
18
19

{

enum DataType
{

Int,
Char
} Type;
union Value
int num;

s i z e o f ()

274

ЗАНЯТИЕ 9. Классы и объекты

20:

char alphabet;

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
5 6:
57:
58:
59:
60:

Value() {}
-Value() {}
}value;
};
void DisplayComplexType(const ComplexType& obj)
{
switch(obj.Type)
{
case ComplexType::Int:
cout«"union содержит число: "«obj .value.num«endl;
break;
case ComplexType::Char:
cout«"union содержит символ: "«obj .value.alphabet«endl;
break;
}
}
int main()
{
SimpleUnion ul, u2;
ul.num = 2100;
u2.alphabet = 'C1;
cout «
"sizeof(ul) с числом: " « sizeof(ul) « endl;
cout «
"sizeof(u2) ссимволом:
" « sizeof(u2) «
endl;
ComplexType myDatal, myData2;
myDatal.Type = ComplexType::Int;
myDatal.value.num = 2017;
myData2.Type = ComplexType::Char;
myData2.value.alphabet = 1X ';
DisplayComplexType(myDatal);
DisplayComplexType(myData2);
return 0;
}

Результат
sizeof(ul) с числом: 4
sizeof(u2) с символом: 4
union содержит число: 2017
union содержит символ: X

Агрегатная инициализация классов и структур

|

275

Анализ
В приведенном примере показано, что s i z e o f () объединений u l и и 2 возвраща­
ет одинаковое количество выделенной для обоих объектов памяти, несмотря на то,
что u l используется для хранения целого числа, а и2 — для хранения c h a r , а размер
c h a r меньше, чем размер i n t . Дело в том, что компилятор выделяет для объединения
количество памяти, которое потребляется крупнейшим объектом, который может со­
держаться в нем. Структура C o m p le x T y p e , определенная в строках 9 -2 5 , содержит
перечисление D a t a T y p e , которое используется для указания характера объекта, храня­
щегося в объединении, помимо данных-члена, который представляет собой объедине­
ние с именем v a l u e . Такое сочетание структуры с перечислением, используемым для
указания сведений о хранимом типе, и объединение для хранения значения является
распространенным применением объединения. Например, широко используемая в
Windows структура V A R IA N T использует аналогичный подход. Эта комбинация приме­
нена в функции D is p la y C o m p le x T y p e (), определенной в строках 2 7 -3 9 , которая ис­
пользует перечисление в конструкции s w i t c h - c a s e . В качестве примера мы включили
в это объявление конструктор и деструктор — в листинге 9.16 это не обязательно, так
как объединение содержит старые простые типы данных. Однако если объединение
состоит из пользовательских типов наподобие класса или структуры, такие конструк­
тор и деструктор могут потребоваться.

СОВЕТ

Ожидается, что в стандарт С++17 будет включена безопасная с точки зре­
ния типов альтернатива объединению. Чтобы узнать о s t d : : v a r i a n t , об­
ратитесь к занятию 29, “Что дальше”.

Агрегатная инициализация
классов и структур
Показанный далее синтаксис инициализации называется синтаксисом агрегатной

инициализации (aggregate initialization):
Ти п Имя_объекта

=

{ аргумент!, . . . ,

аргументы}

;

Начиная с С ++11 имеется альтернативный вариант:
Тип Имя_объекта {аргумент!,

...,

аргументы};

Агрегатная инициализация может быть применена к агрегатам, а потому важно
понимать, какие типы данных попадают в эту категорию.
Вы уже встречались с агрегатной инициализацией при инициализации массивов на
занятии 4, “Массивы и строки”.
int myNums[] = { 9, 5, -1 }; // myNums имеет тип int[3]
char hello[6] = { 'h', 'ef, ’1', '1', 'o', ' \0* };

Однако термин агрегат не ограничивается массивами простых типов, таких как
целые числа или символы, но распространяется и на классы (а потому на структуры

276

|

ЗАНЯТИЕ 9. Классы и объекты

и объединения тоже). Существуют ограничения, введенные в стандарте на специфи­
кации структуры или класса, который может быть назван агрегатом. Эти ограничения
несколько различны в разных версиях стандарта C++. Тем не менее можно с уверен­
ностью утверждать, что классы/структуры, которые состоят из открытых инестати­
ческих данных-членов, не содержащие закрытых или защищенных данных-членов, не
содержащие виртуальных функций-членов, использующие только открытое наследо­
вание, т.е. не p r iv a t e , p r o t e c t e d или виртуальное наследование (или не использую­
щие никакого), и не имеющие пользовательских конструкторов, являются агрегатами
и могут быть инициализированы соответствующим образом.

СОВЕТ

Наследование рассматривается на занятиях 10, “Реализация наследова­
ния”, и И , “Полиморфизм”.

Таким образом, приведенная далее структура удовлетворяет указанным требовани­
ям и, будучи агрегатом, может быть инициализирована как таковой:
struct Aggregatel {
int num;
double pi;

};
Инициализация:
Aggregatel al{ 2017, 3.14 };

Еще один пример:
struct Aggregate2 {
int num;
char hello[6];
int impYears[5];

};
Инициализация:
Aggregate2 a2 {42, {'h\ 'e', 11 1, '1', 'o'b
{1998, 2003, 2011, 2014, 2017}};

Листинг 9.17 содержит пример, демонстрирующий применение агрегатной ини­
циализации к структурам и классам.

ЛИСТИНГ 9.17, А гр е га тн а я
0:
1:
2:
3:
4:
5:

#include
#include
using namespace std;

class Aggregatel
{
6:
public:

и н и ц и а л и з а ц и я к л а с с а ________________________________________

Агрегатная инициализация классов и структур
7:
8:
9: };

|

277

int num;
double pi;

10 :
11: struct Aggregate2

12 :
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:

{

char hello[6];
int impYears[3];
string world;
};
int main()
{
int myNums[] = ( 9, 5, -1 }; // myNums имеет тип int[3]
Aggregatel al{ 2017, 3.14 };
cout « "Pi приближенно равно: " « al.pi « endl;
Aggregate2 a2{ {’h', 'e', fl f, '1', 'o'},
{2011, 2014, 2017}, "world"};
// Альтернативный вариант
Aggregate2 a2_2{'h\ fe', '1',
’1', 'o', '\0',
2011, 2014, 2017, "world"};
cout « a2.hello « ’ ' « a2.world « endl;
cout « "Новый стандарт C++ будет принят в "
« а2.impYears [2] « " году" «endl;
return 0;
}

Результат
Pi приближенно равно: 3.14
hello world
Новый стандарт C++ будет принят в 2017 году

Анализ
В листинге показано, как можно использовать агрегатную инициализацию для соз­
дания экземпляров классов (или структур). Тип A g g r e g a te l, определенный в строках
4 -9 , представляет собой класс с открытыми членами данных, a A g g reg a te2 , опреде­
ленный в строках 11-16, является структурой. Строки 2 1 ,2 4 ,2 5 , 27 и 28 демонстриру­
ют агрегатную инициализацию объектов c l a s s и s t r u c t соответственно. Мы обраща­
емся к членам класса/структуры, демонстрируя, что компилятор корректно размещает
инициализирующие значения в соответствующих данных-членах. Обратите внима­
ние, что некоторые члены являются массивами и что член s t d : : s t r i n g в A ggregate2
также был инициализирован с помощью этой конструкции в строке 24.

|

278

ЗАНЯТИЕ 9. Классы и объекты

ВНИМАНИИ

Агрегатная инициализация инициализирует только первый нестатический
член объединения. Агрегатная инициализация объединений, объявленных
в листинге 9.16, должна иметь следующий вид:

43:
SimpleUnion ul{ 2100 }, u2{ 'С f };
// В u2 член num(int) инициализируется значением 'С1
// (ASCII 67), хотя программист хотел инициализировать
// член alphabet (char)
Таким образом, для ясности не стоит использовать синтаксис агрегатной
инициализации для объединений, несмотря на его применение в листин­
ге 9.16.

constexpr с классами и объектами
Мы уже знакомились с c o n s t e x p r на занятии 3, “Использование переменных и
констант”, на котором узнали, что это ключевое слово предлагает мощный способ по­
высить производительность приложения C++. Помечая функции, работающие с кон­
стантами или константными выражениями, как c o n ste x p r , мы поручаем компилятору
вычисление этих функций и вставку их результата вместо команд, которые вычисляют
результат во время выполнения приложения. Это ключевое слово может также ис­
пользоваться с классами и объектами, которые рассматриваются как константы, как
показано в листинге 9.18. Обратите внимание, что компилятор игнорирует ключевое
слово c o n s te x p r , если класс или функция используется с объектами, которые не яв­
ляются константными.
ЛИСТИНГ 9,18, Использование c o n s te x p r с классом Human__________________________
0: #include
1: using namespace std;
2:
3: class Human
4: {
5:
int age;
6:
public:
7:
constexpr Human(int humansAge) :age(humansAge)
8:
constexpr int GetAgeO const { return age; }
9: };
10:
11: int main()

12 :

{}

{

13:
14:
15:
16:
17:
18:
19: }

constexpr Human somePerson(15);
const int hisAge = somePerson.GetAge();
Human anotherPerson(45); // Неконстантное выражение
return 0;

Резюме

|

279

Результат


Анализ
Обратите внимание на незначительные изменения в классе Hum an в строках 3 -9 .
Теперь он использует c o n s t e x p r в объявлениях конструктора и функции-члена
G e t А д е ( ) . Это маленькое дополнение указывает компилятору на то, что он должен
создавать и использовать экземпляры класса Hum an как константное выражение, где
это возможно. s o m e P e r s o n в строке 13 объявляется как константный экземпляр и
используется как таковой в строке 14. Поэтому данный код будет выполняться ком­
пилятором, который будет генерировать высокопроизводительный код времени вы­
полнения. a n o t h e r P e r s o n в строке 16 не объявлен как константный экземпляр, так
что связанный с ним код может не рассматриваться компилятором как константное
выражение.

Резюме
На этом занятии вы познакомились с одной из самых фундаментальных концеп­
ций языка C++ — классом. Вы узнали, что класс инкапсулирует данные-члены и
функции-члены для работы с ними. Вы увидели, как такие модификаторы д осту­
па, как p u b l i c и p r i v a t e , позволяют абстрагировать данные и функции, которые не
должны быть видимы сущностям вне класса. Вы изучили концепцию копирующих
конструкторов и перемещающих конструкторов, введенных стандартом С + +11, кото­
рые позволяют оптимизировать нежелательные копирования. Вы также рассмотрели
некоторые частные случаи, когда все эти элементы объединяются, позволяя реализо­
вать такие проектные шаблоны, как синглтон.

Вопросы и ответы
■ В чем разница между экземпляром класса и объектом того же класса?
По существу, никакой. Создавая экземпляр класса, вы получаете объект.

■ Как лучше получить доступ к члену: используя оператор точки ( .) или опера­
тор указателя (->)?
Если у вас есть указатель на объект, то лучше использовать оператор указателя.
Если объект создан в стеке как локальная переменная, то лучше подойдет оператор
точки.

■ Должен ли я всегда создавать копирующий конструктор?
Если среди переменных-членов вашего класса есть интеллектуальные указатели,
строковые классы или контейнеры STL, такие как s t d : : v e c t o r , то копирующий
конструктор по умолчанию, предоставляемый компилятором, гарантирует вызов их
копирующих конструкторов. Однако, если среди членов вашего класса есть простой
указатель (такой, как i n t * для динамического массива вместо s t d : : v e c t o r < in t > ) ,

280

|

ЗАНЯТИЕ 9. Классы и объекты

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

■ У моего класса есть только один конструктор, параметр которого был опреде­
лен со значением по умолчанию. Это все еще конструктор по умолчанию?
Да. Если экземпляр класса может быть создан без аргументов, то считается, что
у класса есть конструктор по умолчанию. У класса может быть только один кон­
структор по умолчанию.

■ Почему в некоторых примерах на данном занятии используются такие функ­
ции, как S etA geO , для установки значения переменных, как, например,
Human:: age? Почему бы не сделать переменную а д е открытой и не присваи­
вать ей значение, когда нужно?
С технической точки зрения открытая переменная-член H u m an : : a g e также вполне
работоспособна. Однако с точки зрения дизайна данные-члены имеет смысл де­
лать закрытыми. Функции доступа, такие как G e t A g e () или S e t A g e ( ) , являются
корректным и рекомендуемым средством обращения к таким закрытым даннымчленам, позволяя выполнять проверки на ошибки, прежде чем, например, будет
выполнено присваивание значения переменной Hum an: :a g e .

■ Почему оригинал копирующему конструктору передается по ссылке?
Прежде всего, такой копирующий конструктор ожидается компилятором. Дело в
том, что копирующий конструктор, получая оригинал по значению, вызвал бы сам
себя, а это привело бы к бесконечной рекурсии.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1.

Когда я создаю экземпляр класса с помощью оператора new, где он создается, в
стеке или в динамической памяти?

2. В моем классе есть простой указатель i n t * на динамический массив целых
чисел. Будет ли размер, возвращаемый оператором s i z e o f , зависеть от количе­
ства целых чисел в динамическом массиве?
3. Все члены моего класса являются закрытыми, и для него не объявлен ни один
дружественный класс или функция. Кто может обратиться к этим членам?
4. Может ли один метод класса вызвать другой?

Коллоквиум

281

5. Для чего используется конструктор?
6. Для чего используется деструктор?

Упражнения
1. Отладка. Что не так в следующем объявлении класса?
Class Human
{
int age;
string name;
public:
Human() {}

}
2. Как пользователь класса из упражнения 1 может обратиться к переменнойчлену Human: : age?
3. Напишите лучшую версию класса из упражнения 1, в которой все параметры
инициализируются с использованием списка инициализации в конструкторе.
4. Напишите класс C i r c l e , который вычисляет площадь и периметр по радиу­
су, который передается классу в качестве параметра при создании экземпляра.
Число к должно содержаться в константном закрытом члене, к которому нельзя
обратиться извне класса.

ЗАНЯТИЕ 10

Реализация
наследования
Объектно-ориентированное программирование основано
на четырех важных аспектах: инкапсуляции (encapsulation),
абстракции (abstraction), наследовании (inheritance) и поли­
морфизме (polymorphism). Наследование — это мощнейший
способ многократного использования атрибутов и крае­
угольный камень полиморфизма.
На этом занятии...

■ Наследование в контексте программирования
■ Синтаксис наследования C + +
■ Открытое, закрытое и защищенное наследование
■ Множественное наследование
■ Проблемы, вызванные сокрытием методов базового
класса и срезкой

284

|

ЗАНЯТИЕ 10. Реализация наследования

Основы наследования
То, что Том Смит унаследовал от своих предков, — это, прежде всего, фамилию,
которая и делает его Смитом. Кроме того, он унаследовал некоторые знания, которые
его родители преподали ему, и навыки, приобретенные в лесу, где семья Смита живет
на протяжении многих поколений. Все эти атрибуты вместе характеризуют Тома как
потомственного лесоруба Смита.
В программировании вы нередко будете встречаться с ситуациями, в которых ис­
пользуемые компоненты обладают сходными атрибутами с незначительными различи­
ями в деталях или поведении. Один из способов работы в такой ситуации — сделать
все компоненты классами, каждый из которых реализует все атрибуты, в том числе
общие. Другое решение — использовать наследование, чтобы позволить подобным
классам получать общие атрибуты и общие функциональные возможности из реали­
зующего их базового класса и переопределять их таким образом, чтобы реализовать
поведение, делающее каждый класс индивидуальным. Последнее решение зачастую
предпочтительнее. Добро пожаловать в мир наследования объектно-ориентированного
программирования (рис. 10.1)!

РИС. 10.1. Наследование классов

Наследование и порождение
На рис. 10.1 приведена схема отношений между базовым классом (base class) и
порожденными из него производными классами (derived class). Прямо сейчас трудно
представить, чем могут быть базовый и производный классы; пока просто постарай­
тесь понять, что производный класс унаследован от базового класса и в этом смысле
является базовым классом, как Том является Смитом.

ПРИМЕЧАНИЕ

Отношение ЯВЛЯЕТСЯ между производным и базовым классами примени­
мо только к открытому наследованию (public inheritance). Данное занятие
начинается с рассмотрения открытого наследования, чтобы объяснить саму
концепцию наследования на примере его наиболее распространенной
формы, прежде чем перейти к закрытому и защищенному наследованию.

О сновы наследования

285

ПРИМ ЕЧАНИЕ

Чтобы проще объяснить эту концепцию, рассмотрим базовый класс B i r d (Птица).
Из класса B i r d порождены классы C ro w (Ворона), P a r r o t (Попугай) и K i w i (Киви).
Класс B i r d определяет большинство основных атрибутов птицы, таких как наличие
крыльев, откладывание яиц, способность летать (у большинства). Производные клас­
сы, такие как C ro w , P a r r o t и K i w i , наследуют эти атрибуты и корректируют их (на­
пример, класс K i w i , представляющий нелетающую птицу, не имеет реализации ме­
тода F l y () (летать)). Еще несколько примеров наследования приведено в табл. 10.1.
ТАБЛИЦА 10.1. Примеры открытого наследования из повседневной жизни
Базовый класс

Примеры производных классов

F i s h (Рыба)

G o l d f i s h (Золотая рыбка), C a r p (Карп), T u n a (Тунец) (Тунец я в л я е т с я
ры бой)

Mammal

Human (Человек), E l e p h a n t (Слон), L i o n (Лев), P l a t y p u s (Утконос)

(М лекопитаю щ ее)

(Утконос я в л я е т с я м л екоп итаю щ им )

B i r d (Птица)

C ro w (Ворона), P a r r o t (Попугай), O s t r i c h (Страус), K i w i (Киви),
P l a t y p u s (Утконос) (Утконос я в л я е т с я такж е и птицей!)

S h a p e (Ф орма)

C i r c l e (Круг), P o ly g o n (М но го угольн ик) (Круг я в л я е т с я ф орм ой)

P o ly g o n

T r i a n g l e (Треугольник), O c t a g o n (В осьм и угол ьни к) (В о сь м и у го л ьн и к

(М ногоугольник)

я в л я е т с я м н о го у го л ьн и к о м , кото р ы й я в л я е т с я ф орм ой)

Эти примеры показывают, что если надеть объектно-ориентированные очки, то
примеры наследования можно увидеть повсюду вокруг. F i s h — это базовый класс
для класса T u n a , поскольку тунец, как и карп, является рыбой и имеет все присущие
рыбе характеристики, такие как холоднокровность. Однако тунец отличается от карпа
внешним видом, скоростью плавания и тем, что он — морская рыба. Таким обра­
зом, классы T u n a и C a r p наследуют общие характеристики от общего базового класса
F i s h , но специализируют атрибуты своего базового класса, чтобы отличаться один от
другого (рис. 10.2).

Р И С - 1 0 -2 . И е р а р х и ч е с к и е о тн о ш е н и я м е ж д у к л а с с а м и T u n a , C a r p и F i s h

286

|

ЗАНЯТИЕ 10. Реализация наследования

Утконос может плавать, но все же это млекопитающее животное, поскольку кормит
детенышей молоком, птица (и похож на птицу), поскольку кладет яйца, и рептилия,
поскольку ядовит. Таким образом, класс P l a t y p u s можно представить как наследника
двух базовых классов, класса M am m al и класса B i r d , который наследует возможности
как млекопитающих, так и птиц. Такое наследование называется множественным
наследованием (multiple inheritance) и обсуждается позже.

Синтаксис наследования C++
Как унаследовать класс C a r p от класса F i s h и вообщ е унаследовать класс
Производный от класса Базовый ? В языке C++ для этого используется следующий
синтаксис:
// Объявление базового класса
class Базовый

{
11 ... члены базового класса
};
// Объявление производного класса
class Производный: Модификатор__Доступа Базовый

{
I / ... члены производного класса
};

Модификатор_Доступа может быть как p u b l i c (используется чаще всего) для отно­
шений “производный класс является базовым классом”, так и p r i v a t e или p r o t e c t e d
для отношений “производный класс имеет базовый класс”.
Иерархическое представление наследования класса C a r p , производного от класса
F i s h , может иметь следующий вид:
class Fish

// Базовый класс

{
// ... члены класса Fish

class Carp: public Fish // Производный класс
{
// ... члены класса Carp

};
Пригодные для компиляции версии классов C a r p и T u n a , производных от класса
представлены в листинге 10.1.

F is h ,

Примечание о терминологии
Читая о наследовании, вы встретитесь с такими терминами, как наследуется от (inherits from) и
производный от (derives from). Они имеют одинаковый смысл.
Точно так же базовы й класс (base class) иногда называют суперклассом (super class). Класс, про­
изводный от базового, называется производным классом (derived class), но может упоминаться
и как подкласс (subclass).

Основы наследования
Л И С Т И Н Г 1 0 .1 . П р и м е р и е р а р хи и н а с л е д о в а н и я

0: #include
1: using namespace std;

2:
3: class Fish
4: {
5:
public:
6:
bool isFreshWaterFish;

7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47
48
49

void Swim()
{
if (isFreshWaterFish)
cout « "Пресноводный" « endl;
else
cout « "Морской" « endl;
}
};
class Tuna: public Fish
{
public:
Tuna()
(

isFreshWaterFish = false;
}
};
class Carp: public Fish
{
public:
Carp()
{
isFreshWaterFish = true;
}
};
int main()
{
Carp myLunch;
Tuna myDinner;
cout «

"Моя еда:" «

cout « "Обед: ";
myLunch.Swim();
cout « "Ужин: ";
myDinner.Swim ();
return 0;

endl;

|

287

288

|

ЗАНЯТИЕ 10. Реализация наследования

Результат
Моя еда:
Обед: Пресноводный
Ужин: Морской

Анализ
Обратите внимание на строки 37 и 38 в функции m a in (), где создаются объекты
m y L u n c h и my D in n e r классов C a r p и T u n a соответственно. В строках 43 и 46 я запраши­
ваю свой обед и ужин о среде обитания, вызывая метод S w im (), который они должны
под держивать. Теперь посмотрим на определение класса T u n a в строках 17-24 и класса
C a r p в строках 26-33. Как можно видеть, эти классы очень компактны, а их конструкто­
ры устанавливают соответствующие значения булева флага F i s h : : is F r e s h W a t e r F i s h .
Позднее этот флаг используется в функции F i s h : : S w im ( ). Но ни один из производ­
ных классов, как мы видим, не содержит определение метода S w im (), который, тем
не менее, успеш но вызывается в функции m a in ( ) . Дело в том, что S w im ( ) является
открытым членом базового класса F i s h (от которого унаследованы рассматриваемые
нами классы), определенного в строках 3-1 5 . Открытое наследование в строках 17 и 26
автоматически предоставляет открытые члены базового класса, включая метод S w im ( ) ,
экземплярам производных классов, с которыми мы и работаем в функции m a in ( ) .

Модификатор доступа protected
В листинге 10.1 у класса F i s h есть открытый атрибут i s F r e s h W a t e r F i s h , значе­
ние которого устанавливается производными классами T u n a и C a r p , чтобы настроить
(или специализировать (specialize)) поведение рыбы и адаптировать ее к морской и
пресной воде. Однако в коде листинга 10.1 имеется серьезный недостаток: если вы
захотите, то даже в функции m a in () сможете вмешаться в значение этого флага, кото­
рый помечен как p u b l i c , а следовательно, открыт для изменения извне класса F i s h с
помощью, например, следующего кода:
myDinner.isFreshWaterFish = true; // Сделать тунца пресноводной рыбой!

Очевидно, что этого следует избегать. Необходим механизм, позволяющий опреде­
ленным атрибутам в базовом классе быть доступными только для производного клас­
са, но не для внешнего мира. Это означает, что логический флаг i s F r e s h W a t e r F i s h в
классе F i s h должен быть доступен для классов T u n a и C a r p , которые происходят от
него, но не для функции m a in ( ) , в которой создаются экземпляры класса T u n a или
C a r p . В этом случае вам пригодится ключевое слово p r o t e c t e d .

ПРИМЕЧАНИЕ

Ключевое слово p r o t e c t e d (защищенный), так же, как и слова p u b l i c
(открытый) и p r i v a t e

(закрытый), является модификатором доступа.

Объявляя атрибут как p r o t e c t e d , вы фактически делаете его доступным
для производных классов и друзей, одновременно делая его недоступным
для всех остальных, включая функцию m a in ().

Основы наследования

|

28 9

Если необходимо, чтобы определенный атрибут в базовом классе был доступен
для производных классов, следует использовать модификатор доступа p r o te c te d , как
показано в листинге 10.2.
ЛИСТИНГ 1 0 .2 . Улучшенный класс F ish , использующий ключевое слово p r o t e c t e d
для предоставления его переменных-членов только производным классам
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include
using namespace std;
class Fish
{
protected:
bool isFreshWaterFish;
public:
void Swim()

10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:

{

if (isFreshWaterFish)
cout « "Пресноводный" « endl;
else
cout « "Морской" « endl;
}
};
class Tuna: public Fish
{
public:
Tuna()
{

isFreshWaterFish = false;
(
};
class Carp: public Fish
{
public:
Carpi)

31:
32:
33:
34: };
35:
36: int
37: {
38:
39:
40
41

{
isFreshWaterFish = true;
}

main()
Carp myLunch;
Tuna myDinner;
cout «

"Моя еда:" «

endl;

290

|

ЗАНЯТИЕ 10. Реализация наследования

42:
43:
cout « "Обед:
44:
myLunch.Swim();
45:
46:
cout « "Ужин:
47:
myDinner.Swim();
48
49:
// Снимите комментарий со строки ниже, чтобы убедиться в
50:
// недоступности защищенных членов извне иерархии класса
51:
// myLunch.isFreshWaterFish = false;
52:
53: return 0;
54: }

Результат
Моя еда:
Обед: Пресноводный
Ужин: Морской

Анализ
Несмотря на совпадение вывода листингов 10.1 и 10.2, здесь в класс F i s h , опреде­
ленный в строках 3 -1 6 , внесены фундаментальные изменения. Первое и самое оче­
видное изменение — логическая переменная-член F i s h : : i s F r e s h W a t e r F i s h стала
защищенной, а следовательно, недоступной из функции m a in (), как свидетельствует
строка 51 (снимите комментарий, чтобы увидеть ошибку компиляции). Тем не менее
этот параметр с модификатором доступа p r o t e c t e d доступен из производных классов
T u n a и C a r p , что видно из строк 23 и 32 соответственно. Фактически эта небольшая
программа демонстрирует использование ключевого слова p r o t e c t e d для обеспече­
ния защиты атрибута базового класса, который должен быть унаследован, от обраще­
ния извне иерархии класса.
Это очень важный аспект объектно-ориентированного программирования — ком­
бинация абстракции данных и наследования для обеспечения безопасного наследова­
ния производными классами атрибутов базового класса, в которые не может вмешать­
ся никто извне этой иерархической системы.

Инициализация базового класса — передача
параметров базовому классу
Что если базовый класс содержит перегруженный конструктор, которому во вре­
мя создания экземпляра требуется передать аргументы? Как будет инициализирован
такой базовый класс при создании экземпляра производного класса? Фокус — в ис­
пользовании списков инициализации и вызове соответствующего конструктора ба­
зового класса через конструктор производного класса, как демонстрирует следую ­
щий код:

Основы наследования

|

291

class Base

{
public:
Base(int someNumber) // перегруженный конструктор

{
// Сделать нечто с someNumber

Class Derived: public Base

{
public:
Derived!): Base(25) // Создать экземпляр Base с аргументом 25

{
// Код конструктора производного класса

}
Этот механизм может весьма пригодиться в классе F ish при предоставлении логи­
ческого входного параметра для его конструктора, инициализирующего переменнуючлен F i s h : : is F r e s h W a te r F is h . Так базовый класс F is h может гарантировать, что
каждый производный класс вынужден будет указать, является ли рыба пресноводной
или морской, как показано в листинге 10.3.
Л И СТИ Н Г 1 0 .3 .

0
1

Конструктор производного класса со списками инициализации

#include
using namespace std;

2

3

class Fish

4
5

{
protected:
bool isFreshWaterFish; // Доступно только производным классам

6

7

8
9

public:
// Конструктор класса Fish
Fish(bool IsFreshWater) : isFreshWaterFish(IsFreshWater){}

10
11
12
13
14
15
16
17
18
19
20

void Swim()

{
if (isFreshWaterFish)
cout « "Пресноводный" « endl;
else
cout « "Морской" « endl;

}

21

class Tuna: public Fish

22
23

{
public:

292
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
4 6:
47:

|

ЗАНЯТИЕ 10. Реализация наследования
Tuna(): Fish(false)

{}

};
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
};
int main()
{
Carp myLunch;
Tuna myDinner;
cout

« "Моя еда:" «

endl;

cout « "Обед: ";
myLunch.Swim();
cout « "Ужин: ";
myDinner.Swim();
return 0;
}

Результат
Моя еда:
Обед: Пресноводный
Ужин: Морской

Анализ
Теперь у класса F i s h есть конструктор, который получает заданный по умолча­
нию параметр, инициализирующий переменную F i s h : : i s F r e s h W a t e r F i s h . Таким
образом, единственная возможность создать объект класса F i s h — это предоставить
параметр, который инициализирует данный защищенный член. Так класс F i s h гаран­
тирует, что защищенный член класса не будет содержать случайного значения, если
пользователь производного класса забудет его установить. Теперь производные клас­
сы T u n a и C a r p вынуждены определить конструктор, создающий экземпляр базового
класса F i s h с правильным параметром ( t r u e или f a l s e , указывающим, пресноводная
ли это рыба), как показано в строках 24 и 30 соответственно.

Основы наследования

ПРИМЕЧАНИЕ

Как
да

можно
не

заметить

обращался

в листинге

непосредственно

10.3,

производный

к логической

класс

293

никог­

переменной-члену

F i s h : : is F r e s h W a t e r F i s h , несмотря на то что она является защищен­
ной, поскольку ее значение было установлено конструктором класса F i s h .
Чтобы гарантировать максимальную безопасность, если производные
классы не нуждаются в доступе к атрибуту базового класса, пометьте его
как p r i v a t e . Таким образом, более безопасную версию класса можно
получить, помечая член F i s h : : i s F r e s h W a t e r F i s h как p r i v a t e (как
это сделано в листинге 10.4), после чего доступ к нему имеет только сам
класс F i s h .

Перекрытие методов базового класса в производном
Если производный класс реализует те же функции с теми же возвращаемыми зна­
чениями и сигнатурами, что и базовый класс, от которого он порожден, то тем самым
он перекрывает этот метод базового класса, как показано в следующем коде:
class Base
{
public:
void DoSomething()

{
// Код реализации... Делает нечто

);
class Derived:public Base
{
public:
void DoSomething()

{
// Код реализации... Делает нечто иное

Таким образом, если метод D o S o m e t h in g () вызывается с использованием экзем­
пляра класса D e r iv e d , то при этом никак не используется соответствующая функцио­
нальность класса B a s e .
Если классы T u n a и C a r p должны реализовать собственный метод S w im ( ) , который
имеется также и в базовом классе как F i s h :: S w im (), то вызов этого метода в функции
m a in () так, как показано в следующем отрывке листинга 10.3
36:
// ...
44:

Tuna myDinner;
Другие строки
myDinner.Swim();

привел бы к выполнению локальной реализации метода Tuna: : Swi m(), которая, по
существу, перекрывает метод F i s h :: Swim () базового класса. Это демонстрирует лис­
тинг 10.4.

294

|

ЗАНЯТИЕ 10. Реализация наследования

ЛИСТИНГ 10.4. Производные классы Tuna и Carp,
перекрывающие метод Swim () базового класса Fish
0: tinclude
1: using namespace std;

2:
3:
4:
5:
6:
7:
8:
9:
10:

class Fish
{
private:
bool isFreshWaterFish;
public:
// Конструктор класса Fish
Fish(bool isFreshWater) : isFreshWaterFish(isFreshWater){}

11 :
12:
13:
14:
15:
16:
17:
18:
19:
20 :
21:
22 :
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45

void Swim()
{
if (isFreshWaterFish)
cout « "Пресноводный" « endl;
else
cout « "Морской" « endl;
}
};
class Tuna: public Fish
{
public:
Tuna(): Fish(false) {}
void Swim()
{
cout « "Тунец быстроплавает"
}

« endl;

};
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
void Swim()
{
cout « "Карп медленно плавает" «
}
};
int main()
{
Carp myLunch;

endl;

Основы наследования
46:
47:

Tuna myDinner;

48:

cout «

"Моя еда:" «

50:

cout «

"Обед:

51:

myLunch.Swi m ();

|

295

endl;

49:
";

52:
53:

cout «

54:

myDinner.Swim();

"Ужин:

";

55:
56:
57: }

return 0;

Результат
Моя еда:
Обед: Карп медленно плавает
Ужин: Тунец быстро плавает

Анализ
Вывод показывает, что вызов метода m yLunch . Swim () в строке 51 — это вызов
метода C a r p : : Swim ( ), определенного в строках 3 7 -4 0 . Аналогично вызов метода
m yD inner . Swim () в строке 54 — это вызов метода T una :: Swim (), определенного на
строках 26-2 9 . Другими словами, реализация метода F is h : :Swim() в базовом клас­
се F ish , показанная в строках 12-18, перекрывается идентичной функцией Swim(),
определенной в классах Tuna и Carp, происходящих от класса F ish . Единственный
способ вызова именно метода F i s h : : Swim( ) — это использовать в функции m ain ()
оператор разрешения области видимости ( : : ) в явном вызове метода F is h :: Swim ( ) ,
как будет показано позже на этом занятии.

Вызов перекрытых м етодов б а зо в о г о класса
В листинге 10.4 вы видели пример производного класса Tuna, переопределяющего
функцию Swim( ) из класса F ish путем реализации собственной версии той же функ­
ции. Таким образом:
Tuna myDinner;
myDinner.Swim(); I I Будет вызван Tuna::S w i m ()

Если в листинге 10.4 вы захотите вызвать функцию F i s h : :Swim() в функции
main (), то используйте оператор разрешения области видимости ( : : ) со следующим
синтаксисом:
myDinner.Fish::Swim(); // Вызов Fish::Swim() для экземпляра Tuna

В листинге 10.5 показан вызов члена базового класса с использованием экземпляра
производного класса.

ЗАНЯТИЕ 10. Реализация наследования

296

Вызов методов базового класса
в производном классе
Обычно метод F i s h :: Swim() содержит обобщ енную реализацию, применимую ко
всем рыбам, включая тунцов и карпов. Если специализированные реализации мето­
дов Tuna:: Swim () и C a rp :: Swim () хотят использовать эту обобщенную реализацию
метода базового класса F i s h : : Swim( ) , они могут сделать это с помощью оператора
разрешения области видимости ( : : ) , как показано в следующем фрагменте:
class Carp: public Fish

{
public:
Carp(): Fish (true)

{}

void Swim()

{
cout « "Карп медленно плавает" « endl;
Fish::Swim(); // Использование оператора ::

};
Этот подход использован в листинге 10.5.
ЛИСТИНГ 10.5. Использование оператора : : для вызова методов
базового класса из методов производных классов и функции main ()_____
0:
1:
2:
3:
4:
5:
6:

#include
using namespace std;
class Fish
{
private:
bool isFreshWaterFish;

7:
8:
9:
10:

public:
// конструктор класса Fish
Fish(bool isFreshWater) : isFreshWaterFish(isFreshWater){}

11:
12:
void Swim()
13:
{
14:
if (isFreshWaterFish)
15:
cout « "Пресноводный" « endl;
16:
else
17:
cout « "Морской" « endl;
18:
}
19: };

20 :
21: class Tuna: public Fish

22:

{

Основы наследования
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:

public:
Tuna(): Fish(false)
void Swim()
{
cout «
}

|

297

{}

"Тунец плавает быстро" «

endl;

};
class Carp: public Fish
{
public:
Carp(): Fish(true) {}
void Swim()
{
cout « "Карп плавает медленно" «
Fish::Swim();
}

endl;

};
int main()
{
Carp myLunch;
Tuna myDinner;
cout «

"Моя еда:" «

endl;

cout « "Обед: ";
myLunch.Swim ();
cout « "Ужин: ";
myDinner.Fish::Swim();
return 0;
}

Результат
Моя еда:
Обед: Карп плавает медленно
Пресноводный
Ужин: Морской

Анализ
Метод Carp:: Swim () в строках 37-41 демонстрирует вызов функции F i s h :: Swim ()
базового класса с использованием оператора разрешения области видимости ( : :

298

|

ЗАНЯТИЕ 1 0 . Реализация наследования

В строке 55 показана возможность использования оператора разрешения области ви­
димости для вызова метода базового класса F i s h :: Swim () из функции main () с ис­
пользованием объекта производного класса, в данном случае — Tuna.

Производный класс, скрывающий
методы базового класса
Перекрытие может принять экстремальный характер, когда метод Tuna:: Swim ()
потенциально способен скрыть все перегруженные версии функции F i s h : : Swim(),
приводя к неудаче компиляции при использовании перегруженных функций, как по­
казано в листинге 10.6.
ЛИСТИНГ Ю .в. Сокрытие методом Tuna:: Swim ()
перегруженного метода F i s h : : Swim (bool)__________________________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:

#include
using namespace std;
class Fish
{
public:
void Swim()
{
cout
« "Рыба плавает... !"

9:

« endl;

}

10 :
11:

void Swim(bool isFreshWaterFish)

12 :

{

13:
14:
15:
16:
17:

if (isFreshWaterFish)
cout « "Пресноводный" « endl;
else
cout « "Морской" « endl;
}

18: };
19:
20: class Tuna: public Fish

21:

{

22:
public:
23:
void Swim()
24:
{
25:
cout
« "Тунец плаваетбыстро"
26:
}
27: };
28:
29: int main()
30: {
Tuna myDinner;
31
32

«

endl;

Основы наследования
33:
34:
35:

cout «

3 6:
37:
38:
39: }

"Моя еда:" «

|

299

endl;

// myDinner.Swim(false); // Ошибка компиляции: Fish::Swim(bool)
// скрыт методом Tuna::Swim()
myDinner.Swim();
return 0;

Результат
Моя еда:
Тунец плавает быстро

Анализ
Эта версия класса F i s h немного отличается от тех, которые вы видели до сих пор.
Кроме минимизации версии для объяснения текущей проблемы, данная версия класса
F i s h содержит два перегруженных метода S w im ( ): один не получает никаких параме­
тров (строки 6 -9 ), а другой получает параметр типа b o o l (строки 11-17). Поскольку
класс T u n a наследуется от класса F i s h открыто (строка 20), кажется, что обе версии
метода F i s h :: S w im () будут доступны через экземпляр класса T u n a . Однако в резуль­
тате того, что класс T u n a реализует собственную версию метода T u n a : : S w im () (стро­
ки 2 3-2 6 ), функция F i s h : : S w im ( b o o l ) оказывается скрытой от компилятора. Если
убрать комментарий из строки 35, будет получена ошибка времени компиляции.
Для того чтобы вызвать функцию F i s h : : S w im ( b o o l) через экземпляр класса T u n a ,
можно прибегнуть к следующим решениям.
■ Решение 1. Использовать оператор разрешения области видимости в функции
m a in ():

myDinner.Fish::Swim();

■ Решение 2. Использовать в классе T u n a ключевое слово u s i n g , чтобы показать
скрытые методы S w im ( ) в классе F i s h :
class Tuna: public Fish
{
public:
using Fish::Swim; // Раскрытие скрытых методов Swim()
// в базовом классе Fish
void Swim()
{
cout «

"Тунец плавает быстро" «

endl;

300

|

ЗАНЯТИЕ 10. Реализация наследования

■ Решение 3. Переопределить все перегруженные варианты метода Swim() в клас­
се Tuna (например, при необходимости вызывая метод F i s h : : S w i m ( . . . ) в
Tuna::Swi m(. . .
class Tuna: public Fish
{
public:
void Swim(bool isFreshWaterFish)

{
Fish::Swim(isFreshWaterFish);

)
void Swim()

{
cout «

"Тунец плавает быстро" «

endl;

}
};

Порядок конструирования
При создании объекта класса Tuna, производного от класса F ish , когда будет вы­
зван конструктор класса Tuna: до или после конструктора класса F ish ? Кроме того,
каков при конструировании экземпляра порядок создания таких его атрибутов, как
F i s h : : i s F r e s h W a t e r F i s h ? К счастью, последовательность инстанцирования строго
стандартизована. Объекты базового класса создаются до производного класса. Та­
ким образом, первой создается часть F i s h объекта класса Tuna, так, чтобы ее чле­
ны, в частности открытые и защищенные, были готовы к использованию, когда будет
создаваться Tuna. При инстанцировании классов F i s h и Tuna атрибуты, такие как
F i s h : : isF r e sh W a te r F ish , создаются до вызова конструктора F i s h : : F i s h (), гаран­
тируя существование атрибутов к моменту начала работы с ними конструктора. То же
самое относится и к конструктору Tuna: :Tuna ().

Порядок деструкции
Когда экземпляр класса Tuna выходит из области видимости, последовательность
деструкции противоположна последовательности конструкции. В листинге 10.7 при­
веден простой пример, демонстрирующий последовательность конструкции и д е­
струкции.
ЛИСТИНГ 10.7. Порядок конструкции и деструкции базового
класса, производного класса и его членов____________________________________________
0:
1:
2:
3:
4:

#include
using namespace std;
class FishDummyMember
{

Основы наследования
public:
FishDummyMember ()

5
б
7

{

8
9

cout «

"Конструктор FishDummyMember" «

endl;

}

10

11

-FishDummyMember ()

12

{

13
14
15
16
17
18
19

cout «

"Деструктор FishDummyMember" «

endl;

class Fish

{
protected:
FishDummyMember dummy;

20

21

22

public:
// Конструктор класса Fish
Fish()

23
24
25
26
27
28
29
30
31
32
33
34
35

class TunaDummyMember

36

{

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

{
cout «

"Конструктор Fish" «

endl;

}
-Fish()

{
cout «

"Деструктор Fish" «

endl;

};

public:
TunaDummyMember()

{
cout «

"Конструктор TunaDummyMember" «

endl;

}
-TunaDummyMember()

{
cout «

"Деструктор TunaDummyMember" «

}
};

class Tuna: public Fish

{
private:
TunaDummyMember dummy;

endl;

301

302

|

ЗАНЯТИЕ 10. Реализация наследования

54:
55:
public:
56:
Tuna()
57:
{
58:
cout «
59:
}
60:
-Tuna()
61:
{
62:
cout «
63:
}
64:
65: };

"Конструктор Tuna" «

"Деструктор Tuna" «

endl;

endl;

66:

67: int main ()

68: {
69:
70: }

Tuna myDinner;

Результат
Конструктор FishDummyMember
Конструктор Fish
Конструктор TunaDummyMember
Конструктор Tuna
Деструктор Tuna
Деструктор TunaDummyMember
Деструктор Fish
Деструктор FishDummyMember

Анализ
Функция m a in ( ) в строках 6 7 -7 0 поразительно мала по сравнению с объемом соз­
даваемого ею вывода. Все эти строки выводятся при создании экземпляра класса T un a ,
поскольку вывод в поток c o u t вставлен в конструкторы и деструкторы всех задей­
ствованных при этом объектов. Для демонстрации создания и удаления переменныхчленов определены два вымышленных класса, F ish D u m m y M e m b e r и TunaDum m yMem ber,
в конструкторах и деструкторах которых осуществляется вывод соответствующих
строк в c o u t . Классы F i s h и T u n a содержат члены, которые представляют собой эк­
земпляры этих вымышленных классов (строки 20 и 53). Вывод показывает, что соз­
дание объекта класса T u n a фактически начинается с вершины иерархии. Так, первой
создается часть базового класса F i s h в составе класса T u n a , при этом вначале созда­
ются его члены, такие как F i s h : : dummy. Далее, после создания таких атрибутов, как
dummy, выполняется конструктор класса F i s h . После создания экземпляра базового
класса продолжается создание экземпляра T u n a , которое начинается с создания эк­
земпляра T u n a : : dummy и завершается выполнением кода конструктора T u n a : : T u n a ().
Вывод также демонстрирует, что последовательность удаления прямо противополож­
на последовательности создания.

Закрытое наследование

303

Закрытое наследование
Закрытое наследование (private inheritance) отличается от открытого (рассматри­
вавшегося до сих пор) тем, что в строке объявления производного класса использует­
ся ключевое слово p r i v a t e :
class Base
{
// ... переменные-члены и методы базового класса

class Derived: private Base // закрытое наследование

{
// ... переменные-члены и методы производного класса

};
Закрытое наследование базового класса означает, что все открытые члены и атри­
буты базового класса являются закрытыми (т.е. недоступными) для всех, кроме экзем­
пляра производного класса. Другими словами, даже открытые члены и методы класса
B a s e могут быть использованы только классом D e r iv e d , но ни кем иным, владеющим
экземпляром класса D e r iv e d .
Это резко контрастирует с примерами класса T u n a и его базового класса F i s h , ко­
торые мы рассматривали начиная с листинга 10.1. Функция m a in () в листинге 10.1
может вызвать функцию F i s h :: S w im () у экземпляра класса T u n a , поскольку функция
F i s h : : S w im () является открытым методом, а класс T u n a является производным от
класса F i s h с использованием открытого наследования. Попробуйте заменить клю­
чевое слово p u b l i c ключевым словом p r i v a t e в строке 17, и вы получите сбой ком­
пиляции.
Таким образом, для мира вне иерархии наследования закрытое наследование, по
существу, не означает отношение является (is-a) (вообразите тунца, который не может
плавать!). Поскольку закрытое наследование позволяет использовать атрибуты и ме­
тоды базового класса только производным от него классам, мы получаем отношение
содержит (has-a). В окружающем мире есть множество примеров закрытого насле­
дования (табл. 10.2).

ТАБЛИЦА 10.2. Примеры закрытого наследования из повседневной жизни
Базовый класс

Примеры производных классов

M o to r

(М отор)

Саг

H e a rt

(Сердце)

M am m al (М лекоп итаю щ ее с о д е р ж и т сердце)

(А в том о б и л ь с о д е р ж и т м отор)

R e f i l l (Стержень)

Реп

(Ручка с о д е р ж и т стерж ень)

M oon

Sky

(Н ебо с о д е р ж и т Луну)

(Луна)

Давайте рассмотрим закрытое наследование на примере отношений автомобиля с
его мотором (листинг 10.8).

304

|

ЗАНЯТИЕ 10. Реализация наследования

ЛИСТИНГ 10.8. Класс Саг, связанный с классом Motor закрытым наследованием
0: #include
1: using namespace std;
2:
3: class Motor
4: {
5:
public:
6:
void Switchlgnition()
7:
8:
9:
10:

{

11 :

{

cout « "Зажигание включено" «
}
void PumpFuelO

endl;

12:
cout « "Топливо в цилиндрах" « endl;
13:
}
14:
void FireCylinders()
15:
{
16:
cout « "P-p-p-p-p-p-p-p..
« endl;
17:
}
18: };
19:
20: class Car:private Motor

21: {
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:

public:
void Move()
{
Switchlgnition() ;
PumpFuelO;
FireCylinders();
}
};
int main()
{
Car myDreamCar;
myDreamCar.Move();
return 0;
}

Результат
Зажигание включено
Топливо в цилиндрах

р - р - р -р - р -р - р -р .. .

Защищенное наследование

305

Анализ
Класс M o t o r , определенный в строках 3 -1 8 , очень прост: он содержит три от­
крытые функции-члена, включая зажигание ( S w i t c h l g n i t i o n ()), подачу топлива
( P u m p F u e l ()) и запуск ( F i r e C y l i n d e r s ()). Класс С а г наследует класс M o t o r с ис­
пользованием ключевого слова p r i v a t e (строка 20). Таким образом, открытая функ­
ция C a r : :M o v e () обращается к членам базового класса M o t o r . Если вы попытаетесь
вставить в функцию m a in () строку
myDreamCar.PumpFuel();

то получите при компиляции ошибку с сообщ ением error С2247: Motor:.PumpFuel
not accessible because ‘C a r ’ uses \private’ to inherit from ‘M o to r’ (ошибка C2247:
M o t o r : : P u m p F u e l недоступен, поскольку ’ C a r 1 использует ’ p r i v a t e ' при наследо­
вании от ' M o t o r ' ) .

ПРИМЕЧАНИЕ

Если от класса С а г будет наследован другой класс, например R a c e C a r ,
то, независимо от характера наследования, у класса R a c e C a r не будет до­
ступа к открытым членам и методам базового класса M o to r . Дело в том, что
отношения наследования между классами С а г и M o t o r имеют закрытый
характер, а значит, доступ для всех остальных, кроме класса С а г , будет за­
крытым (т.е. доступа не будет) - даже к открытым членам базового класса.
Другими словами, припринятии компилятором решения о том, должен ли
у некого класса быть доступ к открытым или защищенным членам базового
класса, доминирует наиболее ограничивающий модификатор доступа.

Защищенное наследование
Защищенное наследование отличается от открытого наличием ключевого слова
в строке объявления производного класса:

p ro te c te d

class Base
{
// ... переменные-члены и методы базового класса

};
class Derived: protected Base // Защищенное наследование

{
// ... переменные-члены и методы производного класса

};
Защищенное наследование подобно закрытому в следующем отношении.
■ Реализует отношение содержит (has-a).
■ Позволяет производному классу обращаться ко всем открытым и защищенным чле­
нам базового класса.
■ Вне иерархии наследования с помощью экземпляра производного класса нельзя
обратиться к открытым членам базового класса.

306

|

З А Н Я Т И Е 1 0 . Реал изац и я насл едования

Но защищенное наследование все же отличается от закрытого, когда речь идет о
следующем производном классе, унаследованном от данного производного класса:
class Derived?: protected Derived

{
// Имеет доступ к открытым и защищенным членам Base

};
Иерархия защищенного наследования позволяет подклассу производного класса
(т.е. классу D e r iv e d 2 ) обращаться к открытым и защищенным членам базового класса
(листинг 10.9). Это было бы невозможно, если бы при наследовании классом D e r iv e d
класса B a s e использовалось ключевое слово p r i v a t e .
Л И С Т И Н Г 1 0 .9 . R a c e C a r — п о д к л а с с к л а с с а С а г (н а сл е д н и к а M o t o r )
при з а щ и щ е н н о м н а с л е д о в а н и и ____________________________________________________________

0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
using namespace std;
class Motor
{
public:
void Switchlgnition()
{
cout « "Зажигание включено" «
}
void PumpFuelO

11:

endl;

{

12:
cout « "Топливо в цилиндрах" « endl;
13:
}
14:
void FireCylinders()
15:
{
16:
cout « "P-p-p-p-p-p-p-p..." « endl;
17:
}
18: };
19:
20: class Car protected Motor

21 :

{

22: public:
23:
void Move()
24:
{
25:
Switchlgnition();
26:
PumpFuelO;
27:
FireCylinders();
28:
}
29: );
30:

Защищенное наследование
31:
32:
33:
34:
35:
36
37
38
39
40
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:

class RaceCar:protected Car
{
public:
void Move()
{
Switchlgnition(); //
PumpFuelO;
//
FireCylinders(); //
FireCylinders(); //
FireCylinders();
}
};

|

307

RaceCar имеет доступ к членам класса
Motor благодаря защищенному
наследованию между RaceCar и Саг и
между Саг и Motor

int main()
{
RaceCar myDreamCar;
myDreamCar.Move();
return 0;
}

Результат
Зажигание включено
Топливо в цилиндрах
Р-р-р-р-р-р-р-р...
Р-р-р-р-р-р-р-р...
Р-р-р-р-р-р-р-р...

Анализ
Класс С а г защищенно наследует класс M o t o r (строка 20). Класс R a c e C a r защи­
щенно наследует класс С а г (строка 3 1 ). Как можно заметить, реализация метода
R a c e C a r : :M o v e () использует открытые методы, определенные в базовом классе M o­
t o r . Этот доступ к первому базовому классу M o t o r через промежуточный базовый
класс С а г обеспечивают отношения между классами С а г и M o t o r . Если бы это было
закрытое наследование, а не защищенное, то у производного класса не было бы д о­
ступа к открытым членам M o t o r , поскольку компилятор выбирает самый ограничива­
ющий из использованных модификаторов доступа. Обратите внимание, что характер
отношений между классами С а г и R a c e C a r не имеет значения при доступе к базовому
классу. Так, даже если в строке 31 заменить ключевое слово p r o t e c t e d словом p u b l i c
или p r i v a t e , исход компиляции этой программы остается неизменным.

308

ЗАНЯТИЕ 10. Реализация наследования

ВНИМАНИЕ!___

Используйте закрытое или защищенное наследование только по мере не­
обходимости. В большинстве случаев, когда используется закрытое насле­
дование (как у классов С а г и M o to r ) , базовый класс может также быть
атрибутом (членом) класса С а г , а не суперклассом. При наследовании от
класса M o t o r вы, по существу, ограничили свой класс С а г наличием толь­
ко одного мотора, без какого-либо существенного выигрыша от наличия
экземпляра класса M o t o r как закрытого члена.
Автомобили развиваются, и сейчас не редкость гибридные автомобили, на­
пример в дополнение к обычному мотору может применяться газовый или
электрический. Наша иерархия наследования для класса С а г оказалась
бы узким местом, попытайся мы последовать за такими разработками.

ПРИМЕЧАНИЕ

Наличие экземпляра класса M o t o r как закрытого члена, вместо насле­
дования от него, называется композицией (composition) или агрегацией
(aggregation). Такой класс С а г выглядел бы следующим образом:

class Саг
{
private:
Motor heartOfCar;
public:
void Move()
{
heartOfCar.Switchlgnition();
heartOfCar.PumpFuel();
heartOfCar.FireCylinders();
}
};
Такое решение может оказаться лучшим дизайном, поскольку позволяет
легко добавлять к существующему классу С а г больше моторов как атрибу­
тов, не изменяя его иерархию наследования или предоставляемые клиен­
там возможности.

Проблема срезки
Что будет, если программист сделает так?
Derived objectDerived;
Base objectBase = objectDerived;

Или вот так?
void FuncUseBase(Base input);
Derived objectDerived;
FuncUseBase(objectDerived); // objectDerived будет срезан при
// копировании во время вызова функции

Множественное наследование

309

В обоих случаях объект производного класса копируется в другой объект базово­
го класса, явно при присваивании или косвенно при передаче в качестве аргумента.
В таких случаях компилятор копирует из объекта o b j e c t D e r i v e d только часть, соот­
ветствующую классу B a s e , а не весь объект. При этом будет потеряна информация,
содержащаяся в членах-данных, относящихся к классу D e r iv e d . Это непредвиденное
и нежелательное сокращение части данных, делающих производный класс специали­
зацией базового, называется срезкой (slicing).
Во избежание срезки не передавайте параметры по значению. Передавайте
их как указатели на базовый класс или как (константную) ссылку на него.

Множественное наследование
Ранее в этом занятии упоминалось о том, что иногда может пригодиться множес­
твенное наследование (multiple inheritance), как в случае с утконосом. Утконос — час­
тично млекопитающее, частично птица, частично рептилия. Для таких случаев язык
C++ позволяет унаследовать класс от двух и более базовых классов:
class Производный: Модификатор_Доступа Базовый_класс_1,
Модификатор_Доступа Базовый_класс_2
// Члены класса

};
Схема класса для утконоса на рис. 10.3 выглядит совсем не так, как таковая для
классов T u n a и C a r p (см. рис. 10.2).

Р И С - 1 0 -3 . О тн ош ени я м е ж д у к л а с с о м P l a t y p u s и к л а с с а м и Mammal, R e p t i l e и B i r d

Таким образом, синтаксическое представление C++ класса P l a t y p u s будет сле­
дующим:

310

|

ЗАНЯТИЕ 10. Реализация наследования

class Platypus: public Mammal, public Reptile, public Bird

{
// ... члены класса Platypus

};
Класс P la ty p u s , демонстрирующий множественное наследование, представлен в
листинге 10.10.
ЛИСТИНГ 10.10. Использование множественного наследования для
моделирования утконоса, являющегося млекопитающим, птицей и рептилией__________
0: #include
1: using namespace std;

2:
3: class Mammal
4: {
5: public:
6:
void FeedBabyMilk()
7:
{
8:
cout « "Млекопитающее: люблю молоко!" «
9:
}

10 :

endl;

};

11:
12:
13:
14:
15:
16:
17:
18:
19:

class Reptile
{
public:
void SpitVenomO
{
cout « "Рептилия: плюну ядом!" «
}
};

endl;

20:
21: class Bird

22:

{

23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:

public:
void LayEggs()
{
cout « "Птица: яйца отложены!" «
}
};

endl;

class Platypus: public Mammal, public Bird, public Reptile
{
public:
void Swim()
{
cout « "Утконос: я умею плавать!" « endl;
}
};

Запрет наследования с помощью ключевого слова final

311

39: int main()
40: {
41:
Platypus realFreak;
42:
realFreak.LayEggs();
43:
realFreak.FeedBabyMilk();
44:
realFreak.SpitVenom();
45:
realFreak.Swim();
46:
47:
return 0;
48: }

Результат
Птица: яйца отложены!
Млекопитающее: люблю молоко!
Рептилия: плюну ядом!
Утконос: я умею плавать!

Анализ
Определение свойств класса P l a t y p u s весьма компактно (строки 3 0 -3 7 ). По су­
ществу, класс просто наследует их от трех других: M am m al, R e p t i l e и B i r d . Функция
m a in () в строках 41-44 способна обратиться к трем индивидуальным возможностям
базовых классов, используя объект r e a l F r e a k производного класса P l a t y p u s . Кро­
ме вызова функций, унаследованных от классов M am m al, B i r d и R e p t i l e , функция
m a in () в строке 45 вызывает метод P l a t y p u s :: S w im (). Эта программа демонстриру­
ет синтаксис множественного наследования, а также то, что производный класс пре­
доставляет возможность доступа к открытым членам (в данном случае — к методам)
своих базовых классов.

Запрет наследования с помощью
ключевого слова final
Начиная с C++11 компиляторы поддерживают спецификатор f i n a l . Он использу­
ется для указания того, что класс объявлен как последний в иерархии наследования
и не может использоваться в качестве базового класса. Например, в листинге 10.10
класс утконоса можно объявить как f i n a l , тем самым блокируя возможность его на­
следования. Версия класса утконоса из листинга 10.10, объявленная как f i n a l , будет
выглядеть следующим образом:
class Platypus final: public Mammal, public Bird, public Reptile {
public:
void Swim() {
cout « "Утконос: я умею плавать!" « endl;

312

|

ЗАНЯТИЕ 10.

Реал изац и я наследования

В дополнение к классам ключевое слово f i n a l может использоваться с функциямичленами для управления полиморфным поведением. Этот вопрос рассматривается на
занятии 11, “Полиморфизм”.

ПРИМЕЧАНИЕ

Утконос может плавать, но он - не рыба. Следовательно, в листинге 10.10
мы не стали наследовать P l a t y p u s заодно и от класса F i s h , чтобы прос­
то воспользоваться существующим методом F i s h : : S w im ( ) . Принимая
решения о дизайне, не забывайте, что открытое наследование должно так­
же отражать отношение является и не должно использоваться без достаточ­
ных оснований, просто для решения текущих задач, связанных с повторным
использованием кода. Эти цели могут быть достигнуты и по-другому.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Создавайте открытую иерархию наследования
для установки отношений является.

лишь для повторного использования тривиаль­

Создавайте закрытую

ных функций.

или защищенную иерар­

Не создавайте иерархию

Не используйте закрытое

наследования только

и открытое наследо­

хию наследования для установки отношений со­
держит.

вание без разбора, поскольку впоследствии это

Помните: открытое

может стать узким местом архитектуры вашего

наследование означает, что

у классов, производных от производного класса,

приложения для будущего масштабирования.

есть доступ к открытым и защищенным членам

Не создавайте

базового класса. Объект производного класса

(с тем же именем, но другим набором входных

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

параметров), которые непреднамеренно скры­

Помните:

закрытое наследование означает,

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

Помните: защищенное

наследование означает,

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

Помните:

независимо от характера наслед­

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

функции производного класса

вают таковые в базовом классе.

Резюме

|

313

Резюме
На сегодняшнем занятии рассматривались основы наследования в языке C++.
Вы узнали, что открытое наследование — это отношение является между производ­
ным и базовым классами, а закрытое и защищенное наследование создает отношение
имеет. Вы видели, что применение модификатора доступа p r o t e c t e d предоставляет
доступ к членам базового класса только для производного класса, оставляя их скры­
тыми от классов вне иерархии наследования. Вы узнали, что защищенное наследова­
ние отличается от закрытого тем, что производные классы производного класса могут
обращаться к открытым и защищенным членам базового класса, что невозможно при
закрытом наследовании. Вы изучили основы перекрытия методов и их сокрытия, а
также узнали, как избежать нежелательного сокрытия метода с помощью ключевого
слова u s in g .
Теперь вы готовы ответить на несколько вопросов, а затем перейти к изучению сле­
дующего столпа объектно-ориентированного программирования — полиморфизму.

Вопросы и ответы
■ Меня попросили смоделировать класс Mammal наряду с классами еще не­
скольких млекопитающих: Human, Lion и Whale. Должен ли я использовать
иерархию наследования, и если должен, то какую?
Человек, лев и кит — все млекопитающие и, по существу, поддерживают отноше­
ние является. Используйте открытое наследование, в котором класс M aim nal будет
базовым, а классы Human, L i o n и W h a le — производными от него.

■ В чем разница между терминами производный класс и подкласс ?
По сути, никакой разницы нет. Оба термина подразумевают класс, который порож­
ден от базового класса, т.е. специализирует его.

■ Производный класс использует открытое наследование в отношении своего ба­
зового класса. Может ли он обратиться к закрытым членам базового класса?
Нет. Компилятор всегда гарантирует, что самые ограничивающие из использован­
ных модификаторов доступа останутся в силе. Независимо от характера наследо­
вания закрытые члены класса никогда не предоставляются (т.е. недоступны) вне
класса. Исключение — классы и функции, которые были объявлены дружествен­
ными ( f r i e n d ) .

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.

ЗАНЯТИЕ 10. Реализация наследования

314

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

Контрольные вопросы
1. Я хочу, чтобы некоторые члены базового класса были доступны для произво­
дного класса, но не вне иерархии классов. Какой модификатор доступа мне ис­
пользовать?
2. Что будет, если я передам объект производного класса по значению функции,
ожидающей в качестве параметра базовый класс?
3. Что лучше — закрытое наследование или композиция?
4.

Чем ключевое слово u s i n g может помочь мне в иерархии наследования?

5. Класс D e r i v e d закрыто наследуется от класса B a s e . Другой класс, S u b D e r iv e d ,
открыто наследуется от класса D e r iv e d . Может ли класс S u b D e r iv e d обратить­
ся к открытым членам класса B a s e ?

Упражнения
1. В каком порядке вызываются конструкторы для класса P l a t y p u s из листин­
га 10.10?
2. Как классы P o ly g o n (Многоугольник), T r i a n g l e (Треугольник) и S h a p e (Фор­
ма) связаны один с другим?
3. Класс D2 является производным от класса D1, который является производным
от класса B a s e . Какой модификатор доступа следует использовать и где его рас­
положить, чтобы запретить классу D2 обращаться к открытым членам класса
Base?
4.

Каков характер наследования в этом фрагменте кода?
class Derived: Base
{
// ... члены класса Derived

};
5. Отладка. Что неправильно в этом коде:
class Derived: public Base
{
// ... члены класса Derived

};
void SomeFunc(Base value)
{

П ...
)

ЗАНЯТИЕ 11

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

■ Что означает термин полиморфизм


Что делают виртуальные функции и как их использовать

■ Что такое абстрактные классы и как их объявлять
■ Что такое виртуальное наследование и где оно необходимо

316

З А Н Я Т И Е 1 1 . П олим орф изм

Основы полиморфизма
“Поли” в переводе с греческого языка означает много , а “морф” — форма. По­
лиморфизм (polymorphism) — это возможность объектно-ориентированных языков,
позволяющая аналогичными способами обрабатывать объекты разных типов. Данное
занятие посвящено полиморфному поведению, которое может быть реализовано на
языке C++ с использованием иерархии наследования, известной также как полимор­
физм подтипов (subtype polymorphism).

Потребность в полиморфном поведении
На занятии 10, “Реализация наследования”, вы видели, как классы T u n a и C a r p
наследовали открытый метод S w im ( ) класса F i s h (см. листинг 10.1). Однако классы
T u n a и C a r p могут предоставить собственные методы T u n a :: S w im () и C a r p :: Sw im ( ) ,
чтобы тунец и карп плавали по-разному. Но поскольку каждый из них является также
рыбой, пользователь экземпляра класса T u n a вполне может использовать тип базово­
го класса для вызова метода F i s h : : S w im ( ), который выполнит только общую часть
F i s h : : S w im (), а не полную T u n a : : S w im ( ), даже при том что этот экземпляр базового
класса F i s h является частью класса T u n a . Эта проблема представлена в листинге 11.1.
Во всех примерах кода на этом занятии удалено все, что не является абсо­
лютно необходимым для объяснения рассматриваемой темы, а количество
строк кода сведено к минимуму, чтобы улучшить удобочитаемость.
При реальном программировании необходимо создавать классы правиль­
но, а также разрабатывать осмысленные иерархии наследования, учиты­
вающие в перспективе цели проекта и приложения.

Л И С Т И Н Г 1 1 .1 . В ы з о в м е то д о в с п о м о щ ь ю э к з е м п л я р а
б а з о в о г о к л а с с а F i s h , п р и н а д л е ж а щ е го к л а с с у T u n a

0: linclude
1: using namespace std;

2:
3: class Fish
4: {
5:
public:
6:
void Swim()
7:
{
8:
cout « "Рыба плавает!" «
9:
}

10 :
11 :

};

12: class Tuna:public Fish
13: {
14:
public:

endl;

Основы полиморфизма
15:
16:
17:
18:
19:

20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:

//Перекрытие Fish::Swim
void Swim()
{
cout « "Тунец плавает!" «
}

317

endl;

};

void MakeFishSwim(Fish& InputFish)
{
// Вызов Fish::Swim
InputFish.Swim();
}
int main()
{
Tuna myDinner;
// Вызов Tuna::Swim
myDinner.Swim();
// Передача Tuna как Fish
MakeFishSwim(myDinner);
return 0;
}

Результат
Тунец плавает!
Рыба плавает!

Анализ
Класс Tuna специализирует класс F ish через открытое наследование, как показано
в строке 12. Он также перекрывает метод F i s h :: Swim (). Функция m ain () вызывает
метод T una :: Swim () в строке 33 непосредственно и передает объект myDinner (клас­
са Tuna) как параметр в функцию MakeFishSwim (), которая интерпретирует его как
ссылку Fish&, как видно из ее объявления (строка 22). Другими словами, вызов функ­
ции MakeFishSwim (Fish&) не заботит, что был передан объект класса Tuna; он обра­
батывает его как объект класса F ish и вызывает метод F i s h : : Swim(). Вторая строка
вывода означает, что тот же объект класса Tuna в этот раз привел к выводу, как у клас­
са F ish , без всякой специализации (с тем же успехом это мог быть класс Carp).
Однако в идеале пользователь мог бы ожидать, что объект класса Tuna поведет
себя, как тунец, даже если вызван метод F is h : :S w im () . Другими словами, когда в
строке 25 вызывается метод I n p u tF is h . Swim (), пользователь ожидает, что будет вы­
полнен метод T una :: Swim(). Такое полиморфное поведение, когда объект известного
типа — F is h — может вести себя, как объект фактического типа, а именно — как

318

|

ЗАНЯТИЕ 11. Полиморфизм

объект производного класса T u n a , может быть реализован, если сделать функцию
F i s h : : S w i m () виртуальной.

Полиморфное поведение, реализованное
с помощью виртуальных функций
Д оступ к объекту класса F i s h возможен через указатель F i s h * или по ссылке
Объект класса F i s h может быть создан отдельно или как часть объекта класса
T u n a или C a r p , производного от класса F i s h . Вы не знаете, как именно (да это и не­
важно). Вы вызываете метод S w im (), используя этот указатель или ссылку:
F is h & .

pFish->Swim();
myFish.Swim();

Вы ожидаете, что объект класса F i s h будет плавать, как тунец, если это часть
объекта класса T u n a , или как карп, если это часть объекта класса C a r p , или как безы­
мянная рыба, если объект класса F i s h был создан не как часть специализированного
класса, такого как T u n a или C a r p . Вы можете обеспечить такое поведение, объявив
функцию S w im ( ) в базовом классе F i s h как виртуальную функцию (virtual function):
class Базовый

{
virtual Возвратаемый_Тип Функция(Список_параметров );

};
class Derived
{

Возврата емый_ Тип Функция (Список_параметров) ;
};
Использование ключевого слова v i r t u a l означает, что компилятор обеспечивает
вызов любого перекрытого варианта запрошенного метода базового класса. Таким об­
разом, если метод S w im () объявлен как v i r t u a l , вызов m y F i s h . S w im () ( m y F is h имеет
тип F is h & ) приводит к вызову метода T u n a : : S w im ( ) , как показано в листинге 11.2.
Л И С Т И Н Г 1 1 .2 , Р е зу л ьта т о б ъ я в л е н и я м е то д а F i s h : : S w im () в и р ту а л ь н ы м ______________

0: #include
1: using namespace std;
2:
3: class Fish
4:

{

5:
6:

public:
virtual void Swim()

7:

{

8:

cout «

9:

10
11

}

};

"Рыба плавает!” «

endl;

Основы полиморфизма
12: class Tuna:public Fish
13: {
14:
public:
15:
// Перекрытие Fish::Swim
16:
void Swim()
17:
{
18:
cout « "Тунец плавает!" «
19:
}
2 0 : };

319

endl;

21:
22: class Carp:public Fish
23: {
24:
public:
25:
// Перекрытие Fish::Swim
26:
void Swim()

27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:

{
cout «

"Карп плавает!" «

endl;

}
};
void MakeFishSwim(Fish& InputFish)
{
// Вызов виртуального метода Swim()
InputFish.Swim();
}
int main()
{
Tuna myDinner;
Carp myLunch;
// Передача в качестве Fish
MakeFishSwim(myDinner);

объекта Tuna

// Передача в качестве Fish
MakeFishSwim(myLunch);

объекта Carp

return 0;
}

Результат
Тунец плавает!
Карп плавает!

Анализ
Реализация функции MakeFishSwim (Fish&) никак не изменилась по сравнению с
листингом 11Л , но вывод получился совсем иной. Метод F is h :: Swim () не был вызван

320

|

ЗАНЯТИЕ 11. Полиморфизм

вообще из-за присутствия перекрытых версий метода T u n a : : S w im () и C a r p : : Sw im (),
которые получили преимущество при вызове над методом F i s h : : S w im ( ) , посколь­
ку последний был объявлен как виртуальная функция. Это очень важный момент!
Он свидетельствует о том, что, даже не зная точный тип обрабатываемого объекта,
класс которого происходит от класса F i s h , реализация метода M a k e F is h S w im f ) спо­
собна привести к вызову разных реализаций метода S w im (), определенного в различ­
ных производных классах.
Это и есть полиморфизм: обработка различных рыб как общего типа F i s h при
гарантии выполнения правильной реализации метода S w im (), предоставляемого про­
изводными типами.

Необходимость виртуальных деструкторов
У средств, представленных в листинге 11.1, есть и оборотная сторона: непреднаме­
ренный вызов функций базового класса из экземпляра производного, когда доступна
специализация. Что будет, если функция применит оператор d e l e t e , используя указа­
тель типа B a s e * , который фактически указывает на экземпляр производного класса?
Какой деструктор будет вызван? Рассмотрим листинг 11.3.
Л И С Т И Н Г 1 1 .3 . Ф у н к ц и я , в ы з ы в а ю щ а я о п е р а т о р d e l e t e для ти п а B a s e * ________________

0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
using namespace std;
class Fish
{
public:
Fish()
{
cout «
}
-Fish()

11 :

"Создаем Fish" «

endl;

{

12:
cout « "Уничтожаем Fish" «
13:
}
14: };
15:
16: class Tuna:public Fish
17: {
18:
public:
19:
Tuna()
20:

21:
22:
23:
24:
25:
26:
27: };
28:

endl;

{

cout «

"Создаем Tuna" «

endl;

}
-Tuna()
{
cout «

}

"Уничтожаем Tuna" «

endl;

Основы полиморфизма
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:

321

void DeleteFishMemory(Fish* pFish)
{
delete pFish;
}
int main()
{
cout « "Выделение динамической памяти для Tuna:" «
Tuna* pTuna = new Tuna;
cout « "Удаление Tuna: " «
endl;
DeleteFishMemory(pTuna);

endl;

cout « "Инстанцирование Tuna в стеке:" « endl;
Tuna myDinner;
cout « "Выход из областивидимости: " « endl;
return 0;
}

Результат
Выделение динамической памяти для Tuna:
Создаем Fish
Создаем Tuna
Удаление Tuna:
Уничтожаем Fish
Инстанцирование Tuna в стеке:
Создаем Fish
Создаем Tuna
Выход из области видимости:
Уничтожаем Tuna
Уничтожаем Fish

Анализ
Функция m a in () создает экземпляр класса T u n a в динамической памяти, исполь­
зуя оператор n ew в строке 37, а затем сразу освобож дает выделенную память, ис­
пользуя вспомогательную функцию D e le t e F is h M e m o r y () в строке 39. Для сравнения
другой экземпляр класса T u n a создается в стеке как локальная переменная m y D in n e r
(строка 42) и выходит из области видимости по завершении функции m a in (). Вы­
вод генерируется в конструкторах и деструкторах классов F i s h и T u n a . Обратите
внимание: несмотря на то, что обе части объекта — и часть T u n a , и часть F i s h —
были созданы в динамической памяти, поскольку использовался оператор new , при
удалении был вызван только деструктор класса F i s h , но не класса T u n a . Это сильно
отличается от создания и удаления локального объекта m y D in n e r , когда вызываются
все конструкторы и деструкторы. В листинге 10.7 был представлен правильный по­
рядок создания и удаления классов в иерархии наследования, демонстрирующий, что
должны быть вызваны все деструкторы, включая деструктор - T u n a ( ) . Здесь явно
что-то неправильно.

322

|

ЗАНЯТИЕ 11. Полиморфизм

Дело в том, что код деструктора производного класса, объект которого был создан
в динамической памяти с помощью оператора new, не будет вызван при применении
оператора d e l e t e к указателю типа B ase*. В результате ресурсы не будут освобожде­
ны, произойдет утечка памяти и другие ненужные неприятности.
Чтобы избежать этой проблемы, следует использовать виртуальные деструкторы,
как показано в листинге 11.4.
ЛИСТИНГ 11.4. Использование виртуальных деструкторов для гарантии вызова
деструкторов производных классов при удалении указателя базового типа_____________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
using namespace std;
class Fish
{
public:
Fish()
{
cout « "Создаем Fish" « endl;
}
virtual -Fish() // Виртуальный деструктор!

И:

{

12:
cout « "Уничтожаем Fish" «
13:
}
14: };
15:
16: class Tuna:public Fish
17: {
18:
public:
19:
Tuna()

20 :
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:

endl;

{

cout «

"Создаем Tuna" «

endl;

}

-Tuna()
{
cout «
}

"Уничтожаем Tuna" «

endl;

};
void DeleteFishMemory(Fish* pFish)
{
delete pFish;
}
int main()
{
cout « "Выделение динамической памяти для Tuna:" «
Tuna* pTuna = newTuna;

endl;

Основы полиморфизма
38:
39:
40:
41:
42:
43:
44:
45:
46: }

cout « "Удаление Tuna: " «
DeleteFishMemory(pTuna);

323

endl;

cout « "Инстанцирование Tuna в стеке:" « endl;
Tuna myDinner;
cout « "Выход из области видимости: " « endl;
return 0;

Результат
Выделение динамической памяти для Tuna:
Создаем Fish
Создаем Tuna
Удаление Tuna:
Уничтожаем Tuna
Уничтожаем Fish
Инстанцирование Tuna в стеке:
Создаем Fish
Создаем Tuna
Выход из области видимости:
Уничтожаем Tuna
Уничтожаем Fish

Анализ
Единственное различие между листингами 11.4 и 11.3 — добавление ключевого
слова v i r t u a l в строке 10, где был объявлен деструктор базового класса F i s h . О б­
ратите внимание, что это маленькое изменение, по существу, заставило компилятор
выполнить деструктор T u n a : : ~ T un a ( ) в дополнение к деструктору F i s h : : - F i s h ()
при вызове оператора d e l e t e для указателя F i s h * (который фактически указывает на
объект класса T u n a ) в строке 31. Вывод данного кода демонстрирует, что последова­
тельность вызовов конструкторов и деструкторов одинакова независимо от того, соз­
дан ли объект класса T u n a в динамической памяти с использованием оператора new ,
как показано в строке 37, или в стеке, как локальная переменная (строка 42).

ПРИМЕЧАНИЕ

Всегда объявляйте деструктор базового класса как v i r t u a l :

class Base
{
public:
virtual -Base()

{}; // Виртуальный деструктор

};
Это гарантирует, что будет невозможно вызвать оператор d e l e t e для ука­
зателя B a s e * так, чтобы не были корректно уничтожены объекты произво­
дных классов.

324

|

ЗАНЯТИЕ 11. Полиморфизм

Как работают виртуальные функции.
Понятие таблицы виртуальных функций
ПРИМЕЧАНИЕ

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

Функция M a k e F is h S w im ( F is h & ) в листинге 11.2 заканчивается вызовом метода
() или T u n a :: S w im () несмотря на то, что программист вызвал в ней ме­
тод F i s h : : Sw im ( ) , Безусловно, на момент компиляции компилятору ничего не извест­
но о характере объектов, с которыми встретится такая функция, и он не в состоянии
гарантировать выполнение различных методов S w im ( ) в различные моменты време­
ни. Очевидно, решение о том, какой метод S w im () следует вызвать, принимается во
время выполнения с использованием скрытой логики, реализующей полиморфизм и
предоставляемой компилятором во время компиляции.
Рассмотрим класс Base, в котором объявлено N виртуальных функций:
C a r p :: S w im

class Base
{
public:
virtual void Funcl()

{
// Реализация F u n d

}
virtual void Func2()

{
// Реализация Func2

}
// ... и так далее
virtual void FuncNO
{
// Реализация FuncN

}
};
Класс D e r i v e d , производный от класса B a s e , наследует метод B a s e : : F u n c 2 (),
предоставляя другие виртуальные функции непосредственно из класса B a s e :
class Derived: public Base
{
public:
virtual void Funcl()

{
// F u n d переопределяет Base::Fund

}
// Реализации для Func2 нет

О сновы полим орф изм а

|

325

virtual void FuncN()

{
// Реализация FuncN

1
Компилятор видит иерархию наследования и понимает, что класс B a s e определя­
ет некоторые виртуальные функции, которые перекрыты в классе D e r iv e d . Теперь
компилятор должен составить таблицу, называемую таблицей виртуальных функций
(Virtual Function Table — VFT), для каждого класса, который реализует виртуальную
функцию, или производного класса, который перекрывает ее. Другими словами, клас­
сы B a s e и D e r iv e d получают экземпляр собственной таблицы виртуальных функций.
При создании объектов этих классов инициализируется скрытый указатель (назовем
его VFT*) на соответствующую таблицу VFT. Таблицу виртуальных функций мож­
но представить как статический массив, содержащий указатели на функции, каж­
дый из которых указывает на виртуальную функцию (или ее перекрытую версию)
(рис. 11.1).

V F T для Base

V FT для Derived

Р И С - 1 1 -1 . П р е д ста в л е н и е та б л и ц ы в и р ту а л ьн ы х ф ун кц и й для к л а с с о в D e r i v e d и B a s e

Таким образом, все таблицы состоят из указателей на функции, каждый из которых
указывает на доступную реализацию виртуальной функции. В случае класса D e r iv e d
все, кроме одного указателя на функцию в его таблице VFT, указывают на локальные
реализации виртуального метода в классе D e r iv e d . Класс D e r iv e d не переопределяет
метод B a s e : : F u n c 2 (), а потому соответствующий указатель на функцию указывает
на реализацию в классе B a s e .

326

|

ЗАНЯТИЕ 11. Полиморфизм

Это означает, что при вызове пользователем класса D e r iv e d
CDerived objDerived;
obj Derived.Func2();

компилятор осуществляет поиск в таблице VFT класса D e r i v e d и обеспечивает
вызов реализации B a s e : : F u n c 2 (). Аналогично выполняется вызов переопределен­
ных виртуальных методов:
void DoSomething(Base& objBase)

{
objBase.F u n d (); // Вызов Derived::F u n d

}
int main()

{
Derived objDerived;
DoSomething(objDerived);

};
В данном случае, несмотря на то что объект o b j D e r i v e d интерпретируется из-за
параметра o b j B a s e как экземпляр класса B a s e , указатель VFT в нем все равно ука­
зывает на таблицу виртуальных функций класса D e r iv e d . Таким образом, функци­
ей F u n d ( ) , выполняемой через этот указатель VFT, является, конечно же, функция
D e r iv e d : : F u n d ( ) .

Вот таким образом таблицы виртуальных функций и обеспечивают реализацию
полиморфизма в C++.
Листинг 11.5 доказывает существование скрытого указателя VFT, сравнивая раз­
меры двух идентичных классов, у одного из которых есть виртуальная функция, а у
другого — нет.

ЛИСТИНГ 11.5. Демонстрация наличия скрытого указателя
VFT с помощью сравнения размеров двух классов
0: #include
1: using namespace std;
2:
3: class SimpleClass
4: {
5:
int a, b;
6:
7:
public:
8:void FuncDoSomething() {}
9: };
10:
11: class Base

12 :
13:
14:
15:

{

int a, b;
public:

Основы полиморфизма
16:
17: };
18:
19: int
20: {
21:
22:
23:
24:
25: }

| 327

virtual void FuncDoSomething() {}

main()
cout «
cout «

"sizeof(SimpleClass) = " « sizeof(SimpleClass) «
"sizeof(Base) = " « sizeof(Base) « endl;

endl;

return 0;

Результат в случае 32-разрядного компилятора
sizeof(SimpleClass) = 8
sizeof(Base) = 12

Результат в случае 64-разрядного компилятора
sizeof(SimpleClass) = 8
sizeof(Base) = 16

Анализ
Этот пример ограничен до минимума. Вы видите два класса, S i m p l e C l a s s и B a s e ,
которые идентичны по типам и количеству членов, но функция F u n c D o S o m e t h in g ()
в классе B a s e объявлена как виртуальная, а в классе S i m p l e C l a s s как не виртуаль­
ная. Различие лишь в добавлении ключевого слова v i r t u a l , но компилятор создает
таблицу виртуальных функций для класса B a s e и резервирует место для указателя на
нее в том же классе B a s e в качестве скрытого члена. Этот указатель использует 4 д о ­
полнительных байта в 32-разрядной системе автора, что и является доказательством
его существования.

ПРИМЕЧАНИЕ

Язык C++ позволяет запросить указатель B a s e * , имеет ли он тип D e­
r i v e d * , с помощью оператора приведения d y n a m ic _ c a s t и последую­
щего условного выполнения на основе результата этого запроса.
Этот механизм называется идентификацией типа времени выполнения (Run
Time Type Identification - RTTI), и в идеале его следует избегать, несмотря
на поддержку большинством компиляторов C++. Дело в том, что необходи­
мость выяснять тип объекта производного класса по указателю на базовый
класс обычно является плохой практикой программирования и свидетель­
ствует о плохом проектировании.
Более подробно RTTI и оператор d y n a m ic _ c a s t обсуждаются на заня­
тии 13, “Операторы приведения”.

328

|

ЗАНЯТИЕ 11. Полиморфизм

Абстрактные классы и чисто виртуальные функции
Базовый класс, который не может быть инстанцирован (т.е. не может быть создан
экземпляр этого класса), называется абстрактным классом (abstract base class). Цель
существования такого базового класса только одна — получение производных клас­
сов. Язык C++ позволяет создать абстрактный класс, используя чисто виртуальные
функции.
Функцию называют чисто виртуальной (pure virtual), если ее объявление имеет
следующий вид:
class Абстрактный_Базовый

{
public:
virtual void Некая_функция () = 0 ;

// Чисто виртуальная функция

};
Это объявление, по существу, говорит компилятору о том, что функция Некая_
функция () должна быть реализована классом, производным от класса Абстрактный_
Базовый:
class Производный : public Абстрактный_Базовый

{
public:
void Некая_функция{) // Реализация функции

{
cout «

"Реализация виртуальной функции" «

endl;

}
Таким образом, класс Абстрактный_Базовый выполнил свою задачу — заставил
класс Производный предоставить реализацию виртуального метода Некая_функция( ) .
Вернемся к классу F i s h . Предположим, что тунец не может плавать быстро, посколь­
ку класс T u n a не переопределил метод F i s h : : S w im (). Это ошибка реализации и боль­
шой недостаток. Сделав класс F i s h абстрактным базовым классом с чисто виртуаль­
ной функцией S w im ( ) , мы гарантируем, что класс T u n a , производный от класса F i s h ,
реализует метод T u n a : : S w im ( ) , т.е. тунец будет плавать, как тунец, а не как любая
рыба. Рассмотрим листинг 11.6.
Л И С Т И Н Г 1 1 ,6 . К л а с с F i s h к а к а б с т р а к т н ы й б а з о в ы й к л а с с для к л а с с о в T u n a и C a r p

0:
1:
2:
3:
4:
5:
6:
7:

#include
using namespace std;
class Fish
{
public:
// Определение чисто виртуальной функции Swim
virtual void Swim() = 0;

8 : };

Основы полиморфизма

|

329

9:
10: class Tuna:public Fish

И: {
12:

public:

13:
14:
15:

void Swim()
{
cout «

16:
17: };

}

"Тунец быстро плавает в море!" «

endl;

18: class Carp:public Fish
19:

{

20:
21:

public:
void Swim()

22:

{

23:

cout «

24:

"Карп медленно плавает в озере!" «

endl;

}

25: };
26:
27: void MakeFishSwim(Fish& inputFish)
28:
29:

{

30:

}

inputFish.Swim();

31:
32: int main()
33: {
34:

// Fish myFish;

35:

Carp myLunch;

36:
37:

Tuna myDinner;

//Нельзя инстанцировать абстрактный класс!

38:

MakeFishSwim (myLunch);

39:

MakeFishSwim(myDinner);

40:
41:
42:

return 0;
}

Результат
Карп медленно плавает в озере!
Тунец быстро плавает в море!

Анализ
Существенна первая (закомментированная) строка функции m a in () (строка 34).
Она демонстрирует, что компилятор не позволит создать экземпляр класса F i s h . Он
ожидает чего-то более конкретного, такого как специализация класса F i s h (клас­
са T u n a , например), что имеет смысл и в реальности. Благодаря чисто виртуальной
функции F i s h : : S w im ( ), объявленной в строке 7, оба класса, T u n a и C a r p , вынуждены
реализовать методы T u n a :: S w im () и C a r p :: S w im () соответственно. Строки 2 7-30, в

330

|

ЗАНЯТИЕ 11. Полиморфизм

которых реализован метод M a k e F i s h S w i m ( F i s h &), демонстрируют, что, хотя экзем­
пляр абстрактного класса и не может быть создан, ссылку или указатель на него впол­
не можно использовать. Таким образом, абстрактные классы — это очень хороший
способ потребовать от всех производных классов реализации определенных функций.
Если в классе T r o u t (форель), производном от класса F i s h , забыть реализовать метод
T r o u t :: S w im (), компиляция потерпит неудачу.

ДРИМЕ4АМИЕ

Для абстрактных базовых классов (Abstract Base Class) часто используется
аббревиатура АБК (АВС).

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

Использование виртуального
наследования для решения
проблемы ромба
На занятии 10, “Реализация наследования”, мы рассмотрели любопытный случай
утконоса, который является млекопитающим, но частично и птицей, и рептилией.
В этом случае класс утконоса P l a t y p u s должен происходить от классов Mam m al, B i r d
и R e p t i l e . Однако каждый из них, в свою очередь, происходит от более обобщенного
класса, A n im a l (животное), как показано на рис. 11.2.

ЛИСТИНГ 11.7. Проверка количества экземпляров базового
класса A n im a l в одном экземпляре класса P l a t y p u s _____________________________________
0:
1:
2:
3:
4:
5:
6:

linclude
using namespace std;
class Animal
{
public:
Animal ()

7:

{

8:
9:

)

cout «

"Конструктор Animal" «

10:
11:
12:
13:
14:
15:
16:
17:

// Простая переменная
int age;
};
class Mammal:public Animal
{
};

endl;

Использование виртуального наследования для решения проблемы ромба
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:

331

class Bird-.public Animal
{
};

class Reptile:public Animal
{
};
class Platypus:public Mammal, public Bird, public Reptile
{
public:
Platypus()
{
cout « "Конструктор Platypus" « endl;
}
};
int main()
{
Platypus duckBilledP;
// Раскомментировав следующую строку, получим сбой
// компиляции. Есть три экземпляра аде в базовых классах
// duckBilledP.аде = 25;
return 0;
}

Результат
Конструктор
Конструктор
Конструктор
Конструктор

Animal
Animal
Animal
Platypus

Анализ
Как показывает приведенный вывод, благодаря множественному наследованию
у всех трех базовых классов класса P l a t y p u s (производных, в свою очередь, от
класса A n im a l) есть свой экземпляр класса A n im a l. Следовательно, для каждого эк­
земпляра класса P l a t y p u s , как показано в строке 38, автоматически создаются три
экземпляра класса A n im a l. Это просто смеш но, поскольку утконос — это одно жи­
вотное, которое наследует определенные атрибуты классов M am m al, B i r d и R e p t i l e .
Но проблема с количеством экземпляров базового класса A n im a l не ограничивает­
ся только излишним использованием памяти. У класса A n im a l есть целочисленный
член A n i m a l : : a g e (который для демонстрации был оставлен открытым). При по­
пытке получить доступ к переменной-члену A n i m a l : : a g e через экземпляр класса

|

332

ЗАНЯТИЕ 11. П о л и м о р ф и з м

P l a t y p u s , как показано в строке 42, вы получаете ошибку компиляции, потому что
компилятор просто не знает, хотите ли вы установить значение переменной-члена
M a m m a l: : A n i m a l : : a g e , B i r d : : A n i m a l : : a g e или R e p t i l e : : A n i m a l : : a g e . Как ни
смешно, но при желании вы можете установить значения всех трех членов:

duckBilledP.Mammal: -.Animal::age = 25;
duckBilledP.Bird::Animal::age
= 26;
duckBilledP.Reptile::Animal::age = 27;

РИС. 11.2. С х е м а к л а с с а у тко н о са , д е м о н с т р и р у ю щ е го м н о ж е с т в е н н о е н а сл е д о в а н и е
Так что же произойдет при создании экземпляра класса P l a t y p u s ? Сколько экзем­
пляров класса A n im a l получится в одном экземпляре класса P l a t y p u s ? Листинг 11.7
поможет ответить на этот вопрос.
Безусловно, у одного утконоса должен быть только один возраст. Но все же класс
P l a t y p u s должен быть производным от классов M am m al, B i r d и R e p t i l e . Решение —
в виртуальном наследовании (virtual inheritance). Если вы ожидаете, что производный
класс будет использоваться в иерархии наследования в качестве базового, хорошей
идеей будет определение его отношения к базовому с использованием ключевого сло­
ва v i r t u a l :
class Derivedl: public virtual Base

{
// ... переменные и функции

И с п о л ь з о в а н и е в и р туа л ьн о го н а сл е д о в а н и я для р е ш е н и я п р о б л е м ы р о м б а

333

class Derived2: public virtual Base

{
// ... переменные и функции

};
Улучшенный класс P l a t y p u s (а фактически — улучшенные классы M am m al, B i r d
и R e p t i l e ) приведен в листинге 11.8.
ЛИСТИН Г 1 1 .8 . К а к к л ю ч е в о е с л о в о v i r t u a l в и е р а р хи и н а сл е д о в а н и я
о гр а н и ч и в а е т б а з о в ы й к л а с с о д н и м э к з е м п л я р о м _____________________ __________________ __

0: #include
1: using namespace std;

2:
3: class Animal
4: {
5:
public:
6:
Animal()
7:
{
8:
cout «
9:
}

"Конструктор Animal" «

endl;

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

// Простая переменная
int age;
};
class Mammal:public virtual Animal
{
};
class Bird:public virtual Animal
{

21:
22:

} ;

23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38

class Reptile:public virtual Animal
{
};
class Platypus final:public Mammal, public Bird, public Reptile
{
public:
Platypus()
{
cout «"Конструктор Platypus" « endl;
}
};
int main()
{
Platypus duckBilledP;

334
39:
40:
41:
42:
43:
44: }

ЗАНЯТИЕ 11. Полиморфизм

// OK, есть только один Animal::аде
duckBilledP.age = 25;
return 0;

Результат
Конструктор Animal
Конструктор Platypus

Анализ
Бегло сравнивая приведенный вывод с выводом листинга 11.7, можно сразу же
заметить, что количество экземпляров класса A n im a l уменьшилось до одного, что от­
ражает факт создания только одного утконоса. Все дело в ключевом слове v i r t u a l ,
использованном в отношениях между классами M am m al, B i r d и R e p t i l e и гаранти­
рующем существование только одного экземпляра общего базового класса A n im a l,
если они будут объединены классом P l a t y p u s . Это решает много проблем; одна из
них — строка 41, которая теперь компилируется, как представлено в листинге 11.7.
Обратите также внимание на ключевое слово f i n a l в строке 27, которое гарантирует,
что класс P l a t y p u s не может быть использован в качестве базового.

ПРИМЕЧАНИЕ

Проблема иерархии наследования, содержащей два или больше базовых
класса, которые происходят от одного общего базового класса, приводящая
при отсутствии виртуального наследования к необходимости разрешения
неоднозначности, называется проблемой ромба (diamond problem).
Название “ромб”, вероятно, возникло благодаря форме схемы классов
(см. рис. 11.2), в которой прямоугольники классов и связи между ними
создают ромбовидную фигуру.

ПРИМЕЧАНИЕ

Ключевое слово v i r t u a l в языке C++ нередко используется в различных
контекстах для разных целей. (На мой взгляд, кто-то просто хотел сэконо­
мить время на придумывании ключевых слов.) Вот краткое резюме.
Объявление функции виртуальной означает, что будет вызвана ее перекры­
тая версия из производного класса.
Отношения наследования, объявленные с использованием ключевого сло­
ва v i r t u a l , между классами D e r i v e d l и D e r iv e d 2 , производными
от класса B a s e , означают, что экземпляр следующего класса, D e r iv e d 3 ,
производного от классов D e r i v e d l и D e r iv e d 2 , будет содержать только
один экземпляр класса B a s e .
Таким образом, одно и то же ключевое слово v i r t u a l используется для
реализации двух разных концепций.

Ключевое слово override для указания преднамеренного перекрытия

|

335

Ключевое слово override для указания
преднамеренного перекрытия
Наши версии базового класса F is h содержат виртуальную функцию Sw im (), как
показано в следующем коде:
class Fish {
public:
virtual void Swim() {
cout « "Рыба плавает!" «

endl;

}
};
Предположим, что производный класс Tuna определил функцию Swim (), но с не­
много иной сигнатурой — программист случайно добавил ключевое слово c o n st:
class Tuna: public Fish {
public:
void Swim() const {
cout « "Тунец плавает!" «

endl;

Эта функция T u n a :: Swim () на самом деле не переопределяет функцию
F is h :: Swim (): у этих функций разные сигнатуры благодаря наличию ключевого сло­
ва c o n s t в определении T u n a :: Swim(). Компиляция, тем не менее, успешно выпол­
няется, и программист может ошибочно считать, что он успешно перекрыл функцию
Swim() в классе Tuna. Язык С + +11 в заботах о программистах дал им новый инстру­
мент — спецификатор o v e r r id e , который используется для проверки, была ли пере­
крытая функция объявлена как виртуальная в базовом классе:
class Tuna: public Fish {
public:
void Swim() const
override { // Ошибка: виртуальной функции с такой
// сигнатурой в классе Fish нет
cout « "Тунец плавает!" « endl;

Таким образом, спецификатор o v e r r id e предоставляет мощный способ выраже­
ния явного намерения перекрытия виртуальной функции базового класса, тем самым
позволяя компилятору выполнить проверки того, что
■ функция базового класса объявлена как v ir t u a l;
■ сигнатура виртуальной функции базового класса в точности соответствует сигнату­
ре функции производного класса, объявленной как o v e r r id e .

336

|

ЗАНЯТИЕ 11. Полиморфизм

Использование ключевого
слова final для предотвращения
перекрытия функции
Спецификатор f i n a l , введенный в С ++11, был представлен на занятии 10,
“Реализация наследования”. Класс, объявленный как f i n a l , не может использоваться
в качестве базового класса. Аналогично виртуальная функция, объявленная как f i ­
n a l , не может быть перекрыта в производном классе.
Таким образом, версия класса T u n a , которая не позволяет никакой дальнейшей
специализации виртуальной функции S w im ( ) , будет выглядеть следующим образом:
class Tuna: public Fish {
public:
// Перекрываем Fish::Swim и делаем ее final
void Swim() override final {
cout « "Тунец плавает!" « endl;

}
};
Эта версия класса T u n a может наследоваться, но функция S w im () при этом в
классах-наследниках не может быть перекрыта:
class BluefinTuna final: public Tuna {
public:
void Swim() { // Ошибка: Swim() в Tuna, объявлена
}
// как final и не может быть перекрыта

};
Применение спецификаторов o v e r r i d e и f i n a l показано в листинге 11.9.

ПРИМЕЧАНИЕ

Мы использовали f i n a l в объявлении класса B l u e f in T u n a . Это гаран­
тирует, что класс B l u e f i n T u n a не может использоваться в качестве базо­
вого класса. Таким образом, следующий код приведет к ошибке:

class FailedDerivation:public BluefinTuna
{

};

Виртуальные копирующие
конструкторы?
Обратите внимание на вопросительный знак в конце заголовка данного раздела.
Технически в языке C++ невозможно создать виртуальные копирующие конструкто­
ры. Но все же можно создать коллекцию (например, статический массив) типа B a s e * ,
каждый элемент которого является специализацией этого типа:

Виртуальны е коп ирую щ ие кон структоры ?

337

// Классы Tuna, Carp и Trout открыто наследуют класс Fish
Fish* pFishes[3];
Fishes[0] = new Tuna();
Fishes [1] = new CarpO;
Fishes[2] = new Trout();

Теперь присвоим его другому массиву того же типа, и виртуальный копирующий
конструктор обеспечит глубокое копирование объектов производного класса, а также
гарантирует, что объекты классов T u n a , C a r p и T r o u t будут скопированы именно как
объекты классов T u n a , C a r p и T r o u t , несмотря на то что будет использован копирую­
щий конструктор для типа F i s h * .
Но это все мечты.
Виртуальные копирующие конструкторы невозможны, поскольку ключевое сло­
во v i r t u a l в контексте методов базового класса, перекрываемых реализациями, д о ­
ступными в производном классе, свидетельствует о полиморфном поведении во время
выполнения. Конструкторы же по своей природе не полиморфны, так как способны
создавать экземпляр только фиксированного типа, а следовательно, язык C++ не по­
зволяет использовать виртуальные копирующие конструкторы.
С учетом сказанного выше появляется хороший повод определить собственную
функцию клонирования, которая позволит сделать то, что нам нужно:
class Fish

{
public:
virtual Fish* Clone() const = 0; // Чисто виртуальная функция

class Tuna:public Fish
{
// ... другие члены
public:
Tuna * Clone() const // Виртуальная функция клонирования

{
return new Tuna(*this); // Вернуть новый объект класса Tuna,
// являющийся копией данного

Таким образом, виртуальная функция C lo n e () моделирует виртуальный копирую­
щий конструктор, который должен быть вызван явно, как показано в листинге 11.9.
Л И С Т И Н Г 1 1 .9 . К л а с сы T u n a и C a r p с ф у н к ц и е й C lo n e (),
м од е л и р ую щ е й в и р туа л ьн ы й к о п и р у ю щ и й кон стр ук то р ____________________________________

0: #include
1: using namespace std;
2:
3: class Fish

4: {

338

ЗАНЯТИЕ 11. Полиморфизм

5:
public:
6:
virtual Fish* Clone() = 0;
7:
virtual void Swim() = 0;
8:
virtual ~Fish() {};
9: };

10:
11: class Tuna: public Fish

12 :
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:

{

public:
Fish* Clone() override
{
return new Tuna(*this);
}
void Swim() override final
{

cout «

"Тунец быстро плавает в море" «

endl;

}

};
class BluefinTuna final:public Tuna
{
public:
Fish* Clone() override
{
return new BluefinTuna(*this);
}
// Нельзя перекрыть final Tuna::Swim
};
class Carp final: public Fish
{
Fish* Clone() override
{
return new Carp(*this);
}
void Swim() override final
{
cout « "Карп медленно плавает в озере" «
}
};
int main()
{
const int ARRAY_SIZE = 4;
Fish* myFishes[ARRAY_SIZE] = {nullptr};
myFishes[0] = new Tuna();

endl

Виртуальные копирующие конструкторы?
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:

myFishes[l] = new CarpO;
myFishes[2] = new BluefinTuna();
myFishes[3] = new CarpO;

68:

{

|

339

Fish* myNewFishes[ARRAY_SIZE];
for(int index = 0; index < ARRAY_SIZE; ++index)
myNewFishes[index] = myFishes[index]->Clone();
// Вызов виртуального метода для проверки
forfint index = 0; index < ARRAY_SIZE; t+index)
myNewFishes[index]->Swim();
// Освобождение памяти
for(int index = 0; index < ARRAY_SIZE; t+index)

69:
70:
71:
72:
73:
74: }

delete myFishes[index];
delete myNewFishes[index];
}
return 0;

Результат
Тунец быстро плавает в море
Карп медленно плавает в озере
Тунец быстро плавает в море
Карп медленно плавает в озере

Анализ
Помимо демонстрации виртуальных копирующих конструкторов посредством
виртуальной функции F i s h : : C l o n e ( ) , листинг 11.9 демонстрирует использование
ключевых слов o v e r r i d e и f i n a l — для виртуальных функций и классов. Кроме
того, в строке 8 имеется виртуальный деструктор класса F i s h . Строки 5 2 -5 6 в функ­
ции m a in () демонстрируют объявление статического массива указателей на базовый
класс F i s h * и индивидуальное присваивание его элементам вновь созданных объек­
тов класса T u n a , C a r p , B l u e f i n T u n a и C a r p соответственно. Обратите внимание на то,
что этот массив m y F is h e s способен хранить объекты, казалось бы, разных типов, ко­
торые связаны общим базовым классом F i s h . Это уже замечательно — по сравнению
с предыдущими массивами в этой книге, которые по большей части имели простой
однообразный тип i n t . Если это недостаточно замечательно, то вы можете выполнить
копирование в новый массив m y N e w F is h e s с типом элементов F i s h * с помощью вызо­
ва в цикле виртуальной функции F i s h : : C lo n e ( ), как показано в строке 60. Обратите
внимание, что массив очень мал: в нем только четыре элемента. Но он может быть
намного больше, хотя это и не имеет никакого значения для логики копирования и
требует лишь коррекции условия завершения цикла. Строка 64 фактически является

340

ЗАНЯТИЕ 11.

П олиморф изм

проверкой, которая состоит в вызове виртуальной функции F i s h : : S w im ( ) для каждо­
го хранимого в новом массиве элемента, чтобы проверить, скопировала ли функция
C lo n e () объект класса T u n a как T u n a , а не просто как F i s h . Показанный вывод де­
монстрирует, что все скопировано правильно.

НЕ РЕКОМЕНДУЕТСЯ

РЕКОМЕНДУЕТСЯ
Отмечайте как

v i r t u a l те функции базового

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

Помните, что чисто

Не забывайте оснащать

базовый класс вирту­

альным деструктором.

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

дать экземпляр абстрактного базового класса.

ют класс абстрактным базовым классом, а сами

Не забывайте,

эти функции должны быть реализованы в произ­

спасает общий базовый класс от возникнове­

что виртуальное наследование

водном классе.

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

Помечайте

только один его экземпляр при множественном

в производном классе функцию,

предназначенную

для

перекрытия

базовой

наследовании.

функциональности, как o v e r r i d e .

Не путайте назначение

Используйте виртуальное наследование для ре­

t u a l при использовании в создаваемой иерар­

шения проблемы ромба.

хии наследования с тем же ключевым словом в

ключевого слова v i r ­

объявлении функций базового класса.

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

Вопросы и ответы
■ Зачем использовать ключевое слово v i r t u a l в определении функции базового
класса, если код компилируется и без этого?
Без ключевого слова v i r t u a l вы не в состоянии гарантировать переадресацию вы­
зова o b j B a s e . F u n c t i o n () функции D e r i v e d : : F u n c t i o n (). Успешная компиляция
кода — не единственная мера его качества.

Коллоквиум

341

■ Зачем компилятор создает таблицу виртуальных функций?
Для хранения указателей на функцию, чтобы гарантировать вызов правильной вер­
сии виртуальной функции.

■ Всегда ли у базового класса должен быть виртуальный деструктор?
В идеале — да. Только тогда вы можете гарантировать, что если некто напишет
Base* pBase = new Derived));
delete pBase;

то вызов оператора d e l e t e для указателя типа B a s e * приведет к вызову деструктора
- D e r i v e d (). Для этого деструктор - B a s e () должен быть объявлен виртуальным.

■ Зачем нужен абстрактный базовый класс, если я не могу даже создать его эк­
земпляр?
Абстрактный класс и не предназначен для создания объектов; его задача — быть
унаследованным. Он содержит чисто виртуальные функции, определяющие мини­
мальный набор функциональности, который должен реализовываться производны­
ми классами, выполняя, таким образом, роль интерфейса.

■ Должен ли я использовать в иерархии наследования ключевое слово v i r t u a l
в объявлениях всех виртуальных функций, или же достаточно использовать
его только в базовом классе?
Достаточно указать ключевое слово v i r t u a l в объявлении функции только один
раз, в базовом классе.

■ Могу ли я определить функции-члены и переменные-члены в абстрактном
классе?
Конечно, можете. Но помните, что нельзя создать экземпляр абстрактного класса,
поскольку у него есть по крайней мере одна чисто виртуальная функция, которая
должна быть реализована производным классом.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Вы моделируете формы (круг и треугольник) и хотите, чтобы каждый из этих
классов обязательно реализовывал функции A r e a () и P r i n t (). Как это сделать?
2. Для всех ли классов компилятор создает таблицу виртуальных функций?

342
3.

|

ЗАНЯТИЕ 11. Полиморфизм

У моего класса F is h есть два открытых метода, одна чисто виртуальная функ­
ция и несколько переменных-членов. Этот класс все еще остается абстрактным
базовым классом?

Упражнения
1. Создайте иерархию наследования, которая реализует контрольный вопрос 1 для
круга и треугольника.
2. О тладка. Что неправильно в следующем коде?
class Vehicle
{
public:
Vehicle() {}
-Vehicle(){}

};
class Car: public Vehicle
{
public:

Car() {}
-Car()

{}

};
3. Каким будет порядок выполнения конструкторов и деструкторов в (неверном)
коде упражнения 2, если экземпляр класса Саг создается и удаляется следую ­
щим образом?
Vehicle* pMyRacer = new Car;
delete pMyRacer;

ЗАНЯТИЕ 12

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

Применение ключевого слова o p e r a to r

■ Унарные и бинарные операторы
■ Операторы преобразования
■ Операторы перемещающего присваивания
■ Операторы, которые не могут быть переопределены

344

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

Что такое операторы C++
На синтаксическом уровне оператор от функции отличает очень немногое — лишь
использование ключевого слова o p e r a t o r . Объявление оператора очень похоже на
объявление функции:

Возвращаемый_тип operator Знак {Список_параметров) ;
В данном случае Знак может быть любым оператором, который хочет определить
программист. Это может быть символ + (сложение) или && (логическое И) и т.д. Опе­
ранды помогают компилятору отличить один оператор от другого. Встает вопрос —
зачем язык C++ предоставляет операторы, когда уже поддерживаются функции?
Рассмотрим вспомогательный класс D a t e , инкапсулирующий день, месяц и год:
Date holiday(1, 1, 2017); // Инициализация датой 1 января 2017 года

Если теперь понадобится сменить дату на следующий день, 2 января, то какой из
предложенных способов удобнее и интуитивно понятнее?
1. Использовать оператор: + + h o lid a y ;
2. Использовать функцию D a t e :: I n c r e m e n t ( ) : h o l i d a y . I n c r e m e n t () ;
Конечно, первый способ понятнее и проще, чем применение метода I n c r e m e n t () .
Выражения с операторами легче в использовании и интуитивно понятнее. Реализация
оператора меньше (
1

В ы б о р члена
Л о ги ч е ск о е НЕ

&

П о л уч е н и е адреса

-

Д о п о л н е н и е д о ед и н и ц ы

+

Ун ар н ы й плю с

-

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

О ператоры п р е о б р а з о в а н и я

П р е о б р а зо в а н и е в д р уго й тип

Программирование унарного оператора
инкремента или декремента
Унарный префиксный оператор инкремента (++) может быть создан в пределах
объявления класса с использованием следующего синтаксиса:
// Унарный оператор инкремента (префиксный)
Date& operator ++()

346

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

{
// Код реализации оператора
return *this;

}
Постфиксный оператор инкремента (++) имеет иной тип возвращаемого значения
(которое используется не всегда), а также фиктивный аргумент типа in t , отличающий
постфиксный оператор от префиксного:
Date operator ++(int)

{
// Сохранение текущего объекта до его увеличения
Date сору(*this);
// Реализация оператора
// Возврат сохраненного значения
return сору;

}
Синтаксис префиксного и постфиксного операторов декремента такой же, как и
операторов инкремента, только объявление содержит — там, где объявление инкре­
мента содержит ++. Листинг 12.1 демонстрирует простой класс D ate, позволяющий
увеличивать даты с помощью оператора ++.
ЛИСТИНГ 12.1. Класс даты с операторами инкремента и декремента__________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
using namespace std;
class Date
{
private:
int day, month, year;
public:
Date(int inMonth, int inDay, int inYear)
: month(inMonth), day(inDay), year(inYear)

11 :
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:

Date& operator ++() // Префиксный инкремент
{
++day;
return *this;
}
Dates operator — () // Префиксный декремент
{
— day;
return *this;
}

{};

Унарные операторы
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:

void DisplayDate()
{
cout « day «
}

«

. «

"." «

year «

|

347

endl;

};
int main()
{
Date holiday(l, 7, 2017);
cout « "Дата инициализирована
holiday.DisplayDate();

значением: ";

++holiday; // Вперед на один день
cout « "Дата после префиксного инкремента: ";
holiday.DisplayDate();
— holiday; // Назад на один день
cout « "Дата после префиксного декремента: ";
holiday.DisplayDate();
return 0;
}

Результат
Дата инициализирована значением: 7.1.2017
Дата после префиксного инкремента: 8.1.2017
Дата после префиксного декремента: 7.1.2017

Анализ
Представляющие интерес операторы определены в строках 12-22 и обеспечивают
добавление и вычитание дня из экземпляра класса Date, как показано в строках 37 и
41 в функции main (). Префиксные операторы инкремента и декремента, как видно из
примера, возвращают ссылку на тот же экземпляр после выполнения операции увели­
чения или уменьшения.

ПРИМЕЧАНИЕ

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

Для обеспечения постфиксного инкремента и декремента достаточно добавить в
класс Date следующий код:

348

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

// Постфиксный оператор отличается от префиксного типом
// возвращаемого значения и параметром
Date operator ++(int)

{
Date copy(day, month, year);
++Day;
return copy;

}
// Постфиксный оператор декремента
Date operator — (int)

{
Date copy(day, month, year);
— Day;
return copy;

Теперь вы можете использовать следующие операции с объектами класса D a te :
Date Holiday(1, 1, 2017); // Создание экземпляра
++Holiday; // Использование префиксного оператора инкремента ++
Holiday++;
// Использование постфиксного оператора инкремента ++
— Holiday;
// Использование префиксного оператора декремента —
Holiday— ; // Использование постфиксного оператора декремента —

Как показано в реализации постфиксных операторов, перед операцией ин­

ПРИМЕЧАНИЕ

кремента или декремента создается копия, содержащая текущее состояние
объекта; она и будет возвращена после выполнения операции.
Таким образом, если вам нужен только инкремент, выбирайте префиксный
оператор + + о б ъ е к т ; а не о б ъ е к т + + ;, чтобы избежать создания вре­
менной копии, которая не используется.

Создание операторов преобразования
Если в код функции m a in ( ) из листинга 12.1 добавить строку
cout «

Holiday; // Ошибка из-за отсутствия оператора преобразования

то произойдет отказ компиляции с сообщ ением E r r o r : b i n a r y f « ! : n o o p e r a t o r
f o u n d w h ic h

ta k e s

a r ig h t - h a n d

o p e ra n d o f ty p e

'D a t e 1 (o r th e r e

is

no

(ошибка: бинарный ' « ' : не найден оператор, получающий
правый операнд типа ' D a t e ' (или нет приемлемого преобразования)). По существу,
это сообщ ение означает, что поток c o u t не знает, как интерпретировать экземпляр
класса D a te , поскольку этот класс не поддерживает операторы, которые могли бы пре­
образовать его содержимое в тип, который в состоянии вывести поток c o u t .
Однако поток c o u t вполне может работать с константной строкой типа c o n s t
a c c e p t a b l e c o n v e r s io n )

c h a r* :

std::cout «

"Hello world"; // const char* работает!

Унарны е операторы

|

349

Поэтому, чтобы поток c o u t работал с объектом класса Date, достаточно добавить
оператор, который преобразует его в тип c o n s t char*:
operator const char*()

{
// Реализация оператора, возвращающая const char*

}
Листинг 12.2 демонстрирует пример реализации такого оператора.
Л И С Т И Н Г 1 2 .2 . Р е а л и з а ц и я о п е р а т о р а п р е о б р а з о в а н и я
в

c o n s t char* для к л а с с а Date______________________________________________________
0:
1:
2:
3:
4:
5:

tinclude
#include // Заголовочный файл для ostringstream
#include
using namespace std;
class Date

6: {
7:
private:
8:
int day, month, year;
9:
string datelnString;

10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:

public:
Date(int inMonth, int inDay, int inYear)
: month(inMonth), day(inDay), year(inYear)

{};

operator const char*()
{
ostringstream formattedDate; // Помогает создать строку
formattedDate « day « ".” « month « "." « year;
datelnString = formattedDate.str();
return datelnString.c_str();

}
};
int main()
{
Date Holiday(1,7, 2017);
cout « "Рождество: "

«

Holiday «

endl;

// string strHoliday(Holiday);
// OK!
// strHoliday = Date(11, 7, 2017); // Тоже OK!
return 0;
}

350

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

Результат
Рождество: 7.1.2017

Анализ
Преимущество реализации оператора c o n s t c h a r * (строки 15-23) проявляется
в строке 29 функции m a in (). Теперь экземпляр класса D a t e может непосредственно
использоваться потоком c o u t благодаря тому, что этот поток принимает тип c o n s t
c h a r * . Компилятор автоматически использует результат подходящего (а в данном слу­
чае единственно доступного) оператора и передает его потоку c o u t , который отобра­
жает дату на экране. В нашей реализации оператора c o n s t c h a r * использован опе­
ратор s t d : . - o s t r i n g s t r e a m , преобразующий целочисленные члены класса в объект
s t d : : s t r i n g (строка 18). Можно было бы сразу вернуть результат метода f o r m a t t e d D a t e . s t r (), но мы сохраняем его копию в закрытом члене D a t e :: D a t e l n S t r i n g
(строка 20), поскольку переменная f o r m a t t e d D a t e является локальной и уничтожает­
ся при завершении работы оператора. Поэтому указатель, полученный вызовом мето­
да s t r ( ) , после выхода из оператора будет недействителен.
Этот оператор открывает новые возможности использования класса D a t e . Теперь
можно даже присвоить экземпляр даты строке непосредственно:
string strHoliday(Holiday);

// OK! Компилятор вызывает
// оператор const char*
strHoliday = Date(11, 11, 2011); // Также OK!

ВНИМАНИЕ!

Обратите внимание, что такое присваивание вызывает неявное преобра­
зование, т.е. компилятор использует доступный оператор преобразования
(в нашем случае - c o n s t c h a r * ) , тем самым разрешая непреднамерен­
ные присваивания, которые компилируются без каких-либо сообщений об
ошибках. Чтобы избежать неявных преобразований, используйте ключевое
слово e x p l i c i t в начале объявления оператора:
e x p lic it

o p e ra to r c o n st

c h a r* ()

{
//

Код п рео б р а зо в ан и я

}
Использование этого ключевого слова заставляет программиста явно ука­
зывать свои намерения с использованием операторов приведения:

string strHoliday(static_cast(Holiday));
s t r H o lid a y = s t a t ic _ c a s t < c o n s t c h a r * > ( D a te (1 1 ,1 1 ,2 0 1 6 ) ) ;
Операторы приведения, включая s t a t i c _ c a s t , подробнее рассматрива­
ются на занятии 13, “Операторы приведения”.

Унарные операторы

ПРИМЕЧАНИЕ

351

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

explicit operator int()

{
// Здесь код преобразования

}
Такой оператор позволяет использовать экземпляр класса D a t e в качестве
целого числа:

FuncTakesInt(static_cast(Date(25,12,2016)));
В листинге 12.7 показано применение операторов преобразования с клас­
сом строки.

Создание оператора разыменования (*)
и оператора выбора члена (->)
Оператор разыменования (*) и оператор обращения к члену класса (выбора члена)
(-> ) чаще всего используются при создании классов интеллектуальных указателей.
Интеллектуальные указатели (smart pointer) — это вспомогательные классы, явля­
ющиеся оболочками для обычных указателей и облегчающие управление памятью
(или ресурсом), решая проблемы владения и копирования. В некоторых случаях они
даже способны повысить производительность приложения. Подробно интеллектуаль­
ные указатели обсуждаются на занятии 26, “Понятие интеллектуальных указателей”,
а здесь рассматривается лишь вопрос о том, как перегрузка операторов помогает ра­
боте интеллектуальных указателей.
Давайте проанализируем использование указателя s t d : : u n iq u e _ p tr в листин­
ге 12.3 и рассмотрим, как операторы (*) и (-> ) помогают использовать класс интел­
лектуального указателя как любой обычный указатель.
Л И С Т И Н Г 1 2 .3 . И с п о л ь з о в а н и е и н те л л е к ту а л ьн о го у к а з а т е л я

0:
1:
2:
3:
4:

#include
#include
// Включение для использования unique_ptr
using namespace std;
class Date

5: {
6:
7:
8:

private:
int day, month, year;
string datelnString;

9:
10:
11:
12:
13:

u n iq u e p tr _____________

public:
Date(int inMonth, int inDay, int inYear)
: month(inMonth), day(inDay), year(inYear)

{};

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

352

14:
15:
16:
17:
18: };
19:
20: int

21 :

void DisplayDate()
{
cout « day «
}

"." «

month «

«

year «

endl;

main()

{

22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35: }

unique_ptr smartIntPtr(new int);
*smartIntPtr = 42;
// Использование интеллектуальногоуказателя как
cout « "Целое число равно: " « *smartIntPtr «

int*
endl;

unique_ptr smartHoliday(new Date(l, 1, 2017));
cout « "Новый экземпляр даты: ";
// Использование smartHoliday как
smartHoliday->DisplayDate();

Date*

return 0;

Результат
Целое число равно: 42
Новый экземпляр даты: 1.1.2017

Анализ
В строке 22 объявляется интеллектуальный указатель типа i n t . Эта строка демон­
стрирует синтаксис инициализации шаблона для класса интеллектуального указателя
u n i q u e j o t r . Аналогично в строке 28 объявляется интеллектуальный указатель на эк­
земпляр класса D a t e . Пока сосредоточьтесь на использовании указателя и игнорируй­
те детали.

ПРИМЕЧАНИЕ

Не волнуйтесь, если синтаксис пока что кажется вам непонятным, посколь­
ку шаблоны рассматриваются позже, на занятии 14, “Введение в макросы
и шаблоны”.

Этот пример демонстрирует не только то, как интеллектуальный указатель позво­
ляет использовать обычный синтаксис указателя (строки 23 и 32). В строке 23 вы мо­
жете вывести значение типа i n t , используя синтаксис * s m a r t I n t P t r , а в строке 32 вы
используете вызов s m a r t H o lid a y - > D is p la y D a t a () так, как будто эти две переменные
имеют тип i n t * и D a t e * соответственно. Секрет кроется в классе интеллектуального
указателя s t d : : u n iq u e j p t r , который реализует операторы (*) и (-> ).

Бинарные операторы

ПРИМЕЧАНИЕ

|

353

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

Бинарные операторы
Операторы, работающие с двумя операндами, называются бинарными оператора­
ми (binary operator). Определение бинарного оператора, реализованного как глобаль­
ная функция или статическая функция-член, имеет следующий вид:

Возвращаемый_тип operator Знак (Параметр1, Параметр2)
{
// ... Реализация

}
Определение бинарного оператора, реализованного как член класса, имеет вид

Возвратаемый_тип operator Знак (Параметр)
{
// ... Реализация

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

Типы бинарных операторов
Бинарные операторы, которые могут быть перегружены или переопределены в
приложении C++, приведены в табл. 12.2.

ТАБЛИЦА 12.2. Перегружаемые бинарные операторы
Оператор

Название

/

Запятая

1=
О

Н е р а в е н ств о

Ъ

Д ел е н и е по м од улю

о,—
о-

Д ел е н и е по м од улю с п р и св а и в а н и е м

&

П о б и то в о е И

ScSc

Л о ги ч е ск о е И

&=


П о б и тов ое И с п р и св а и в а н и е м
У м н ож ен и е

*=

У м н о ж ен и е с п р и св а и в а н и е м

+

Слож ение

+=

С л о ж е н и е с п р и св а и в а н и е м

354

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка
Окончание табл. 12.2

О ператор

Н азвание

-

Вы читан ие

-=

Вы читан ие с п р и св а и в а н и е м

-> *

К о св е н н о е об р а щ е н и е к указателю на член класса

/

Д ел е н и е

/=

Д ел е н и е с п р и св а и в а н и е м

<

М еньш е

«

С д в и г влево

«=

С д в и г в л е в о с п р и св а и в а н и е м



Б о льш е

>=

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

»

Сдвиг вправо

»=

С д в и г в п р а в о с п р и св а и в а н и е м

A

И скл ю ча ю щ е е ИЛИ

A __

И скл ю чаю щ ее ИЛИ с п р и св а и в а н и е м
П о б и то в о е ИЛИ

1
1=

П о б и то в о е ИЛ И с п р и св а и в а н и е м

11

Л о ги ч е ск о е ИЛИ

П

О п е р а то р и н д е ксац и и

Создание бинарных операторов
сложения (а + Ь ) и вычитания ( а -Ь )
П одобно операторам инкремента и декремента, бинарные операторы “плюс” и
“минус”, будучи определены, позволяют суммировать и вычитать значения поддержи­
ваемого типа данных из объекта класса, который реализует эти операторы. Вернемся к
нашему календарному классу D a t e . Хотя мы уже реализовали в нем возможность ин­
кремента, переводящего календарь на один день вперед, он все еще не поддерживает
возможность перевода, скажем, на пять дней вперед. Для этого необходимо реализо­
вать бинарный оператор (+), как сделано в листинге 12.4.
Л И СТИ Н Г 1 2 .4 . К а л е н д а р н ы й к л а с с с б и н а р н ы м о п е р а т о р о м с у м м ы ______________________

0:
1:
2:
3:
4:

#include
using namespace std;

class Date
{
5:
private:

Бинарные операторы
6:

int day, month, year;

7:

string datelnString;

8:
9:

public:

10:

Date(int inMonth,

11:

: month(inMonth), day(inDay), year(inYear)

int inDay, int inYear)
{};

12 :
13:

Date operator + (int daysToAdd)

14:

{

15:

// Сложение

Date newDate(month, day + daysToAdd, year);

16:

return newDate;

17:
18:

}

19:

Date operator -(int daysToSub)

20 :

{

21:

// Вычитание

return Date(month, day - daysToSub, year);

22:

}

23:
24:
25:
26:
27:
28:

void DisplayDate()
{
cout « day «
}

«

month «

};

29:
30: int main()
31:
32:

{
Date Holiday(l, 7, 2017);

33:

cout : дата 2 позже" «

endl;

endl;

if (holidayl = holidayl)
cout « ">=: дата 1 не позже даты 2" «

endl;

return 0;

endl;

Бинарные операторы

|

363

Результат
Дата 1: 25.12.2016
Дата 2: 31.12.2016
: дата 2 позже
=: дата1 не позже даты 2

Анализ
Интересующие нас операторы реализованы в строках 12-50 и частично исполь­
зуют оператор == из листинга 12.6. Применение этих операторов в строках 6 8 -7 8 в
функции m ain () демонстрирует, насколько реализация этих операторов делает ис­
пользование класса Date простым и интуитивно понятным.

Перегрузка оператора
копирующего присваивания (=)
Нередко содержимое экземпляра класса необходимо присвоить другому экземпляру:
Date holiday(25, 12, 2016);
Date anotherHoliday(1, 1, 2017);
anotherHoliday = holiday; // Использование оператора копирующего присваивания

Это присваивание ведет к вызову оператора копирующего присваивания по умол­
чанию, который компилятор сгенерирует автоматически, если вы не предоставите
такового. В зависимости от природы вашего класса стандартный копирующий кон­
структор может оказаться не соответствующим стоящей перед ним задаче, особенно
если ваш класс задействует ресурс, который не будет скопирован. Данная проблема
с копирующим присваиванием по умолчанию аналогична уже рассматривавшейся на
занятии 9, “Классы и объекты”, проблеме копирующего конструктора по умолчанию.
Чтобы гарантировать глубокое копирование, как и в случае с копирующим конструк­
тором, необходимо определить собственный оператор копирующего присваивания:
Тил& operator= (const Тил& исходный_объект)

{
if (this != Шсходный_объект) // Защита от копирования
{
// объекта в себя самого
// Реализация оператора присваивания

}
return *this;

}
Глубокое копирование важно, если ваш класс инкапсулирует простой указатель,
такой как у класса M yString из листинга 9.9. Чтобы гарантировать глубокое копиро­
вание во время присваивания, определите оператор копирующего присваивания, как
это сделано в листинге 12.8.

364

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

ЛИСТИНГ 12.8. Улучшенный класс M yString из листинга 9.9
с оператором копирующего присваивания
0
1
3

#include
using namespace std;
#include
class MyString

4

{

2

5
6

private:
char* buffer;

7

8

public:
MyString(const char* initiallnput)

9

10

{

11

if(initiallnput != nullptr)

12

{
buffer = new char[strlen(initiallnput) + 1];
strcpy(buffer, initiallnput);

13
14
15

}

16

else
buffer = nullptr;

17

}

18
19

20

// Оператор копирующего присваивания
MyString& operator=(const MyString& copySource)

21
22

{

23

if ((this != &copySource)& & (copySource.buffer != nullptr))

24

{
if (buffer != nullptr)
delete!] buffer;

25
26
27

// Глубокое копирование в свой буфер
buffer = new char[strlen(copySource.buffer) + 1];

28
29
30

// Копирование из исходного объекта в локальный буфер
strcpy(buffer, copySource.buffer);

31
32

}

33
34

return *this;

35

}

36
37
38

operator const char*()

39

{
return buffer;

40

}

41
42
43

^MyString()

44

{
delete[] buffer;

45

}

46
47

};

Бинарные операторы
48:
49: int main()
50: {
51:
MyString stringl("Hello ");
52:
MyString string2(" World");
53:
54:
cout « "До присваивания: " « endl;
55:
cout « stringl « string2 «
endl;
56:
string2 = stringl;
57:
cout « "После присваивания string2 = stringl: " «
58:
cout « stringl « string2 «
endl;
59:
60:
return 0;
61: }

|

365

endl;

Результат
До присваивания:
Hello World
После присваивания string2 = stringl:
Hello Hello

Анализ
Я преднамеренно опустил копирующий конструктор в этом примере, чтобы сокра­
тить объем кода (но при создании подобного класса обязательно добавьте его; см. лис­
тинг 9.9). Оператор копирующего присваивания реализован в строках 21-36. Он очень
похож на копирующий конструктор, но с предварительной проверкой, гарантирующей,
что оригинал и копия не являются одним и тем же объектом. После успешной провер­
ки оператор копирующего присваивания класса M y S t r i n g сначала освобождает свой
внутренний буфер, затем повторно резервирует место для текста копии, а потом ис­
пользует функцию s t r c p y () для копирования, как показано в строке 14.
Еще одно незначительное различие между листингами 12.8 и 9.9 в том,
что функция G e t S t r i n g O

заменена оператором c o n s t

c h a r * , как

видно из строк 3 8 -4 1 . Этот оператор облегчает использование класса
M y S t r in g , как показано в строке 55, где один поток c o u t используется
для отображения двух экземпляров класса M y S t r in g .

ВНИМАНИЕ!

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

366

|

З А Н Я Т И Е 1 2 . Т и п ы о п е р а т о р о в и их п е р е г р у з к а

СОВЕТ

Чтобы создать класс, который не может быть скопирован, объявите копи­
рующий конструктор и оператор копирующего присваивания как закрытые.
Объявления как p r i v a t e при отсутствии реализации вполне достаточно
для компилятора, чтобы сообщить об ошибке при любых попытках копиро­
вания этого класса, например при передаче в функцию по значению или
при присваивании одного экземпляра другому.

Оператор индексации ([])
Оператор [ ], позволяющий обращаться к классу в стиле массива, называется опе­
ратором индексации (subscript operator). Типичный синтаксис оператора индексации
таков:

Возвращаемый_типЬ operator[] (Тип_индекса& значение_индекса );
Так, при создании такого класса, как M y S t r in g , инкапсулирующего класс динами­
ческого массива символов c h a r * b u f f e r , оператор индексации существенно облегчит
произвольный доступ к отдельным символам в буфере:
class MyString
{
// ... другие члены класса
public:
/*const*/ char& operator[](int index) /*const*/

{
// Возврат из буфера символа в позиции index

}
};
Пример в листинге 12.9 демонстрирует, как оператор индексации ([ ]) обеспечива­
ет возможность итерации символов, содержащихся в экземпляре класса M y S t r i n g с
использованием обычной семантики массива.
Л И С Т И Н Г 1 2 .9 . Р е а л и з а ц и я о п е р а т о р а и н д е к с а ц и и ([ ])

в к л а с с е M y S t r in g ,

о б е с п е ч и в а ю щ е г о п р о и з в о л ь н ы й доступ к с и м в о л а м в б у ф е р е M y S t r i n g : : b u f f e r ______

0:
1:
2:
3:
4:
5:
6:
7:

#include
#include
#include
using namespace std;
class MyString
{
private:
char* buffer;

8:
9:
10:

// Закрытый конструктор по умолчанию
MyString() {}

11 :
12: public:

Бинарные операторы
// Конструктор
MyString(const char* initiallnput)

13
14
15
16
17
18
19
20

{
if(initiallnput != nullptr)

{
buffer = new char[strlen(initiallnput) + 1];
strcpy(buffer, initiallnput);

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

else
buffer = nullptr;

}
// Копирующий конструктор: вставить из листинга 9.9
MyString(const MyString& copySource);
// Оператор копирующего присваивания: вставить из листинга 12.8
MyString& operator=(const MyString& copySource);
const char& operator!](int index) const

{
if (index < GetLengthO)
return buffer[index];

// Деструктор
^MyString()

{
if (buffer != nullptr)
delete[] buffer;

}
int GetLengthO const

{
return strlen(buffer);

operator const char*()

{
return buffer;

int main()

{
cout « "Введите предложение: ";
string strlnput;
getline(cin, strlnput);
MyString youSaid(strlnput.c_str());
cout «

"Ваш ввод с использованием operator!]:

«

endl;

|

367

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

368

64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75: }

for(int index = 0; index < youSaid.GetLength(); ++index)
cout « youSaid[index] « " ";
cout « endl;
cout « "Введите индекс 0 - " « youSaid.GetLength() - 1 «
int Inindex = 0;
cin » Inindex;
cout « "Искомый символ в позиции " « Inindex;
cout « " - " « youSaid[Inindex] « endl;

": ";

return 0;

Результат
Введите предложение: OK, operator[] работает!
Ваш ввод с использованием operator!]:
OK,
o p e r a t o r ! ]
р а б о т а е т !
Введите индекс 0 - 37: 2
Искомый символ в позиции 2 - ,

Анализ
Эта программа получает предложение, которое вы вводите, создает строку
использует ее, как показано в строке 6 1 , а затем применяет цикл f o r для
посимвольного вывода строки с помощью оператора индексации ([ ]) и использования
синтаксиса, как у массива (строки 6 4 и 6 5 ). Сам оператор ([ ]) определяется в стро­
ках 3 1 - 3 5 ; он обеспечивает прямой доступ к символу в определенной позиции после
проверки того, что требуемая позиция находится в пределах буфера c h a r * b u f f e r .

M y S t r in g ,

ВНИМАНИЕ!

При программировании операторов приобретает особую важность исполь­
зование ключевого слова c o n s t . Обратите внимание, как листинг 12.9
ограничил возвращаемое значение оператора индексации ( [ ] ) типом
const

c h a r & . Программа работает и компилируется и без ключевых

слов c o n s t , но причина, по которой они использованы в коде, - избежать
модифицирующего кода наподобие
M y S t r in g

s a y H e llo ( " H e llo

s a y H e llo [ 2 ]

= ' k f //

W o r ld " ) ;

О ш и б к а:

o p e ra to r!]

константны й

При использовании ключевого слова c o n s t вы защищаете внутренний
членкласса M y S t r i n g : : b u f f e r от непосредственного изменения из­
вне с помощью оператора [ ]. Кроме объявления возвращаемого значе­
ния как c o n s t , следует объявить как c o n s t и функцию оператора, чтобы
обеспечить ее неспособность изменять члены-данные класса.
Как правило, желательно использовать ограничение c o n s t везде, где это
возможно, чтобы избежать непреднамеренных изменений данных и повы­
сить защиту членов-данных класса.

Оператор функции ()

369

Код, представленный в листинге 12.9, хотелось бы усовершенствовать, реализовав
один оператор индексации, который позволял бы и читать содержимое строки, и за­
писывать значения в элемент динамического массива.
Для этого можно реализовать два оператора индексации: один как константную
функцию, а второй — как не константную:
char& operator[](int nlndex); // Используется для записи / изменения
// буфера по индексу
char& operator[](int nlndex) const; // Используется только для
// чтения символа по индексу

Компилятор достаточно интеллектуален, чтобы вызывать константную версию
функции для операций чтения и не константную — для операций записи в объект
MyString. Таким образом, вы можете (если хотите) разделить функциональность
между двумя функциями. Существуют и другие бинарные операторы (см. табл. 12.2),
которые могут быть переопределены или перегружены, но они на этом занятии не
обсуждаются. Однако их реализация подобна уже рассмотренной.
Другие операторы, такие как логические и побитовые, следует создавать, только
если они улучшат класс. Конечно, такому календарному классу, как Date, не обяза­
тельно реализовать логические операторы, тогда как классу, реализующему строку
или число, возможно, они будут требоваться достаточно часто.
Принимая решение о перегрузке имеющихся операторов или создании новых, пом­
ните о цели вашего класса и способе его использования.

Оператор функции ()
Оператор (), заставляющий объекты вести себя, как функции, называется опера­
тором функции (function operator). Такие операторы применяются в стандартной би­
блиотеке шаблонов (STL) и обычно используются в алгоритмах STL. Они применимы
при принятии решений; такие функциональные объекты обычно называются унарным
или бинарным предикатом (predicate) в зависимости от количества операндов. В лис­
тинге 12.10 анализируется настолько простой функциональный объект, что вы сразу
сможете понять, почему ему дано такое интригующее название!
ЛИСТИНГ 12.10. Функциональный объект, созданный с использованием оператора ()
1:
2:
3:
4:
5:

#include
#include
using namespace std;
class Display

6: {

7: public:
8:
void operator()(string input) const
9:
{
10:
cout « input « endl;

11 :

}

370

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

12:};
13:
14: int main ()
15: {
16:
Display displayFuncObject;
17:
18:
// Эквивалентно displayFuncObject.operator()("Моя строка!”);
19:
displayFuncObject("Моя строка!");

20:
21:
22:

return 0;
}

Результат
Моя строка!

Анализ
В строках 8-11 реализуется оператор (), который затем используется в строке 19 в
функции m a in ( ). Обратите внимание, что объект d i s p l a y F u n c O b j e c t используется с
синтаксисом вызова функции: компилятор неявно преобразует код, который выглядит
как вызов функции, в вызов o p e r a t o r ( ) .
Собственно, именно поэтому данный оператор и называется оператором функ­
ции () , а объект D i s p l a y — функциональным объектом, или функтором (functor).
Более подробная информация по этой теме рассматривается на занятии 21, “Понятие
о функциональных объектах”.

Перемещающий конструктор и оператор
перемещающего присваивания
Перемещающий конструктор и оператор перемещающего присваивания представ­
ляют собой средства оптимизации производительности, которые стали частью стан­
дарта С ++11 и гарантируют, что временные значения (r-значения, которые не сущ ес­
твуют вне инструкций) не будут копироваться понапрасну. Это особенно полезно при
работе класса, который управляет динамически распределяемым ресурсом, таким как
динамический массив или строка.

Проблема излишнего копирования
Обратите внимание на оператор сложения, реализованный в листинге 12.4. Факти­
чески этот оператор создает копию и возвращает ее. Если класс M y S t r i n g из листин­
га 12.9 поддерживает оператор сложения, то приведенный далее фрагмент исходного
текста представляет собой корректный пример конкатенации строк:
MyString Hello("Hello ");
MyString World("World");

Перемещающий конструктор и оператор перемещающего присваивания
MyString СРР(" of C++");
MyString sayHello(Hello + World + CPP); //
//
MyString sayHelloAgain("overwrite this");
sayHelloAgain = Hello + World + CPP;
//
//
//

|

371

Оператор +,
копирующий конструктор
Оператор +,
копирующий конструктор,
копирующее присваивание

Эта простая конструкция, выполняющая конкатенацию трех строк, использует би­
нарный op era to r+ :
MyString operator+(const MyString& addThis)

{
MyString newStr;
if (addThis.buffer != nullptr)

{
// Копирование в newStr

)
return newStr; // Возврат копии по значению с
// вызовом копирующего конструктора

Такой оператор, облегчающий программирование с применением конкатенации
с помощью интуитивно понятных выражений, может привести к проблемам произ­
водительности. Создание объекта s a y H e llo требует двойного выполнения оператора
суммирования; в результате каждого выполнения оператора + создается временная ко­
пия, поскольку объект класса M yString возвращается по значению, так что при этом
вызывается копирующий конструктор. Он осуществляет глубокое копирование строки
во временный объект, который не существует после завершения инструкции. Получа­
ется, что это интуитивно понятное выражение приводит к созданию нескольких вре­
менных копий (r-значений), которые после завершения выполнения инструкции будут
уничтожены, а следовательно, являются узким местом с точки зрения производитель­
ности, создаваемым языком C++. По крайней мере, так было до недавнего времени.
Теперь эта проблема решена. Компилятор, соответствующий стандарту С++11,
распознает временные объекты и использует для них перемещающий конструктор
или оператор перемещающего присваивания, если таковые предоставлены програм­
мистом.

Объявление перемещающих конструктора
и оператора присваивания
Перемещающий конструктор имеет следующий синтаксис:
class Sample {
private:
Type * ptrRes;
public:

372

З А Н Я Т И Е 1 2 . Т ип ы о п е р а т о р о в и их п е р е г р у з к а

Sample(Sample && moveSource)

{

ptrRes = moveSource.ptrRes;
moveSource.ptrRes = nullptr;

//
//
//
//

Sample & operator= (Sample &&
//
moveSource) { //
if (this != & moveSource) {
delete[] ptrRes;
ptrRes = moveSource.ptrRes;
moveSource.ptrRes = nullptr;

Перемещающий конструктор,
обратите внимание на &&
Получение владения,
перемещение

Оператор перемещающего
присваивания, см. &&
// Освобождение ресурса.
// Получение владения,
// перемещение

}

};

Sample();
// Конструктор по умолчанию
Sample(const Sample & copySource);
// Копирующий конструктор
Sample & operator= (const Sample & copySource); // Копирующее
// присваивание

Таким образом, объявление перемещающих конструктора и оператора присваива­
ния отличается от обычных копирующих конструктора и оператора присваивания тем,
что входной параметр имеет тип M y C la s s & & . Кроме того, поскольку входной параметр
является исходным объектом для перемещения, он не может быть константным, так
как в процессе перемещения он изменяется. Возвращаемые же значения остаются
теми же, что и ранее, поскольку это просто перегруженные версии конструктора и
оператора присваивания соответственно.
Поддерживающие стандарт C++11 компиляторы гарантируют, что для временных
объектов (r-значений) используется перемещающий, а не копирующий конструктор, а
также оператор перемещающего присваивания вместо копирующего оператора при­
сваивания. В нашей реализации мы обеспечим вместо копирования простое переме­
щение ресурса из объекта источника в объект получателя. Листинг 12.11 демонстри­
рует эффективность этих нововведений С ++11 для оптимизации класса M y S t r in g .
Л И С Т И Н Г 1 2 . 1 1 . К л а с с M y S t r i n g с п е р е м е щ а ю щ и м и к о н стр ук то р о м
и о п е р а т о р о м п р и с в а и в а н и я в д о п о л н е н и е к к о п и р у ю щ и м ____________

0:
1:
2:
3:

#include
#include
using namespace std;
class MyString

4: {
5:
private:
6:
char* buffer;
7:
8:
MyString(): buffer(nullptr) // Закрытый конструктор
9:
{
/ / п о умолчанию
10:
cout « "Конструктор по умолчанию" « endl;

11 :

12 :

}

Перемещающий конструктор и оператор перемещающего присваивания
13
14
15
16
17
18
19
20

public:
MyString(const char* initiallnput) // Конструктор

{
cout « "Конструктор: " « initiallnput «
if(initiallnput != nullptr)

endl;

{
buffer = new char[strlen(initiallnput) + 1];
strcpy(buffer, initiallnput);

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

else
buffer = nullptr;

}
MyString(MyString&& moveSrc) // Перемещающий конструктор

{
cout « "Перемещ. конструктор: " «
if(moveSrc.buffer != nullptr)

moveSrc.buffer «

endl;

{
buffer = moveSrc.buffer;
moveSrc.buffer = nullptr;
}

// Получение владения
// Освобождение перемещен// ного ресурса

}
MyString& operator=(MyString&& moveSrc) // Перемещающее
{
// присваивание
cout « "Перемещ. присваивание: " « moveSrc.buffer «
i f ((moveSrc.buffer != nullptr) && (this != &moveSrc))

endl;

{
delete!] buffer; // release own buffer
buffer = moveSrc.buffer;
moveSrc.buffer = nullptr;
}

// Получение владения
// Освобождение перемещен// ного ресурса

return *this;

MyString(const MyString& copySrc) // Копирующий конструктор

{
cout « "Копир, конструктор: " «
if (copySrc.buffer != nullptr)

copySrc.buffer «

{
buffer = new char[strlen(copySrc.buffer) + 1];
strcpy(buffer, copySrc.buffer);

}
else
buffer = nullptr;

endl;

|

373

374
62:
63:
64:
65:
66:

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка
MyString& operator=(const MyStringfc copySrc) // Оператор
{
// копирующего присваивания
cout « "Копир, присваивание: ” « copySrc.buffer « endl;
if ((this != &copySrc) && (copySrc.buffer != nullptr))
{

67:
if (buffer != nullptr)
68:
delete[] buffer;
69:
70:
buffer = new char[strlen(copySrc.buffer) + 1];
71:
strcpy(buffer, copySrc.buffer);
72:
}
73:
74:
return *this;
75:
}
76:
77:
-MyString() // Деструктор
78:
{
79:
if (buffer != nullptr)
80:
delete[] buffer;
81:
}
82:
83:
int GetLengthO
84:
{
85:
return strlen(buffer);
86:
}
87:
88:
operator const char*()
89:
{
90:
return buffer;
91:
}
92:
93:
MyString operator*(const MyStringS addThis)
94:
{
95:
cout « "operator*:
" « endl;
96:
MyString newStr;
97:
98:
if (addThis.buffer
!=nullptr)
99:
{ newStr.buffer =
100:
new char[GetLength()tstrlen(addThis.buffer)+1];
101:
strcpy(newStr.buffer, buffer);
102:
strcat(newStr.buffer, addThis.buffer);
103:
}
104:
105:
return newStr;
106:
}
107: };
108
109 int main()

110

Перемещающий конструктор и оператор перемещающего присваивания
111:
112:
113:
114:
115:
116:
117:
118:
119: }

MyString
MyString
MyString

|

375

Hello("Hello ");
World("World");
CPP(" of C++");

MyString sayHelloAgain("overwrite this");
sayHelloAgain = Hello + World + CPP;
return 0;

Результат
Вывод без перемещающих конструктора и оператора присваивания (при закоммен­
тированных строках 26-^18):
Конструктор: Hello
Конструктор: World
Конструктор: of C++
Конструктор: overwrite this
operator+:
Конструктор по умолчанию

Копир, конструктор: Hello World
operator+:
Конструктор по умолчанию

Копир, конструктор: Hello World of C++
Копир, присваивание: Hello World of C++

Вывод с перемещающими конструктором и оператором присваивания:
Конструктор: Hello
Конструктор: World
Конструктор: of C++
Конструктор: overwrite this
operator+:
Конструктор по умолчанию

Перемещ. конструктор: Hello World
operator+:
Конструктор по умолчанию

Перемещ. конструктор: Hello World of C++
Перемещ. присваивание: Hello World of C++

Анализ
Код получился действительно весьма длинным, но большая его часть уже была
представлена в предыдущих примерах и на занятиях. Самая важная часть этого лис­
тинга находится в строках 2 6 -4 8 , где реализованы перемещающий конструктор и
оператор перемещающего присваивания соответственно. Те части вывода, на которые
влияют нововведения стандарта С++11, выделены полужирным шрифтом. Обратите
внимание, насколько существенно изменился вывод по сравнению с тем же классом,
но без этих двух средств. Если рассмотреть реализацию перемещающих конструктора

376

|

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

и оператора присваивания, то можно заметить, что семантика перемещения, по су­
ществу, реализуется за счет передачи владения ресурсом от источника перемещения
(строка 31 в перемещающем конструкторе и строка 43 в перемещающем операторе
присваивания). Непосредственно за этим следует присваивание значения n u llp t r ис­
ходному указателю (строки 32 и 44). Это присваивание гарантирует, что деструктор
экземпляра, из которого выполнено перемещение, не выполняет освобождение памя­
ти с помощью оператора d e l e t e в строке 80, поскольку владение ресурсом передано
целевому объекту. Обратите внимание, что в отсутствии конструктора перемещения
вызывается копирующий конструктор, который осуществляет глубокое копирование
строки. Таким образом, перемещающий конструктор существенно экономит время
работы программы и сокращает количество нежелательных операций распределения
памяти и копирования.
Создание перемещающих конструктора и оператора присваивания не является обя­
зательным. В отличие от копирующего конструктора и оператора присваивания копии
компилятор не генерирует его реализацию самостоятельно.
Используйте эти возможности для оптимизации работы классов, которые указыва­
ют на динамически распределяемые ресурсы и которые в противном случае требуют
глубокого копирования даже тогда, когда они используются как временные объекты.

Пользовательские литералы
Литеральные константы были введены на занятии 3, “Использование переменных
и констант”. Вот несколько их примеров:
int bankBalance = 10000;
double pi = 3.14;
char firstAlphabet = ’a';
const char* sayHello = "Hello!";

В приведенном коде 10000, 3 Л 4, ' ' и " H e llo !" представляют собой литеральные
константы. Стандарт C + + И расширяет поддержку литералов, позволяя определять
собственные литералы. Например, если вы работаете над научным приложением, ко­
торое занимается термодинамическими расчетами, то можете захотеть хранить ваши
температурные данные с использованием шкалы Кельвина. Новый стандарт позволяет
объявить ваши температуры с использованием синтаксиса, подобного следующему:
Temperature kl = 32.15_F;
Temperature k2 = 0.0_C;

С помощью определенных вами литералов _F и _С вы делаете ваше приложение
проще для чтения, а значит, и для поддержки. Чтобы определить собственный лите­
рал, следует определить o p era to r" " , как показано далее:

Возвращаемый_тип operator "" Ваш__литерал(Тип значение) {
// Код преобразования

Пользовательские литералы

|

377

В зависимости от природы пользовательского литерала параметр Т и п

ПРИМЕЧАНИЕ

ограничен одним из следующих значений:

u n sig n e d lo n g lo n g i n t для целочисленного литерала
lo n g d o u b le для литерала с плавающей точкой
c h a r , w ch a r_ t, c h a r l6 _ t и c h a r 3 2 _ t для символьного литерала
c o n s t char* для необработанного строкового литерала
c o n s t char* с s i z e _ t для строкового литерала
c o n s t w char_t* с size_ _ t для строкового литерала
c o n s t c h a r l6 _ t* с s i z e _ t для строкового литерала
c o n s t c h a r3 2 _ t* с s i z e _ t для строкового литерала
В листинге 12.12 показаны пользовательские литералы, выполняющие преобразо­
вание типа.
ЛИСТИНГ 12.12. Преобразование температур по Фаренгейту
и Цельсию в значения по шкале Кельвина
0: #include
1: using namespace std;

2:
3: struct Temperature
4: {
5:
double Kelvin;
6:
Temperature(long double kelvin)
7: };

: Kelvin(kelvin)

8:
9: Temperature operator"" _C(long double celcius)

10:

{

11:

12 :
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:

return Temperature(celcius + 273);
}

Temperature operator "" _F(long double fahrenheit)
{
return Temperature((fahrenheit + 459.67) * 5 / 9 ) ;
}
int main()
{

Temperature kl = 31.73_F;
Temperature k2 = 0.0_C;
cout «
cout «

"kl
"k2

return 0;
}

= " «
= " «

kl.Kelvin «
k2.Kelvin «

" K" «
" K" «

endl;
endl;

{}

378

ЗАНЯТИЕ 12. Т ип ы

о п е р а т о р о в и их п е р е г р у з к а

Результат
kl = 273 К
к2 = 273 К

Анализ
В строках 21 и 22 исходного текста показана инициализация двух объектов
один с использованием пользовательского литерала _ F для объявления
значения в градусах Фаренгейта и _ С для объявления значения в градусах Цельсия.
Эти литералы, определенные в строках 9 -1 7 , выполняют работу по преобразованию
соответствующих значений в температуру по шкале Кельвина и возвращают экземпля­
ры T e m p e r a t u r e . Обратите внимание, что переменная к2 преднамеренно инициализи­
рована значением 0 . 0 _ С , а не 0__С, потому что литерал _ С определен таким образом,
что требует в качестве входного значения lo n g d o u b le , а 0 будет интерпретироваться
как целое число.
T e m p e ra tu re ,

Операторы, которые не могут
быть перегружены
При всей гибкости, которую предоставляет язык C++ в настройке поведения опе­
раторов и классов, он, тем не менее, не разрешает изменять поведение некоторых
операторов, которые в любых обстоятельствах должны работать согласованно. Эти
операторы, которые не могут быть переопределены, представлены в табл. 12.3.

ТАБЛИЦА 12.3. Операторы, которые не могут быть перегружены
или переопределены
______________________________
Оператор

Название
О б р а щ е н и е к члену



О б р а щ е н и е к указателю на член класса
Р азр е ш е н и е области в и д и м о сти

?:

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

size o f

Р азм ер об ъ е кта или типа

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Создавайте столько

Не забывайте, что, если вы не предоставите
собственные копирующий оператор присваи­

операторов, сколько необ­

ходимо для упрощения использования класса,
но не больше.

Маркируйте

операторы преобразования как

e x p l i c i t , чтобы избежать неявных преобра­
зований.

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

Резюме

РЕКОМЕНДУЕТСЯ
Всегда создавайте копирующий

|

379

НЕ РЕКОМЕНДУЕТСЯ
оператор при­

Не забывайте,

что, если вы не предоставите

сваивания (с копирующим конструктором и де­

перемещающий оператор присваивания или

структором) для класса, среди членов которого

перемещающий конструктор, компилятор не

имеется простой указатель.

сгенерирует их автоматически, а использует

При использовании

обычные копирующие оператор присваивания

компилятора, поддержи­
вающего стандарт C++И всегда создавайте
перемещающий

и конструктор.

оператор присваивания (и

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

Резюме
Вы узнали, как создание операторов может существенно упростить использова­
ние вашего класса. При разработке класса, который управляет ресурсами, например
динамическим массивом или строкой, в дополнение к деструктору необходимо пре­
доставить как минимум копирующие конструктор и оператор присваивания. В спо­
могательный класс, который управляет динамическим массивом, стоит снабдить пере­
мещающими конструктором и оператором присваивания, которые гарантируют, что
при использовании временных объектов не будет выполняться затратное глубокое ко­
пирование хранимого ресурса. Наконец вы узнали, что не могут быть переопределены
такие операторы, как .,
и s iz e o f.

Вопросы и ответы
■ Мой класс инкапсулирует динамический массив целых чисел. Какой мини­
мум функций и операторов я должен реализовать?
При разработке такого класса необходимо четко определить его поведение в слу­
чае, когда его экземпляр копируется в другой непосредственно, через присваива­
ние, или косвенно, при передаче в функцию по значению. Как правило, реализуют­
ся копирующие конструктор и оператор присваивания, а также деструктор. Если
вы хотите повысить производительность вашего класса, имеет смысл снабдить его
перемещающими конструктором и оператором присваивания. Для обращения к
хранящимся в массиве элементам имеет смысл перегрузить также оператор индек­
сации o p e r a t o r [].


меня есть экземпляр o b ject класса. Я хочу обеспечить возможность исполь­
зования синтаксиса c o u t « o b j e c t ; . Какой оператор я должен реализовать?

У

Необходимо реализовать оператор преобразования, который позволит интер­
претировать объект вашего класса как тип, который может обработать оператор
s td : :c o u t. Один из способов сделать это — определить оператор char* (), как в
листинге 12.2.

380

ЗАНЯТИЕ 12. Типы операторов и их перегрузка

■ Я хочу создать собственный класс интеллектуального указателя. Какой мини­
мум функций и операторов я должен реализовать?
Интеллектуальный указатель должен позволять использовать себя как обычный
указатель: * p S m a r t P t r или p S m a r t P t r - > F u n c ( ) . Для этого вы должны реализовать
операторы (*) и (-> ). Кроме того, чтобы указатель был интеллектуальным, нуж­
но также позаботиться об автоматическом освобождении ресурсов, предоставив
деструктор, а также точно определиться с тем, как именно осуществляется копи­
рование и присваивание, либо реализуя копирующие конструктор и оператор при­
сваивания, либо запрещая их (объявив их закрытыми).

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Может ли мой оператор индексации [ ] возвращать константный и не констант­
ный типы возвращаемого значения?
const Types operator!](int index);
Types operator!](int index); // Это нормально?

2. Объявляли бы вы копирующий конструктор или копирующий оператор при­
сваивания как p r i v a t e ?
3. Имеет ли смысл определять перемещающие конструктор и оператор присваи­
вания для нашего класса D a t e ?

Упражнения
1.

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

2. Создайте перемещающие конструктор и оператор присваивания для класса
D y n ln t e g e r s , который инкапсулирует динамически выделенный массив в виде
закрытого члена типа i n t * .

ЗАНЯТИЕ 13

Операторы
приведения
Приведение типов (casting) — это механизм, позволяю­
щий программисту изменить интерпретацию объекта компи­
лятором. Приведение не подразумевает изменение самого
объекта, изменяется только его интерпретация. Операторы,
которые изменяют интерпретацию объекта, называются
операторами приведения (casting operator).
На этом занятии...

я Потребность в операторах приведения
■ Почему приведение в стиле С не нравится некоторым
программистам C++
■ Четыре оператора приведения типов C + +
■ Концепции повышающего и понижающего приведения
■ Почему приведение типов C + + — не всегда наилучший
выбор

382

|

ЗАНЯТИЕ 13. Операторы приведения

Потребность в приведении типов
В идеальном строго типизированном мире хорошо продуманных приложений С++
не должно быть никакой потребности в приведении типов и операторах приведения.
Однако мы живем в реальном мире, где программы разрабатывают по частям множес­
тво разных людей и исполнителей, и не редкость необходимость взаимодействия раз­
личных систем. Поэтому часто приходится заставлять компилятор интерпретировать
данные таким образом, чтобы приложение компилировалось без ошибок и корректно
работало.
Рассмотрим реальный пример: хотя большинство компиляторов C++ поддержива­
ют тип b o o l как фундаментальный, множество все еще использующихся библиотек,
которые были созданы годы назад на языке С, его не поддерживают. Эти библиотеки
созданы для компиляторов С и должны полагаться на использование целочисленного
типа для хранения логических данных. Тип b o o l у этих компиляторов выглядит при­
мерно так:
typedef unsigned short BOOL;

Функция, возвращающая логическое значение, должна быть при этом объявлена как
BOOL IsX();

Теперь, если такая библиотека должна использоваться с новым приложением, соз­
данным для последней версии компилятора C++, разработчик должен найти способ
сделать логические данные типа b o o l, воспринимаемые компилятором, доступными
в виде BOOL, понятном библиотеке. Для этого используется приведение типов:
bool result = (bool)IsX(); // Приведение в стиле С

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

Почему приведения в стиле С не нравятся некоторым программистам C++

383

Почему приведения в стиле
С не нравятся некоторым
программистам C++
Безопасность типов (type safety) — один из аргументов, которые приводят про­
граммисты C++, восхищаясь качествами этого языка программирования. Фактически
большинство компиляторов C++ не позволит вам даже такую мелочь:
char* staticStr = "Hello World!";
int* pBuf = staticStr; // Ошибка: нельзя преобразовать char* в int*

...Причем вполне обоснованно!
Современные компиляторы C++ учитывают необходимость обратной совместимос­
ти и поддержки устаревшего кода, а потому автоматически разрешают такой синтак­
сис, как
int* pBuf = (int*)pszString; // Устранение одной проблемы создает другую

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

Операторы приведения C++
Несмотря на недостатки приведения типов отказываться от самой их концепции
нельзя. Во многих ситуациях приведение — единственная возможность решения важ­
ных проблем совместимости. Кроме того, язык C++ предоставляет новый оператор
приведения, предназначенный для случаев наследования, которых не существовало
в языке С.
В C++ имеется четыре оператора приведения:


s t a t ic _ c a s t



d y n a m ic _ c a s t



r e in t e r p r e t _ c a s t



c o n s t_ c a s t

Синтаксис их применения одинаков:

Целевой_тип результат = Приведение {Приводимый_объект);

384

|

ЗАНЯТИЕ 13. Операторы приведения

Использование оператора s t a t ic _ c a s t
Оператор s t a t i c c a s t применяется для преобразования указателей связанных
типов и выполняет явное преобразование стандартных типов данных, которое в про­
тивном случае осуществлялось бы автоматически или неявно. Когда речь идет об
указателях, оператор s t a t i c _ c a s t реализует простую проверку времени компиляции
приводимости указателя к соответствующему типу. Это является усовершенствова­
нием по сравнению с приведением в стиле С, которое позволяет указателю на один
объект быть приведенным к указателю на абсолютно несвязанный тип безо всяких за­
мечаний со стороны компилятора. Используя оператор s t a t i c _ c a s t , указатель можно
привести как к базовому классу, так и к производному, что демонстрируется в следую­
щем примере:
Base* objBase = new Derived!);
Derived* objDer = static_cast(objBase); // OK!
// Класс Unrelated не связан c Base
Unrelated* notRelated = static_cast(objBase); // Ошибка!
// Приведение к несвязанному типу не разрешено

ПРИМЕЧАНИЕ

Приведение указателя на производный тип к указателю на базовый тип на­
зывается восходящим приведением (upcasting) и может быть выполнено
без явного оператора приведения:

Derived objDerived;
Base* pBase = &objDerived; // OK!
Приведение указателя на базовый тип к указателю на производный тип на­
зывается нисходящим приведением (downcasting) и не может быть выпол­
нено без применения явных операторов приведения:

Derived objDerived;
Base* pBase = &objDerived; // Восходящее приведение: OK!
Derived* pDerived = pBase; // Ошибка: нисходящее приведение
// должно быть явным

Обратите внимание, что оператор s t a t i c _ c a s t проверяет только то, что ссылоч­
ные типы связаны. Он не выполняет проверок времени выполнения. Таким образом, с
оператором s t a t i c _ c a s t разработчик вполне может совершить следующую ошибку:
Base* objBase = new Base();
Derived* objDer = static_cast(objBase); // Bee OK

Здесь указатель o b jD e r фактически указывает на частичный объект D e riv e d , по­
скольку фактически объект, на который он указывает, имеет тип B a se. Так как опе­
ратор s t a t i c _ c a s t выполняет только проверку времени компиляции, подтверждая
связанность рассматриваемых типов, и не выполняет проверку времени выполнения,
вызов o b j D e r - > D e r iv e d F u n c t io n () будет скомпилирован, но, вероятно, приведет к
неожиданному поведению во время выполнения.

Операторы приведения C++

385

Помимо помощи в восходящем и нисходящем приведениях, оператор s t a t i c c a s t
во многих случаях может помочь сделать неявные приведения явными и привлечь к
ним внимание разработчика или читателя:
double Pi = 3.14159265;
int num = static_cast(Pi); // Делает неявное приведение явным

В приведенном выше коде выражение num=Pi работало бы не хуже и с тем же успе­
хом. Однако использование оператора s t a t i c _ c a s t привлекает внимание читателя к
характеру преобразования и указывает (тому, кто знает оператор s t a t ic _ c a s t ) , что для
выполнения необходимого преобразования типов компилятор выполнил необходимые
корректировки на основании информации, доступной во время компиляции.
Оператор s t a t i c _ c a s t необходим также при использовании операторов преобра­
зования или конструкторов, которые были объявлены с использованием ключевого
слова e x p l i c i t . Как избежать неявных преобразований с помощью этого ключевого
слова, обсуждается на занятиях 9, “Классы и объекты”, и 12, “Типы операторов и их
перегрузка”.

Использование оператора d y n a m ic _ c a s t
и идентификация типа времени выполнения
Динамическое приведение типов, как следует из его названия, является противопо­
ложностью статического приведения типов и фактически выполняет приведение вре­
мени выполнения. Можно проверить результат выполнения оператора d y n a m ic _ c a s t
и выяснить, была ли успешной попытка динамического приведения типов. Синтаксис
применения оператора d y n a m ic _ c a s t имеет следующий вид:

Целевой_тип* Dest = dynamic_cast (Source);
if (Dest) // Проверка успешности приведения типов,
// прежде чем использовать указатель
Dest->CallFunc();

Например:
Base* objBase = new Derived!);
// Нисходящее приведение
Derived* objDer = dynamic_cast (objBase);
if (objDer) // Проверка успешности приведения
objDer->CallDerivedFunction();

Как показано в приведенном выше коротком примере, имея указатель на объект ба­
зового класса, разработчик может прибегнуть к оператору d y n a m ic _ c a s t, чтобы про­
верить тип целевого объекта, прежде чем перейти к использованию указателя на него.
Обратите внимание, что из фрагмента кода кажется, что целевой объект имеет тип De­
r iv e d . Но это пример лишь для демонстрации. Так бывает не всегда, например когда
указатель типа D e riv e d * передается функции, получающей указатель Base*. Функция
может применить оператор d y n a m ic _ c a s t к переданному указателю типа базового

ЗАНЯТИЕ 13. Операторы приведения

386

класса, чтобы выяснить его тип, а затем выполнить операции, специфические для кон­
кретного типа. Таким образом, оператор d y n a m ic _ c a s t позволяет определить тип во
время выполнения и использовать приведенный указатель, когда это безопасно. Лис­
тинг 13.1 использует уже знакомую нам иерархию классов Tuna и C a rp , связанных с
базовым классом F is h , где функция D e t e c t F is h T y p e () динамически выясняет, явля­
ется ли указатель F is h * на самом деле указателем Tuna* или C a rp * .

ПРИМЕЧАНИЕ

Данный механизм идентификации типа объекта во время выполнения
называется идентификацией типа времени выполнения (runtime type
identification - RTTI).

ЛИСТИНГ 13.1. Использование динамического приведения типов для
выяснения, является ли объект класса F i s h объектом класса Tuna или C a rp
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include
using namespace std;
class Fish
{
public:
virtual
{
cout «
}

voidSwim()
"Рыба плавает в воде" «

endl;

10 :
11:
// Базовый класс должен иметь виртуальный деструктор
12:
virtual -Fish() {}
13: };
14:
15: class Tuna: public Fish
16: {
17:
public:
18:
void Swim()
19:
{
20:
cout « "Тунец быстро плавает в море" « endl;

21 :

}

22 :
23:
void BecomeDinner()
24:
{
25:
cout « "Из тунца готовят суши" « endl;
26:
}
27: };
28:
29: class Carp: public Fish
30: {
31:
public:
32:
void Swim()
33:
{
cout « "Карп медленно плавает в озере" «
34

endl;

Операторы приведения C++
35:
}
36:
37:
void Talk()
38:
{
39:
cout « "Карп разговаривает с карпом!" « endl;
40:
}
41: };
42:
43: void DetectFishType(Fish* objFish)
44: {
45:
Tuna* objTuna = dynamic_cast (objFish);
46:
if (objTuna) // Проверка успешности приведения
47:
{
48:
cout « "Обнаружен тунец: " « endl;
4 9:
obj Tuna->BecomeDinner();
50:
}
51:
52:
Carp* objCarp = dynamic_cast (objFish);
53:
if(objCarp)
54:
{
55:
cout « "Обнаружен карп: " « endl;
56:
objCarp->Talk();
57:
)
58:
59:
cout « "Проверка вызовом Fish::Swim: " « endl;
60:
objFish->Swim(); // Вызов виртуальной функции Swim
61: )
62:
63: int main()
64: {
65:
Carp myLunch;
66:
Tuna myDinner;
67:
68:
DetectFishType(SmyDinner);
69:
cout « endl;
70:
DetectFishType(&myLunch);
71:
72:
return 0;
73:}

Результат
Обнаружен тунец:
Из тунца готовят суши
Проверка вызовом Fish::Swim:
Тунец быстро плавает в море
Обнаружен карп:
Карп разговаривает с карпом!
Проверка вызовом Fish::Swim:
Карп медленно плавает в озере

387

388

|

ЗАНЯТИЕ 13. Операторы приведения

Анализ
В этом примере используется иерархия классов Tuna и C a rp , производных от клас­
са F is h . С дидактическими целями эти два производных класса не только реализуют
виртуальную функцию S w im (), но и содержат функции, специфичные для каждо­
го типа, а именно — T u n a : -.Becom eD inner () и C a r p : : T a l k (). Особенностью дан­
ного примера является то, что, имея указатель на экземпляр базового класса F is h * ,
вы можете динамически обнаружить, не указывает ли он на объект класса Tuna или
C a rp . Такое динамическое обнаружение, или идентификация типа времени выполне­
ния, осуществляется в функции D e t e c t F is h T y p e ( ) , определенной в строках 43-61.
В строке 45 оператор d y n a m ic _ c a s t используется для проверки входного указателя
базового класса типа F is h * , не является ли он фактически указателем на тип Tuna*.
Если этот указатель F is h * указывает на тип Tuna, оператор возвращает корректный
адрес, в противном случае — значение n u l l p t r . Следовательно, всегда должна прове­
ряться корректность результата выполнения оператора d y n a m ic _ c a s t. После провер­
ки успешности в строке 46 вы знаете, что указатель указывает на допустимый объект
класса Tuna и его можно использовать для вызова функции T u n a : : B ecom eD inner ( ) ,
как показано в строке 49. В случае, если передан указатель на объект C a rp , вы ис­
пользуете его для вызова функции C a r p : : T a l k ( ), как показано в строке 56. Перед
выходом функция D e t e c t F i s h T y p e () осущ ествляет проверку типа, вызвав ме­
тод F i s h : :S w im () , который, будучи виртуальным, переадресовывает вызов методу
Swim (), реализованному в классе Tuna или C a rp соответственно.

ВНИМАНИЕ!

Возвращаемое значение оператора d y n a m ic _ c a s t всегда следует про­
верять на корректность. Если приведение неудачно, возвращается значе­
ние n u l l p t r .

Использование оператора r e in t e r p r e t _ c a s t
Оператор приведения C++ r e i n t e r p r e t _ c a s t ближе всех к приведению в стиле
С. Он позволяет разработчику приводить один тип объекта к другому независимо от
того, связаны ли их типы:
Base * objBase = new Base();
Unrelated * notRelated = reinterpret_cast(objBase);
// Код компилируется, но это плохой стиль!

Такое приведение фактически заставляет компилятор считать приемлемыми си­
туации, которые оператор s t a t i c _ c a s t не пропустил бы. Оно находит применение в
некоторых низкоуровневых приложениях (например, таких, как драйверы), в которых
данные должны быть преобразованы в простой тип, с которым может работать API (на­
пример, ряд функций API работает только с байтовыми потоками, т.е. u n sign ed char*):
SomeClass* object = new SomeClassO;
// Необходимо передать объект как поток байтов.
unsigned char* bytesForAPI = reinterpret_cast (object);

Операторы приведения C++

|

389

Приведение, использованное в показанном фрагменте, не изменяет бинарное пред­
ставление исходного объекта и фактически обманывает компилятор, разрешая разработ­
чику выбирать отдельные байты, содержащиеся в объекте типа Som eClass. Поскольку
никакой другой оператор приведения C++ не допускает такого нарушения безопасно­
сти типов, оператор r e i n t e r p r e t _ c a s t является последним, небезопасным средством.

ВНИМАНИЕ!

По возможности воздержитесь от использования оператора r e i n t e r p re t_ _ ca st в ваших приложениях, поскольку он позволяет заставить ком­
пилятор рассматривать тип X как несвязанный с ним тип Y, что плохо и с
точки зрения проектирования, и с точки зрения реализации.

Использование оператора c o n s t _ c a s t
Оператор c o n s t _ c a s t позволяет отключать модификатор c o n s t доступа к объекту.
Если вы задаетесь вопросом, зачем это приведение нужно вообще, то вы, вероятно,
правы. В идеальной ситуации, когда разработчики пишут свои классы правильно,
они не забывают использовать ключевое слово c o n s t и применяют его в правильных
местах. На практике все, к сожалению, совсем не так, и код наподобие следующего
весьма распространен:
class SomeClass

{
public:

//

. . .

void DisplayMembers(); // Хотя функция вывода должна
// быть константной

};
Если при этом вы создаете такую функцию, как показано ниже, вы сталкиваетесь
с запретом компилятора:
void DisplayAllData(const SomeClass& object)

{
object.DisplayMembers(); // Отказ компиляции.
// Причина отказа: вызов неконстантного члена
// класса с использованием константной ссылки

Вы совершенно правы, передавая o b j e c t как константную ссылку. В конце концов,
функция отображения предназначена только для чтения и не должна позволять вызы­
вать неконстантные функции-члены, способные изменить состояние объекта. Однако
реализация функции D is p la y M e m b e rs (), которая также должна быть константой, к
сожалению, таковой не является. Если класс S o m e C la ss принадлежит вам и его ис­
ходный код находится под вашим контролем, вы можете внести корректирующие
изменения в функцию D is p la y M e m b e rs (). Но в большинстве случаев она входит в
библиотеку стороннего производителя, и внести в нее изменения вы не в состоянии.
В таких ситуациях на выручку приходит оператор c o n s t c a s t .

390

|

ЗАНЯТИЕ 13. Операторы приведения

Синтаксис его применения для функции D isp la y M e m b e rs () следующий:
void DisplayAllData(const SomeClass& mData)

{
SomeClass& refData = const_cast(mData);
refData.DisplayMembers(); // Теперь разрешено!

}
Обратите внимание на то, что применение оператора c o n s t c a s t для вызова не­
константных функций должно быть последним средством. Имейте в виду, что исполь­
зование оператора c o n s t _ c a s t для изменения константного объекта может привести
к неопределенному поведению.
Обратите внимание на то, что оператор c o n s t _ c a s t применяется и с указателями:
void DisplayAllData(const SomeClass* data)

{
// data->DisplayMembers();

// Ошибка: попытка вызвать
// неконстантную функцию!
SomeClass* pCastedData = const_cast (data);
pCastedData->DisplayMembers(); // Разрешено!

Проблемы с операторами
приведения C++
Не все довольны всеми операторами приведения типов C++, даже те, кому они
нравятся. Причины недовольства самые разнообразные — от слишком громоздкого и
интуитивно непонятного синтаксиса до избыточности.
Просто сравните следующие фрагменты кода:
double Pi = 3.14159265;
// Приведение в стиле C++: static_cast
int num = static_cast(Pi); // Результат: num равен 3
// Приведение в стиле С
int num2 = (int)Pi;

// Результат: num2 равен 3

// Оставить приведение типов компилятору
int num3 = Pi;
// Результат: питЗ равен 3

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

П р о б л е м ы с о п е р а т о р а м и п р и в е д е н и я C++

|

391

Аналогично другой случай применения оператора s t a t i c c a s t также вполне об­
рабатывается приведениями в стиле С, которые, по общему мнению, выглядят куда
проще:
// Использование static_cast
Derived* objDer = static_cast (objBase);
/ / Н о этот код работает точно так же:
Derived* objDerSimple = (Derived*)objBase;

Таким образом, преимущества использования оператора s t a t i c _ c a s t зачастую
омрачаются неуклюжестью его синтаксиса.
Рассмотрим другие операторы. Оператор r e i n t e r p r e t _ c a s t позволяет вам все же
скомпилировать код, в котором отказывается работать оператор s t a t i c _ c a s t ; точ­
но так же применяется оператор c o n s t _ c a s t , изменяя модификатор доступа c o n s t .
Таким образом, операторов приведения C++, кроме d y n a m ic _ c a s t , вполне можно
избежать в современных приложениях C++. Применение других операторов приве­
дения становится уместным только тогда, когда требуется использовать устаревшие
приложения. В таких случаях предпочтительно использование приведений в стиле С,
а использование операторов приведения C++ зачастую является делом вкуса. Однако
лучше всего максимально избегать приведений, а когда это не удается, следует по
крайней мере ясно понимать, что при этомпроисходит.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Помните, что приведение типа D e riv e d * к

Не забывайте проверять корректность ука­

Base* называется восходящим приведением,

зателя,

и оно безопасно.

d y n a m ic _ c a s t.

полученного

с

помощью

оператора

Помните, что приведение типа Base* непо­

Не проектируйте свои приложения так, чтобы

средственно к типу D e riv e d * называется нис­

их работоспособность опиралась на примене­

ходящим приведением и может быть небезопас­

ние возможностей RTTI с использованием опе­

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

ратора d y n a m ic _ c a s t.

d y n a m ic _ c a s t и не проверяете результат его
применения.
Помните, что цель создания иерархии насле­
дования обычно заключается в наличии вирту­
альных функций, при вызове которых с исполь­
зованием указателей базового класса можно
обеспечить доступ к их версии в производном
классе.

392

|

ЗАНЯТИЕ 13. Операторы приведения

Резюме
На сегодняшнем занятии рассматривались различные операторы приведения C++,
аргументы “за” их применение и “против”. Вы также узнали, что применения приве­
дений в общем случае следует избегать.

Вопросы и ответы
■ Нормально ли изменять содержимое константного объекта при приведении
типа указателя или ссылки на него с использованием оператора const_cast?
В большинстве случаев, определенно, нет. Результат такой операции непредсказуем
и, определенно, нежелателен.

■ Мне нужен указатель Bird*, а имеется Dog*. Компилятор не позволяет мне
использовать указатель на объект Dog в качестве Bird*. Однако, когда я ис­
пользую оператор r e in te r p r e t_ c a st для приведения типа Dog* к типу Bird*,
компилятор не жалуется, и, кажется, я могу использовать этот указатель
для вызова функции-члена Fl y( ) класса B ird. Все ли у меня в порядке?
И вновь, определенно, нет. Оператор r e i n t e r p r e t _ c a s t изменяет только интер­
претацию указателя, но не объект, на который он указывает (он все еще остается
объектом класса Dog). Вызов функции F ly () объекта класса Dog не даст ожидае­
мых результатов и может привести к неработоспособности приложения.

■ У меня есть объект класса Derived и указатель на него objBase, типа Base*.
Я уверен, что указатель objBase указывает на объект класса Derived. Должен
ли я использовать оператор dynamic_cast?
Если вы абсолютно уверены, что типом объекта, на который он указывает, является
D erived , можете сэкономить ресурсы исполняющей среды и использовать опера­
тор s t a t i c _ c a s t .

■ Язык C + + предоставляет несколько операторов приведения, но автор настоя­
тельно советует их не использовать. Почему?
Вы храните дома аспирин, но не едите его ложкой каждый день только потому, что
он есть, правда же? Используйте их, но только когда это на самом деле необходимо
и оправданно.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления полу­
ченных знаний, а также упражнения, которые помогут применить на практике по­
лученные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выпол­
нить задания, а потом сверьте полученные результаты с ответами в приложении Д,
“Ответы”. Если остались неясными хотя бы некоторые из предложенных ниже во­
просов, не приступайте к изучению материала следую щ его занятия.

Коллоквиум

|

393

Контрольные вопросы
1. У вас есть указатель ob j Base на объект базового класса. Какой оператор приве­
дения следует использовать, чтобы выяснить, является ли его типом D e r iv e d l
или D e riv e d 2 ?
2. У вас есть константная ссылка на объект, и вы пытаетесь вызвать открытую
функцию-член, написанную вами. Компилятор не позволяет сделать это, по­
скольку рассматриваемая функция — неконстантный член класса. Как вы по­
ступите — исправите функцию или используете оператор c o n s t _ c a s t ?
3. Оператор r e i n t e r p r e t c a s t для приведения следует использовать только тог­
да, когда оператор s t a t i c _ c a s t не работает, но при этом известно, что данное
приведение необходимо и безопасно. Верно ли данное утверждение?
4. Верно ли, что большинство преобразований, выполняемых оператором s t a t i c c a s t , в особенности между простыми типами данных, компилятор C++ спо­
собен выполнить автоматически?

Упражнения
1. Отладка. Что неправильно в следующем коде?
void DoSomething(Base* objBase)

{
Derived* objDer = dynamic_cast (objBase);
obj Der->DerivedClassMethod();

}
2.

У вас есть указатель o b j F is h * , указывающий на объект класса Tuna.
Fish* objFish = new Tuna;
Tuna* pTuna = obj Fish;

Какой оператор приведения следует использовать, чтобы получить указатель
Tuna* на этот объект типа Tuna? Продемонстрируйте использующий его код.

ЗАНЯТИЕ 14

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

Введение в препроцессор

■ Ключевое слово t d e f i n e и макросы
■ Введение в шаблоны
■ Как писать шаблоны функций и классов
■ Различие между макросами и шаблонами
■ Как использовать ключевое слово С + + 1 1 s t a t i c _ a s s e r t
для выполнения проверок времени компиляции

396

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

Препроцессор и компилятор
Впервые о препроцессоре вы узнали на занятии 2, “Структура программы на C++”.
Препроцессор (preprocessor), как свидетельствует его название, запускается перед
компилятором. Другими словами, на основании полученных от программиста указа­
ний препроцессор фактически решает, как будет выглядеть компилируемый исходный
текст. Все директивы препроцессора (preprocessor directive) начинаются со знака #,
например:
// Указание вставить содержимое заголовочного файла iostream
#include
// Определить макрос константы
#define ARRAY_LENGTH 25
int MyNumbers[ARRAY_LENGTH]; // Массив из 25 целых чисел
// Определить макрофункцию
#define SQUARE(х) ((х) * (х))
int TwentyFive = SQUARE(5);

На этом занятии рассматриваются два типа директив препроцессора, показанных
в представленном выше фрагменте кода; в одном случае директива t d e f i n e исполь­
зуется для определения константы, а в другом — для определения макрофункции.
Обе эти директивы, независимо от их роли, фактически указывают препроцессору
заменить каждый экземпляр макроса ( ARRAY_LENGTН или SQUARE) значением, которое
они определяют.

ПРИМЕЧАНИЕ

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

Использование #def±ne
для определения констант
Синтаксис применения директивы td e f in e для определения константы очень прост:
#define Идентификатор Значение

Например, константа ARRAY LENGTH, заменяемая значением 25, выше была объяв­
лена следующим образом:
#define ARRAY_LENGTH 25

Теперь этот идентификатор заменяется текстом 25 везде, где препроцессор его
встретит:

Использование #define для определения констант

397

int numbers[ARRAY_LENGTH] = {0};
double radiuses[ARRAY_LENGTH] = {0.0};
std::string names[ARRAY_LENGTH];

После запуска препроцессора эти три строки будут переданы компилятору в таком
виде:
int numbers[25] = {0};
double radiuses[25] = {0.0};
std::string names[25];

//Массив из 25 целых чисел
//Массив из 25 чисел типа double
//Массив из 25 std::strings

Замена применима к любому разделу кода, включая, например, цикл for, как по­
казано далее:
for(int index = 0; index < ARRAY_LENGTH; ++index)
numbers[index] = index;

Этот цикл for компилятор видит таким:
for(int index = 0; index < 25; ++index)
numbers[index] = index;

Чтобы увидеть все это в действии, рассмотрим листинг 14.1.

ЛИСТИНГ 14.1, Объявление и использование макросов, определяющих константы
0: tinclude
1: #include
2: using namespace std;
3:
4:
#define ARRAY_LENGTH
25
5:
tdefine PI
3.1416
6: #define MY_D0UBLE
double
7:
#define FAV_WHISKY "Jack Daniels"

8:
9: int main()

10: {
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

int numbers[ARRAY_LENGTH] = {Obcout « "Длина массива: "«sizeof (numbers)/sizeof (int) «endl;
cout « "Введите радиус:
";
MY_D0UBLE radius = 0;
cin » radius;
cout « "Площадь: " « PI * radius * radius «
string favoriteWhisky(FAV_WHISKY);
cout « "Предпочитаю: " « FAV_WHISKY «

21
22

23

return 0;

}

endl;

endl;

398

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

Результат
Длина массива: 25
Введите радиус: 2 . 1 5 6 9
Площадь: 14.7154
Предпочитаю: Jack Daniels

Анализ
ARRAY_LENGTH, PI, MY_DOUBLE и FAV_WHISKY являются четырьмя макроконстанта­
ми, определенными в строках 4 - 7 соответственно. Как можно заметить, первая ис­
пользуется при определении длины массива в строке 11 (которая проверяется с по­
мощью оператора s i z e o f () в строке 12). MY_DOUBLE используется при объявлении
переменной r a d iu s типа d o u b le в строке 15, а константа PI используется при вы­
числении площади круга в строке 17. И наконец константа FAV_WHISKY используется
при инициализации объекта класса s t d : : s t r i n g в строке 19 и непосредственно для
вывода в поток c o u t (строка 20). В се эти случаи демонстрируют, что препроцессор
осуществляет простую текстовую замену.
У такой “тупой” текстовой замены, которая нашла применение в листинге 14.1,
есть и недостатки.

СОВЕТ

Поскольку препроцессор делает лишь простую текстовую подстановку, он
не проверяет корректность такой подстановки (что делает компилятор). Вы
могли бы определить константу FAV_WHISKY в строке 7 листинга 14.1 так:

#define FAVJWHISKY 42 // "Jack Daniels"
Это могло бы закончиться ошибкой компиляции в строке 19 при создании
экземпляра класса s t d : : s t r i n g , но при ее отсутствии компилятор про­
должил бы работу и вывел следующий текст:

Предпочитаю: 42
Это, конечно, не имело бы смысла, но важнее всего то, что эта бессмысли­
ца осталась бы необнаруженной компилятором. Кроме того, у вас нет осо­
бого контроля над определением константы PI: какой у нее тип или

flo a t?

double

Ответ: ни тот, ни другой. PI для препроцессора - только текст,

заменяемый текстом 3 .1 4 1 6 . Ни о каком типе данных нет и речи.
Константы лучше определять, используя ключевое слово

const

с типами

данных. Так намного лучше:

const int ARRAY_LENGTH = 25;
const double PI = 3.1416;
const char* FAV_WHISKY = "Jack Daniels";
typedef double MY_DOUBLE; // typedef для псевдонима типа

Использование #define для определения констант

399

И спользование м акроса для защ иты
от м нож ественного включения
Программисты C++, как правило, объявляют свои классы и функции в файлах с
расширением . h, называемых заголовочными файлами (header file). Соответствующие
функции определяются в файлах с расширением . с р р , в которые включают заголовоч­
ные файлы, используя директиву препроцессора # i n c l u d e .
Если один заголовочный файл (назовем его c l a s s l .h ) объявляет класс, членом которого
является другой класс, объявленный в заголовочном файле c l a s s 2 .h , то файл c l a s s l .h
должен включать файл c l a s s 2 .h . В случае сложного проекта возможна ситуация, ког­
да заголовочный файл c l a s s 2 . h требует включения заголовочного файла c l a s s l . h !
Но для препроцессора два заголовочных файла, которые включают один другой,
являются проблемой рекурсивного характера. Чтобы избежать этой проблемы, можно
использовать макрос вместе с директивами препроцессора # i f n d e f и # e n d if .
Заголовочный файл h e a d e r 1 .h , включающий заголовочный файл h e a d e r 2 .h , вы­
глядит так:
#ifndef HEADER1_H_
tdefine HEADER1_H_

// Защита от множественного включения:
// препроцессор будет читать эту и
// последующие строки только один раз

#include
class Classl
{
// Члены класса

};
#endif // Конец headerl.h

Заголовочный файл h e a d e r 2 .h выглядит похоже, но с другим макроопределением,
и включает заголовочный файл h e a d e r 1 .h :
#ifndef HEADER2_H_ // Защита от множественного включения
#define HEADER2_H_
#include
class Class2
{
// Члены класса

};
#endif // Конец header2.h

ПРИМЕЧАНИЕ

Директиву # i f n d e f можно прочитать как “если не определено”. Это ди­
ректива условного выражения, требующая от препроцессора продолжить
выполнение, только если идентификатор не был определен.
Директива # e n d if отмечает конец этой условной инструкции препроцессора.

400

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

Таким образом , если препроцессор встречает первым заголовочный файл
h e a d e r l . h , он выполняет директиву # i f n d e f и, заметив, что идентификатор
H E A D E R 1 _ H _ не был определен, продолжает выполнение. Первая строка после дирек­
тивы # i f n d e f определяет идентификатор H EAD ER 1_Н_, гарантируя, что вторая попыт­
ка препроцессора загрузить этот файл закончится первой же строкой, содержащей
директиву # i f n d e f , поскольку теперь это условие будет ложным. То же самое спра­
ведливо и для заголовочного файла h e a d e r 2 .h . Этот простой механизм, возможно, —
одна из наиболее часто используемых возможностей макросов при программировании
на языке C++.

Использование директивы #define
для написания макрофункции
Способность препроцессора к простой замене текстовых элементов, идентифици­
руемых макросом, позволяет писать простые макрофункции, например:
#define SQUARE(х) ((х) * (х))

Эта функция вычисляет квадрат числа. Аналогично макрос, вычисляющий пло­
щадь круга, выглядит следующим образом:
#define PI 3.1416
#define AREA_CIRCLE(г) (PI*(г)*(г))

Макрофункции (macro function) нередко используются для подобных очень прос­
тых вычислений. Они предоставляют то преимущество, что обычный вызов функций,
которым они выглядят, раскрывается в код, встраивающийся в исходный текст перед
компиляцией, а следовательно, макрофункции могут в определенных ситуациях по­
высить производительность кода. Листинг 14.2 демонстрирует использование этих
макрофункций.
ЛИСТИНГ 14.2. Использование макрофункций, вычисляющих квадрат

числа, площадь круга, а также наибольшее и наименьшее из двух чисел_______________
0:
1:
2:
3:
4:
5:
6:
7:
8:

#include
#include
using namespace std;
#define
idefine
#define
#define
#define

SQUARE(x) ((x) * (x))
PI 3.1416
AREA_CIRCLE(r) (PI*(r)* (r))
MAX(a, b) (((a) > (b)) ? (a) : (b))
MIN(a, b) (((a) < (b)) ? (a) : (b))

9 :

10: int main()

11: {
12:
13:

cout « "Введите целое число:
int num = 0;

Использование директивы #define для написания макрофункции
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31: }

cin »

|

401

num;

cout «
cout «
cout «

"SQUARE (" « num « ") = " « SQUARE (num)
« endl;
"Площадь круга с радиусом " « num « "равна:
";
AREA_CIRCLE(num) « endl;

cout « "Введите другое целое число: ";
int num2 = 0;
cin » num2;
cout «
cout «

"MIN(" « num « ", " «
MIN(num, num2) « endl;

cout « "MAX(" « num «
cout « MAX (num, num2) «

", " «
endl;

num2 «

num2 «

") = ";

") = ";

return 0;

Результат
Введите целое число: 36
SQUARE(36) = 1296
Площадь круга с радиусом 36 равна: 4071.51
Введите другое целое число: - 1 0 1
MIN(36, -101) = -101
МАХ(36, -101) = 36

Анализ
Строки 4 -8 содержат несколько вспомогательных макрофункций, которые возвра­
щают квадрат числа, площадь круга, а также наибольшее и наименьшее из двух чисел.
Обратите внимание, что функция A R EA _ _ C IR C LE в строке 6 вычисляет площадь с ис­
пользованием константы P I , свидетельствуя, таким образом, что один макрос может
повторно использовать другой макрос. В конце концов, это всего лишь команды пре­
процессора для простой замены одного текста другим. Давайте рассмотрим строку 25,
в которой используется макрофункция MIN:
cout «

MIN (num, num2) «

endl;

После раскрытия макроса эта строка передается компилятору в следующем виде:
cout «

(((num) < (num2)) ? (num) : (num2)) «

ВНИМАНИЕ!

endl;

Макрофункции ничего не знают о типах, а потому могут быть опасны.
Например, макрофункция A R E A _ C IR C L E в идеале должна возвращать
значение типа d o u b le независимо от того, какое значение радиуса ей
передается.

402

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

Зачем все эти скобки?
Еще раз взгляните на макрофункцию вычисления площади круга:
#define AREA_CIRCLE(г) (PI*(г)*(г))

У этого выражения странный синтаксис с множеством скобок. Сравните его с функ­
цией Area () из листинга 7.1 занятия 7, “Организация кода с помощью функций”.
// Определения функций (реализации)
double Area(double radius)

{
return Pi * radius * radius; // Никаких скобок

}
Так почему же для макроса мы так усердствуем в расставлении скобок, если та же
формула в функции обходится без них? Причина — в способе обработки макроса пре­
процессором, т.е. в механизме текстовой подстановки.
Рассмотрим макрос без множества скобок:
#define AREA_CIRCLE(г) (PI*r*r)

Что произойдет при следующем использовании этого макроса?
cout «

AREA_CIRCLE (4+6);

Он будет развернут препроцессором в такой код:
cout «

(Р1*4+6*4+6); // Совсем не то же, что и Р1*10*10

Таким образом, с учетом приоритета операций, согласно которым умножение вы­
полняется до сложения, компилятор фактически вычисляет площадь так:
cout «

(Р1*4+24+6); // 42.5664 (что явно неправильно)

Без круглых скобок преобразование текста привело к искажению логики програм­
мы! Применение круглых скобок позволяет избежать этой проблемы:
#define AREA_CIRCLE(г) (PI*(г)* (г))
cout « AREA_CIRCLE (4+6);

Выражение после подстановки воспринимается компилятором как следующее:
cout «

(PI*(4+6)*(4+6)); // PI*10*10, как и ожидалось

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

Использование макроса a ssert
для проверки выражений
Хотя, конечно, следует тестировать каждый путь выполнения кода непосредственно
после его написания, это зачастую оказывается физически невозможным. Но можно
хотя бы вставить в код проверки, тестирующие значения выражений или переменных.

И с п о л ь з о в а н и е д и р е к т и в ы # d e fin e д л я н а п и с а н и я м а к р о ф у н к ц и и

|

403

Макрос a s s e r t позволяет выполнять такие проверки. Чтобы использовать макрос
a s s e r t , необходимо включить в код заголовочный файл a s s e r t .h ; синтаксис же ис­
пользования этого макроса следующий:
assert

(вы р а ж ен и е , во звр а щ а ю щ е е

tr u e или f a l s e ) ;

Вот простой пример использования макроса a s s e r t (), проверяющего содержимое
указателя:
#include
int m a i n ()

{
char* sayHello = new char[25];
assert(sayHello != nullptr); // Сообщить о нулевом указателе
// Прочий код
delete[] sayHello;
return 0;

}
Макрос a s s e r t () гарантирует вывод уведомления, если указатель окажется ну­
левым. Для проверки я инициализировал указатель s a y H e l l o значением n u l l p t r и
при запуске в режиме отладки Visual Studio немедленно получил на экране окно, по­
казанное на рис. 14.1.

Р И С . 1 4 .1 . М а к р о с a s s e r t о б н а р у ж и л н е к о р р е к т н ы й у к а з а т е л ь

Таким образом, макрос a s s e r t (), реализованный в отладочном режиме Microsoft
Visual Studio, позволяет щелкнуть на кнопке Retry (Повторить) и вернуться в при­
ложение, а стек вызовов укажет строку, приведшую к нарушению условия макроса
a s s e r t . Это делает макрос a s s e r t () весьма удобным средством отладки; например, с
его помощью можно проверять входные параметры функций. Это настоятельно реко­
мендуется и позволяет улучшить качество вашего кода.

404

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

ПРИМЕЧАНИЕ

В производственной версии макрос a s s e r t () обычно отключен и выво­
дит сообщения об ошибках и иную информацию только в отладочном режи­
ме большинства сред разработки.
Кроме того, некоторые среды реализуют его как функцию, а не как макрос.

ВНИМАНИЕ!

Поскольку макрос a s s e r t не работает в производственных версиях, те­
сты, критически важные для функционирования приложения (например,
возвращаемое значение оператора d y n a m ic _ c a s t ) , следует выполнять
с использованием инструкции i f . Макрос a s s e r t позволяет обнаружи­
вать проблемы, но не является заменой проверки значения указателя, не­
обходимой в коде.

Преимущества и недостатки
использования макрофункций
Макросы позволяют повторно использовать некоторые вспомогательные функции
независимо от типа используемых переменных. Вернемся к следующей строке из ли­
стинга 14.2:
# d e fin e MIN (а, b)

(((а)

< (Ь )) ?

(а)

:

(Ь ))

Эту макрофункцию M IN можно использовать для целых чисел:
cout «

M IN (25,

101)

«

e n d l;

Но ее же можно использовать для типа d o u b le :
cout «

M I N (0.1,

0.2)

«

e n d l;

Обратите внимание, что, если бы функция M IN () была обычной функцией, вам
пришлось бы создать два ее варианта: M IN IN T (), получающий параметры типа i n t и
возвращающий тип i n t , и M IN J0 O U B L E ( ) , делающий то же самое, но с типом d o u b le .
Такая оптимизация и сокращение количества кода представляют собой определенное
преимущество и соблазняют некоторых программистов на использование макросов
для определения простых вспомогательных функций. Эти макрофункции раскрыва­
ются и встраиваются в код перед компиляцией, а следовательно, производительность
простого макроса выше, чем обычного вызова функции, решающего ту же задачу. Это
связано с тем, что вызов функции требует создания стека вызовов, передачи аргумен­
тов и так далее, так что дополнительные затраты зачастую отнимают больше процес­
сорного времени, чем работа самой функции M IN.
Несмотря на все эти преимущества макросы представляют серьезную проблему:
они не поддерживают никаких форм безопасности типов. Если этого недостаточно, то
учтите, что отладка макроса — также весьма непростое дело.
Если необходимо создание обобщенных функций, которые не зависят от типа, но
при этом поддерживают безопасность типов, вместо макрофункции лучше использо­
вать шаблон функции. Если необходимо повышение производительности, объявите
свою функцию встраиваемой ( i n l i n e ) .

Введение в шаблоны

405

Вы уже познакомились со встраиваемыми функциями и использованием ключевого
слова i n l i n e в листинге 7.10 на занятии 7, “Организация кода с помощью функций”.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Создавайте собственные макрофункции по воз­

Не забывайте заключать

можности реже.

ременную в определении макрофункции.

Используйте по возможности

константы вместо

Не забывайте использовать в ваших заголовоч­
ных файлах защиту от множественного включе­

макросов.

Помните,

в скобки каждую пе­

что макросы небезопасны с точки

ния, используя директивы t i f n d e f , t d e f i n e

зрения типов, а препроцессор не выполняет

и # e n d if .

никаких проверок типов.

Не забывайте

снабжать свой код макросами

a s s e r t () - в окончательной версии они не
выполняют никаких действий, но облегчают от­
ладку и тестирование вашего кода в процессе
разработки.

Теперь пришло время изучения практики обобщ енного программирования с ис­
пользованием шаблонов!

Введение в шаблоны
Шаблоны (template) — это, вероятно, одно из самых мощных средств языка C++,
на которые зачастую не обращают внимания или не понимают. Прежде чем заняться
этой темой, сначала посмотрим, как определяется термин “шаблон” в словаре В еб­
стера.
Функция: существительное
Этимология: вероятно, от французского tem plet, уменьшительное от tem ple (храм), часть ткацкого
станка, вероятно, от латинского templum
Дата: 1677
1. Короткий элемент или блок, располагающийся горизонтально в стене под балкой, чтобы рас­
пределить ее вес или давление (как над дверью).
2. (1). Лекало, выкройка или шаблон (как тонкая пластина или лист), используемые как направ­
ляющие при вырезании детали сложной формы; (2) а: молекула (как ДНК), которая служит шабло­
ном для создания другой макромолекулы (как РНК); Ь: перекрытие.
3. Нечто устанавливающее или служащее образцом.

Последнее определение, вероятно, ближе всего к интерпретации слова шаблон при
использовании в языке C++. Шаблоны в языке C++ позволяют определить действие,
которое можно применить к объектам разных типов. Это звучало бы зловеще близко
к тому, что позволяет делать макрос (посмотрите на простой макрос МАХ, который

406

|

ЗАНЯТИЕ 14. В ведение в макросы и шаблоны

определял большее из двух чисел), если бы не тот факт, что в отличие от макросов
шаблоны обеспечивают безопасность типов.

Синтаксис объявления шаблона
Объявление шаблона начинается с ключевого слова te m p la te , сопровождаемого
списком параметров типа (type parameter). Формат объявления таков:
template

объявление шаблона функции / к л а сса ..
Ключевое слово te m p la te отмечает начало объявления шаблона, а далее следу­
ет список параметров шаблона. Этот список параметров содержит ключевое слово
typenam e, которое определяет параметр шаблона, делая его заполнителем для типа
объекта, для которого создается экземпляр шаблона.
template ctypename Tl, typename T2 = Tl>
bool TempiateFunction(const T1& paraml, const T2& param2);
// Шаблон класса
template ctypename Tl, typename T2 = Tl>
class Template
{
private:
Tl member1;
T2 member2;
public:
Tl GetObjlO {return memberl; }
// ... Другие члены

};
Здесь представлены шаблон функции и шаблон класса, каждый из которых полу­
чает два параметра шаблона Т1 и Т2, где параметр Т2 имеет заданный по умолчанию
тип Т1.

Типы объявлений шаблонов
Объявление шаблона может быть следующим:
■ объявление или определение функции;
■ объявление или определение класса;
■ определение функции-члена или класса-члена шаблона класса;
■ определение статической переменной-члена шаблона класса;
■ определение статической переменной-члена класса, вложенного в шаблон класса;
■ определение шаблона-члена класса или шаблона класса.

Введение в шаблоны

407

Шаблонные функции
Вообразите функцию, которая сама приспосабливается к параметрам различных
типов. Такая функция вполне возможна — при использовании синтаксиса шаблона!
Давайте проанализируем типичное объявление шаблона, который является эквивален­
том обсуждавшегося ранее макроса МАХ, который возвращает больший из двух пере­
данных параметров:
template
const objTypeS GetMax(const objTypeS valuel,
const objTypeS value2)

{
if (valuel > value2)
return valuel;
else
return value2;

}
Вот как выглядят примеры его применения:
int numl = 25;
int num2 = 40;
int maxVal = GetMax(numl, num2);
double doublel = 1.1;
double double2 = 1.001;
double MaxDVal = GetMax(doublel, double2);

Обратите внимание на фрагмент < in t > , использованный в вызове функции G e t M ax (). Фактически это определение параметра шаблона o b j T y p e как типа i n t . При­
веденный выше код заставляет компилятор создать две версии функции G e tM a x ( ) ,
которые можно представить так:
const ints GetMax(const ints valuel, const int& value2)

{
//...
const doubles GetMax(const doubles valuel, const doubles value2)

{
//

. . .

}
Однако в действительности шаблонные функции не обязательно нуждаются в со­
ответствующем спецификаторе типа. Так, отлично сработает следующий вызов:
int MaxValue = GetMax(numl, num2);

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

|

408

ЗАНЯТИЕ 14. В ведение в макросы и шаблоны

ЛИСТИНГ 14.3. Шаблон функции GetMax для вычисления большего из двух чисел
0:
1:
2:
3:
4:
5:

#include
#include
using namespace std;

6:

{

template
const Type& GetMax(const Types valuel, const Types value2)

7:
8:
9:
10:

11 :

if (valuel > value2)
return valuel;
else
return value2;
}

12 :
13:
14:
15:
16:
17:
18:
19:
20:

template ctypename Type>
void DisplayComparison(const Types valuel, const Types value2)
{
cout « "GetMax(" « valuel «
", " « value2 « ") = ";
cout « GetMax(valuel, value2) « endl;
}

21:

{

int main()

22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32: }

int numl = -101, num2 = 2011;
DisplayComparison(numl, num2);
double dl = 3.14, d2 = 3.1416;
DisplayComparison(dl, d2);
string namel("Jack"), name2("John");
DisplayComparison(namel, name2);
return 0;

Результат
GetMax(-101, 2011) = 2011
GetMax(3.14, 3.1416) = 3.1416
GetMax(Jack, John) = John

Анализ
Это пример возможностей двух шаблонов функций: GetMax () (строки 4-11), ко­
торая используется функцией D isp la y C o m p a riso n () (строки 13-18). Строки 23, 26
и 29 функции main () демонстрируют многократное использование одного и того же
шаблона функции для совершенно разных типов данных: i n t , d ou b le и s t d : : s tr in g .
Кроме того что этот шаблон функции используется повторно (точно так же, как и

Введение в шаблоны

409

его аналог-макрос), его проще создать и поддерживать и он к тому же обеспечивает
безопасность типов!
Обратите внимание, что функцию D i s p l a y C o m p a r i s o n O вполне возможно вы­
звать с явным указанием типа:
23:

DisplayComparison(numl, пиш2);

Однако при вызове шаблонных функций это условие не является обязательным.
Вы не обязаны указывать тип(ы) параметров шаблона, поскольку компилятор в со­
стоянии вывести их самостоятельно. Тем не менее это необходимо при работе с ша­
блонами классов.

Шаблоны и безопасность типов
Шаблонные функции D i s p l a y C o m p a r i s o n ( ) и G e tM a x (), представленные в ли­
стинге 14.3, обеспечивают безопасность типов. Это значит, что они не позволят такой,
например, бессмысленный вызов:
DisplayComparison(numl,паше1);

Это немедленно привело бы к ошибке компиляции.

Шаблонные классы
На занятии 9, “Классы и объекты”, упоминалось, что классы — это программные
блоки, инкапсулирующие определенные атрибуты и методы, работающие с этими
атрибутами. Атрибуты, как правило, — это закрытые члены, такие как i n t A g e в клас­
се Human. Классы — это только чертежи проекта, а реальным представлением класса
являются его объекты. Так, например, Т о т может быть объектом класса Human с атри­
бутом А д е , содержащим значение 15. Мы подразумеваем, что это годы. Но что если
по каким-то причинам возраст нужно хранить в секундах, и типа i n t окажется недо­
статочно, так что вместо него потребуется использовать тип lo n g lo n g ? Здесь могли
бы пригодиться шаблонные классы. Шаблонный класс (template class) представляет
собой шаблонную версию классов C++. Фактически это чертежи чертежей. При ис­
пользовании шаблона класса появляется возможность определить тип, который спе­
циализирует класс. Это позволяет создать одни объекты класса Hum an с параметром
шаблона А д е типа lo n g lo n g , другие — типа i n t , а третьи — типа s h o r t .
Простой пример шаблона класса с одним параметром шаблона Т для переменнойчлена может быть написан следующим образом:
template ctypename Т>
class HoldVarTypeT {
private:
T value;
public:
void SetValue(const T & newValue)
value = newValue;

}

{

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

410

Т & GetValue() {
return value;

}
);
Типом переменной v a l u e является Т — тип, который назначается во время исполь­
зования шаблона, т.е. его инстанцирования. Рассмотрим типичное применение этого
шаблона класса:
HoldVarTypeT holdlnt; // Инстанцирование шаблона для int
holdlnt.SetValue(5);
cout « "Сохраненное значение - " « holdlnt.GetValue() « endl;

Мы использовали этот шаблон класса для хранения и возврата объекта типа i n t ,
т.е. шаблон класса инстанцируется для параметра шаблона i n t . Точно так можно ис­
пользовать этот же класс для работы с символьными строками:
HoldVarTypeT holdStr;
holdStr.SetValue("Sample string");
cout « "Сохраненное значение - " «

holdStr.GetValue() «

endl;

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

СОВЕТ

Шаблонные классы могут быть инстанцированы не только простыми типами
наподобие i n t , c h a r * или классами стандартной библиотеки. Вы можете
инстанцировать шаблонные классы с помощью собственноручно написан­
ных классов. Например, внеся код, который определяет шаблонный класс
H o ld V a r T y p e T в листинг 9.1 занятия 9, “Классы и объекты”, вы сможе­
те инстанцировать шаблон для класса Human, добавляя следующий код в
функцию m a in ():

HoldVarTypeT holdHuman;
holdHuman.SetValue(firstMan);
holdHuman.GetValue().IntroduceSelf();

Объявление шаблонов с несколькими параметрами
Список параметров шаблона может быть расширен и содержать несколько параме­
тров, разделенных запятой. Так, если вы хотите объявить обобщенный класс, содер­
жащий два объекта, типы которых могут различаться, можете использовать конструк­
ции, показанные в следующем примере (в котором приведен шаблон класса с двумя
параметрами шаблона):
template ctypename Tl, typename T2>
class HoldsPair
{
private:
Tl valuel;

Введение в шаблоны

|

411

Т2 value2;
public:
// Конструктор, инициализирующий переменные-члены
HoldsPair(const Т1& vail, const T2& val2)

{
valuel = vail;
value2 = val2;

};
// ... Другие функции-члены

};
Здесь класс H o l d s P a i r получает два параметра шаблона с именами Т 1 и Т 2 .
Мы можем использовать этот класс для хранения двух объектов одинаковых или раз­
ных типов:
// Создание экземпляра шаблона для типов int и double
HoldsPair pairlntDouble(6, 1.99);
// Создание экземпляра шаблона для типов int и int
HoldsPair pairlntDouble(6, 500);

Объявление шаблонов параметрами по умолчанию
Можно изменить предыдущую версию шаблона H o l d s P a i r < . . . > так, чтобы объ­
явить тип i n t как заданный по умолчанию тип параметра шаблона:
template
class HoldsPair
{
// ... Объявления методов

};
Это очень похоже на определение значений по умолчанию для входных параметров
функций, но в данном случае мы определяем заданные по умолчанию типы . В этом
случае применение шаблона H o l d s P a i r может быть сжато до следующего:
// Создание экземпляра шаблона для типов int и int (тип по умолчанию)
HoldsPair о pairlntDouble(6, 500);

Простой шаблон класса H o ld s P a ir
Пришло время дальнейшего усовершенствования версии шаблона H o l d s P a i r . Рас­
смотрим листинг 14.4.
ЛИСТИНГ 14.4. Шаблон класса с двумя атрибутами___________________________________
0: #include
1: using namespace std;
2:
3: // Шаблон с параметрами по умолчанию: int и double

412

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

|

4: template ctypename Tl=int, typename T2=double>
5: class HoldsPair
6: {

7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

private:
T1 value1;
T2 value2;
public:
HoldsPair(const T1& vail, const T2& val2) // Конструктор
: valuel(vail), value2(val2) {}
// Функции доступа
const Tl & GetFirstValue() const
{
return valuel;
}
const T2 & GetSecondValue() const

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:

{

return value2;
}
};
int main()
{
HoldsPairo pairlntDbl(300, 10.09);
HoldsPairpairShortStr(25,"Шаблон");
cout «
cout «
cout «

"Первый объект содержит:" «
endl;
"valuel: ” « pairlntDbl.GetFirstValue() « endl;
"value2: " « pairlntDbl.GetSecondValue() « endl;

cout «
cout «
cout «

"Второй объект содержит:" «
endl;
"valuel: " « pairShortStr.GetFirstValue() « endl;
"value2: " « pairShortStr.GetSecondValue() « endl;

return 0;
}

Результат
Первый объект содержит:
valuel: 300
value2: 10.09
Второй объект содержит:
valuel: 25
value2: Шаблон

Введение в шаблоны

| 413

Анализ
Эта простая программа демонстрирует объявление шаблона класса H o l d s P a i r , со­
держащего значения двух типов, зависящих от списка параметров шаблона. В строке 1
содержится список параметров шаблона, определяющий два параметра шаблона, Т 1 и
Т2, с заданными по умолчанию типами i n t и d o u b le соответственно. Функции досту­
па, G e t F i r s t V a l u e () и G e t S e c o n d V a lu e ( ) , применяются для доступа к значениям,
содержащимся в объекте. Обратите внимание, как функции G e t F i r s t V a l u e () и G e t ­
S e c o n d V a lu e () адаптированы для возвращения объектов соответствующих типов на
основании синтаксиса создания экземпляра шаблона. Теперь у нас определен шаблон
H o l d s P a i r , который можно повторно использовать для предоставления одинаковой
логики обработки переменных различных типов. Таким образом, шаблоны повышают
уровень повторного использования кода.

Инстанцирование и специализация шаблона
Шаблонный класс является схемой класса, а потому не существует для компилято­
ра в реальности до тех пор, пока не будет использован в том или ином виде. Что каса­
ется компилятора, то шаблонный класс, который вы определили, но не использовали
в коде, просто игнорируется. Однако вы инстанцируете шаблонный класс, такой как
H o l d s P a i r , указывая аргументы шаблона, например, следующим образом:
HoldsPair pairlntDbl;

Вы поручаете компилятору создать класс с использованием шаблона и инстанци­
ровать его для типов, указанных в качестве аргументов шаблона (в данном случае —
i n t и d o u b le ) . Таким образом, для шаблонов инстанцирование представляет собой
акт, или процесс, создания определенного типа с использованием одного или несколь­
ких аргументов шаблона.
С другой стороны, могут быть ситуации, которые требуют явного определения
(различного) поведения шаблона при инстанцировании с определенным типом. Этот
процесс называется специализацией шаблона для данного типа. Специализация ша­
блона класса H o l d s P a i r при инстанцировании с параметрами типа i n t будет выгля­
деть следующим образом:
templateo class HoldsPair {
// Код реализации для данных типов

};
Излишне говорить, что код, который специализирует шаблон, должен соответство­
вать определению шаблона. Листинг 14.5 является примером специализации шаблона,
демонстрирующим, насколько сильно различные специализированные версии могут
отличаться от шаблона, который они специализируют.
ЛИСТИНГ 14.5. Демонстрация специализации шаблона
0: #include
1: using namespace std;
2:

414
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:

ЗАНЯТИЕ 14. Введение в макросы и шаблоны
template
class HoldsPair
{
private:
T1 valuel;
T2 value2;
public:
HoldsPair(const T1& vail, const T2& val2) // Конструктор
: valuel(vail), value2(val2) {}
// Функции доступа
const Tl & GetFirstValue() const;
const T2& GetSecondValue() const;
};
// Специализация HoldsPair для двух int
templateo class HoldsPair

20: {

21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:

private:
int valuel;
int value2;
string strFun;
public:
HoldsPair(const int& vail, const int& val2) // Конструктор
: valuel(vail), value2(val2) {}
const int & GetFirstValue() const
{
cout « "Возвращает " « valuel «
return valuel;
}

endl;

};
int main()
{
HoldsPaircint, int> pairlntlnt(222, 333);
pairlntlnt.GetFirstValue();
return 0;
}

Вывод
Возвращает 222

Анализ
Очевидно, что если вы сравните поведение класса H o ld sP a ir в листинге 14.4 и в
этом листинге, то заметите, что шаблон ведет себя совсем иначе. В самом деле, функция

Введение в шаблоны

| 415

G e t F i r s t V a l u e () изменена при инстанцировании шаблона H o l d s P a i r < i n t , i n t > так,
что не только возвращает значение, но и выводит его. Внимательное рассмотрение
кода специализации в строках 18-34 показывает, что данная версия шаблона имеет
дополнительный член-строку, объявленный в строке 24, — член, который отсутствует
в определении исходного шаблона H o l d s P a i r o в строках 3 -1 6 . Более того, опреде­
ление исходного шаблона даже не предоставляет реализацию функций доступа G e t ­
F i r s t V a l u e () и G e t S e c o n d V a lu e ( ) , но программа по-прежнему компилируется. Дело
в том, что компилятору требуется рассмотреть лишь инстанцирование шаблона для
параметров < i n t / in t > , для которых имеется достаточно полная специализированная
реализация. Таким образом, этот пример демонстрирует не только специализацию
шаблона, но и то, как рассматривается (или даже игнорируется) код шаблона в зави­
симости от его применения.

Шаблонные классы и статические члены
Как уже упоминалось, код в шаблонах начинает свое существование для компи­
лятора только тогда, когда используется в программе, и никак иначе. А что можно
сказать о статическом члене шаблонного класса? Из занятия 9, “Классы и объекты”,
вы знаете, что объявление члена статическим приводит к тому, что он совместно ис­
пользуется всеми экземплярами класса. То же самое справедливо и для шаблонного
класса — статический член совместно используется всеми экземплярами класса с од­
ними и теми же параметрами. Так, статический член X в шаблонном классе является
статическим для всех экземпляров класса, инстанцированных для типа i n t . Такой
же статический член X является статическим для всех экземпляров класса, инстанци­
рованных для типа d o u b le , и при этом никак не связан со статическим членом X для
i n t . Другими словами, вы можете представить это как создание компилятором двух
версий статического члена шаблонного класса: X _ i n t — для первого и X _ d o u b le —
для второго случая (листинг 14.6).
ЛИСТИНГ 14,6. Статические переменные шаблонного класса__________________________
0:
1:
2:
3:
4:
5:
6:
7:
8 :
9

iinclude
using namespace std;
template ctypename T>
class TestStatic
{
public:
static int staticVal;
};

:

10:
11:
12:
13:
14:
15:

// Инициализация статического члена
templatectypename T> int TestStatic::staticVal;
int main()
{
TestStatic intlnstance;

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

416
16:
17:
18:
19:
20:
21:

cout « "staticVal для int равен 2011м «
intInstance.staticVal = 2011;

endl;

TestStatic dblnstance;
cout « "staticVal для double равен 1011” «
dblnstance.staticVal = 1011;

endl;

22 :
23:
24:
25:
26:
27: }

cout «
cout «

’’intlnstance: " « intlnstance.staticVal « endl;
’’dblnstance: ” « dblnstance.staticVal « endl;

return 0;

Результат
staticVal для int равен 2011
staticVal для double равен 1011
intlnstance: 2011
dblnstance: 1011

Анализ
В строках 17 и 21 устанавливаются значения члена s t a t i c V a l экземпляров ша­
блона для типов i n t и d o u b le соответственно. Вывод на экран демонстрирует, что
компилятор хранит два разных значения в двух разных статических членах, имена
которых — s t a t i c V a l . Таким образом, компилятор гарантирует, что поведение ста­
тической переменной остается неизменным для специализации шаблонного класса
для конкретного типа.

ПРИМЕЧАНИЕ

Обратите внимание на синтаксис создания экземпляра статического члена
для шаблона класса в строке 11 листинга 14.6.

te m p latecty p e n am e Т> i n t T e s t S t a t i c < T > : : s t a t i c V a l ;
Он следует общей схеме:

t emp 1at е< пар а м ет р ы ша б л о н а > Тип_ ч л е н а
Имя_Кла с са < А р гум ен т ы ша б л о н а > :: Имя_Ста т и ч е с к о г о _ ч л е н а ;

Шаблоны с переменным количеством
параметров (вариадические шаблоны)
Предположим, что вы хотите написать обобщ енную функцию, которая суммирует
два значения. Шаблон функции Sum () делает только это:
template
void Sum(Tl & result, T2 numl, T3 num2) {

Шаблоны с переменным количеством параметров (вариадические шаблоны)

417

result = numl + num2;
return;

Здесь все просто. Однако, если вам требуется написать одну функцию, которая
могла бы складывать лю бое количество значений, каждое из которых передается в
качестве аргумента, то вам нужно использовать в определении такой функции вариа­
дические шаблоны. Такие шаблоны являются частью C++ начиная со стандарта С ++
14, выпущенного в 2014 году. В листинге 14.7 демонстрируется использование вариадических шаблонов в определении обобщенной функции.
ЛИСТИНГ 14,7. Применение вариадических шаблонов________________________________
0:
1:
2:
3:
4:
5:
6:
7:

#include
using namespace std;
template
void Sum(Res& result, ValType& val)
{
result = result + val;
}

8:

9: template ctypename Res, typename First, typename... Rest>
10: void Sum(Res& result, First vail, Rest... valN)

11:

{

12:
13:
14: }
15:
16: int
17: {
18:
19:
20:

result = result + vail;
return Sum(result, valN

...);

main()
double dResult = 0;
Sum(dResult, 3.14, 4.56, 1.1111);
cout « "dResult = " « dResult «

endl;

21 :
22:
23:
24:
25:
26:
27: )

string strResult;
Sum(strResult, "Hello ", "World");
cout « "strResult = " « strResult.c_str() «
return 0;

Вывод
dResult = 8.8111
strResult = HelloWorld

endl;

418

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

Диализ
В этом примере демонстрируется, что функция Sum (), которую мы определили с
использованием вариадических шаблонов, может не только работать с совершенно
различными типами аргументов, как показано в строках 19 и 23, но и справиться с
различным числом аргументов. Функция Sum(), вызванная в строке 19, получает че­
тыре аргумента, а в строке 23 — три аргумента, один из которых представляет собой
s t d : : s t r i n g , а следующие два — c o n s t c h a r *. Во время компиляции компилятор
создает код функции Sum (), который корректно выполняет все требуемые вычисления
с помощью рекурсивных вызовов, пока не будут обработаны все переданные функции
аргументы.

ПРИМЕЧАНИЕ

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

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

C++14 обеспечивает вас оператором, который может сообщить вам ко­
личество аргументов, переданных в вызове шаблона с переменным коли­
чеством аргументов. В листинге 14.7 этот оператор можно использовать
внутри функции как

Sum ()

следующим образом:

int arrNums[sizeof...(Rest)];
// Длина массива вычисляется с помощью sizeof...() во время
компиляции
Не путайте

s iz e o f ...

() с

s i z e o f (Туре) ! Последнее

выражение воз­

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

Поддержка шаблонов с переменным числом аргументов открыла возможность
стандартной поддержки кортежей. Шаблонный класс, реализующий кортежи, —
s t d : : t u p l e . Он может быть создан с различным количеством элементов и их типов.
Эти элементы могут быть доступны индивидуально с помощью функции стандартной
библиотеки s t d : : g e t . В листинге 14.8 демонстрируется создание и использование
экземпляра s t d : : t u p l e .

Шаблоны с переменным количеством параметров (вариадические шаблоны)

|

419

ЛИСТИНГ 1 4 .8 . Инстанцирование и использование s t d : : t u p l e ______________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
#include
#include
using namespace std;
template ctypename tupleType>
void DisplayTuplelnfo(tupleType& tup)
{
const int numMembers = tuple_size:rvalue;
cout«"Элементов в кортеже: " « numMembers « endl;
cout«" Последний элемент: " « get(tup) «

11:

}

endl;

12 :
13: int main()
14: {
15:
tupletupl(make_tuple(101,'s',"Hello Tuple!"));
16:
DisplayTuplelnfo(tupl);
17:
18:
auto tup2 (make__tuple (3.14, false));
19:
DisplayTuplelnfo(tup2);
20:

21:
auto concatTup(tuple_cat(tup2, tupl)); // Члены tup2, tupl
22:
DisplayTuplelnfo(concatTup);
23:
24:
double pi;
25:
string sentence;
26:
tie(pi, ignore, ignore, ignore, sentence) = concatTup;
27:
cout « "Pi: " « pi « " и \"" « sentence « "\"" « endl;
28:
29: return 0;
30: }

Вывод
Элементов в кортеже: 3
Последний элемент: Hello Tuple!
Элементов в кортеже: 2
Последний элемент: 0
Элементов в кортеже: 5
Последний элемент: Hello Tuple!
Pi: 3.14 и "Hello Tuple!"

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

420

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

применение в обобщ енном программировании. В этой книге мы просто хотим дать
вам представление об этой развивающейся концепции. Строки 15, 18 и 21 содержат
три различных экземпляра s t d : : t u p le , tu p l содержит три члена: s t d : : s t r in g , i n t и
char. tup2 содержит значения d o u b le и b o o l, а также использует возможности авто­
матического вывода типа компилятором с помощью ключевого слова auto. tup3 пред­
ставляет собой кортеж из пяти членов: d o u b le, b o o l, in t , char и s t r i n g — результат
конкатенации с использованием шаблонной функции s td : :tu p le _ c a t .
Шаблонная функция D is p la y T u p le ln f o () в строках 5-11 демонстрирует ис­
пользование шаблона t u p l e _ s i z e , который разрешается в количество элементов,
содержащихся в экземпляре s t d : : t u p le во время компиляции. Функция s td : :g e t,
использованная в строке 10, предоставляет механизм доступа к отдельным значени­
ям, хранящимся в кортеже, с помощью их индексов (как обычно в C++, нумерация
начинается с нуля). Наконец функция s t d : : t i e в строке 26 демонстрирует, как со­
держимое кортежа может быть распаковано или скопировано в отдельные объекты.
Значение s t d : : ig n o r e используется для того, чтобы указать s t d : : t i e те элементы
кортежа, которые не интересуют наше приложение.

Использование static_assert
для выполнения проверок
времени компиляции
Эта возможность появилась в языке программирования начиная со стандарта
C++11 и позволяет блокировать компиляцию в случае, когда указанные программис­
том тесты во время компиляции не выполняются. Несмотря на кажущуюся стран­
ность этой возможности, она может оказаться очень полезной при разработке шабло­
нов классов. Например, вы можете захотеть гарантировать, что ваш шаблон класса не
будет инстанцирован для типа i n t ! Использование s t a t i c a s s e r t позволяет отоб­
разить специальное сообщ ение времени компиляции в вашей среде разработки (или
выдать его на консоль):
stat ic_assert {Проверяемое_выражение,

"Сообщение об ошибке”);

Чтобы предотвратить инстанцирование вашего шаблона класса для типа in t , мож­
но, например, использовать s t a t i c _ a s s e r t () с оператором s i z e o f (Т), сравнивая
возвращаемое им значение с результатом выражения s i z e o f ( in t ) и отображая со­
общение об ошибке, если проверка на неравенство терпит неудачу:
static_assert(sizeof(Т) != sizeof(int), "int не разрешен!");

Такой шаблон класса, использующий s t a t i c _ a s s e r t для блокировки компиляции
при определенных условиях, показан в листинге 14.9.

Использование static_assert для выполнения проверок времени компиляции

|

421

ЛИСТИНГ 14.9. Шаблон класса, не работающий
с типами с размером, равным размеру in t _________________________________
0: template
1: class EverythingButlnt
2: {

3: public:
4:
EverythingButlnt ()
5:
{
6:
static_assert(sizeof(T)
7:
)
8:
9

!= sizeof(int), "int запрещен!");

);

:

10: int main()

11 :

{

12:
13:
14: )

EverythingButInt test; // Инстанцирование для int.
return 0;

Результат
Вывода нет, поскольку компиляция неудачна — отображается указанное вами со­
общение:
error: int запрещен!

Анализ
Запрет на инстанцирование запрограммирован в строке 6. s ta tic _ a s s e rt — это
языковое средство С ++11, которое помогает, в частности, защитить свой код шаблона
от нежелательного инстанцирования.

Использование шаблонов в практическом
программировании на C++
Самое важное и мощное применение шаблоны нашли в стандартной библиотеке
шаблонов (Standard Template Library — STL). Библиотека STL состоит из коллекции
шаблонов классов и функций, содержащей обобщенные вспомогательные классы и
алгоритмы. Шаблонные классы библиотеки STL позволяют реализовать динамиче­
ские массивы, списки и контейнеры пар “ключ-значение”, в то время как алгоритмы,
как, например, алгоритм сортировки, работают с этими контейнерами и обрабатывают
содержащиеся в них данные.
Знание синтаксиса шаблонов, с которым вы познакомились, очень поможет да­
лее, при использовании контейнеров и функций STL, которые будут рассматриваться
на следующих занятиях. Хорошее понимание контейнеров и алгоритмов библиотеки
STL, в свою очередь, поможет вам создавать эффективные приложения на языке про­
граммирования C++ с использованием проверенной и надежной реализации библио­
теки STL, а также избежать долгих часов копания в дебрях кода.

422

|

ЗАНЯТИЕ 14. Введение в макросы и шаблоны

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Используйте

Не забывайте использовать константность при
разработке шаблонов функций и классов.

шаблоны для реализации обоб­

щенных концепций.

Предпочитайте шаблоны

макросам.

Не забывайте, что

статический член, содержа­

щийся в шаблоне класса, является статическим
для каждой специализации класса.

Резюме
На сегодняшнем занятии представлено большое количество подробностей о рабо­
те препроцессора. Каждый раз, когда вы запускаете компилятор, сначала запускается
препроцессор, преобразующий в исходный текст такие директивы, как # d ef in e .
Препроцессор осуществляет только простую текстовую подстановку, хотя исполь­
зование макросов может давать достаточно сложные результаты. Макрофункции обес­
печивают сложную текстовую подстановку на основании аргументов, передаваемых
макросу во время компиляции. Каждый аргумент в макросе следует помещать в круг­
лые скобки, чтобы гарантировать правильность подстановки.
Шаблоны помогают обеспечить повторное использование кода, применимого для
множества различных типов данных. Они также являются альтернативой макросам,
обеспечивающей безопасность типов. Со знанием шаблонов, полученным на этом за­
нятии, вы готовы приступить к изучению библиотеки STL.

Вопросы и ответы
■ Почему я должен использовать защиту от повторного включения в своих за­
головочных файлах?
Защита от повторного включения с использованием директив # if n d e f , # d e fin e
и # e n d if защищает ваш заголовочный файл от ошибок, неизбежных при множе­
ственном или рекурсивном включении, и в некоторых случаях даже ускоряет ком­
пиляцию.

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

■ Должен ли я определять аргументы шаблона при вызове шаблона функции?
Обычно нет, поскольку компилятор способен вывести их самостоятельно на основе
переданных при вызове аргументов функции.

■ Сколько экземпляров статических переменных имеется для данного шаблона
класса?

Коллоквиум

|

423

Все зависит от количества типов, для которых шаблон класса был инстанциро­
ван. Так, если шаблон инстанцирован для типов i n t , s t r i n g и пользовательского
типа X, то будут доступны три экземпляра статической переменной — по одному
для каждого инстанцирования.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления полу­
ченных знаний, а также упражнения, которые помогут применить на практике по­
лученные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выпол­
нить задания, а потом сверьте полученные результаты с ответами в приложении Д,
“Ответы”. Если остались неясными хотя бы некоторые из предложенных ниже во­
просов, не приступайте к изучению материала следую щ его занятия.

Контрольные вопросы
1. Что такое защита от повторного включения?
2. Рассмотрим следующий макрос:
#define SPLIT(х) х / 5

Каков будет его результат при вызове со значением 20?
3. Каков будет результат, если вызвать макрос S P L I T из вопроса 2 со значением

10 + 10 ?
4. Как изменить макрос S P L I T , чтобы избежать ошибочных результатов?

Упражнения
1. Напишите макрос, умножающий два числа.
2. Напишите шаблонную версию макроса из упражнения 1.
3.

Реализуйте шаблонную функцию sw ap () для обмена значений двух переменных.

4. Отладка. Как улучшить следующий макрос, вычисляющий четверть исходного
значения?
#define QUARTER(х) (х / 4)

5. Напишите простой шаблон класса, хранящий два массива элементов с типами,
которые определены в списке параметров шаблона класса. Размер массива —
10; шаблон класса должен быть оснащен функциями доступа, обеспечивающи­
ми работу с элементами массива.
6. Напишите шаблонную функцию D i s p l a y (), которая может быть вызвана с раз­
ными количеством и типами аргументов и выводит на консоль каждый из них.

Часть III

Стандартная
библиотека
шаблонов
В ЭТОЙ ЧАСТИ...
ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов
ЗАНЯТИЕ 16. Класс строки библиотеки STL
ЗАНЯТИЕ 17. Классы динамических массивов библиотеки STL
ЗАНЯТИЕ 18. Классы list и forwardjist
ЗАНЯТИЕ 19. Классы множеств STL
ЗАНЯТИЕ 20. Классы отображений библиотеки STL

ЗАНЯТИЕ 15

Введение
в стандартную
библиотеку
шаблонов
Попросту говоря, стандартная библиотека шаблонов
(STL) — это набор шаблонов классов и функций, который
предоставляет программистам следующее:
■ контейнеры для хранения информации,
■ итераторы для доступа к хранимой информации,
■ алгоритмы для манипуляции содержимым контейнеров.
На этом занятии вы получите общее представление об
этих трех китах библиотеки STL.

428

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов

Контейнеры STL
Контейнеры (container) — это классы библиотеки STL, предназначенные для хра­
нения данных. Библиотека STL предоставляет два типа контейнерных классов:
■ последовательные контейнеры;
■ ассоциативные контейнеры.
В дополнение к ним библиотека STL предоставляет классы, называемые адапте­
рами контейнеров (container adapter), являющиеся версиями имеющихся контейнеров
с ограниченными функциональными возможностями, предназначенные для специфи­
ческих целей.

Последовательные контейнеры
Как и подразумевает их название, последовательные контейнеры (sequential
container) используются для хранения данных в последовательном виде, таком как
массивы и списки. Последовательные контейнеры характеризуются быстрым выпол­
нением вставки, но относительно медленным поиском.
Ниже приведены последовательные контейнеры библиотеки STL.
■ s t d : : v e c t o r . Работает как динамический массив и увеличивается с конца. Век­
тор похож на книжную полку, книги на которую можно добавлять или удалять по
одной с конца.
■ s t d : :deque. Подобен контейнеру s t d : : v e c to r , но новые элементы можно встав­
лять и удалять также в начало контейнера.
■ s t d : : l i s t . Работает как двухсвязный список. Список похож на цепочку, в которой
каждый объект связан с предыдущим и последующим звеньями. Вы можете доба­
вить или удалить звено (т.е. объект) в любой позиции.
■ s t d : : fo r w a r d _ lis t . Подобен списку s t d : : l i s t , но односвязный список позволя­
ет осуществлять проход по списку только в одном направлении.
Класс v e c t o r библиотеки STL сродни массиву и обеспечивает произвольный до­
ступ к элементам, т.е. вы можете обращаться к элементам вектора непосредственно с
использованием их позиции в векторе, используя оператор индексации ([ ]), и рабо­
тать с этими данными. Кроме того, вектор STL является динамическим массивом и,
таким образом, может изменять свои размеры, чтобы соответствовать требованиям
приложения. Для обеспечения этой возможности при сохранении способности произ­
вольного обращения к элементам массива по индексу контейнер v e c t o r библиотеки
STL хранит все элементы последовательно, в непрерывной области памяти. Поэтому
вектор должен уметь изменять свои размеры (что может отрицательно влиять на про­
изводительность приложения в зависимости от типа объектов, которые он содержит).

Контейнеры STL

| 429

Вкратце вектор был представлен в листинге 4.4, а более подробно контейнер v e c t o r
рассматривается на занятии 17, “Классы динамических массивов библиотеки STL”.
Контейнер l i s t библиотеки STL является реализацией обычного связанного спис­
ка. Хотя к элементам списка нельзя обращаться произвольно, как в векторе STL,
список может хранить элементы в несмежных блоках памяти. Поэтому у контейнера
s t d : : l i s t нет присущих вектору проблем с производительностью, связанных с пере­
распределением его внутреннего массива. Подробно класс списка библиотеки STL
обсуждается на занятии 18, “Классы list и forward_list”.

Ассоциативные контейнеры
Ассоциативные контейнеры (associative container), хранящие данные в отсортиро­
ванном виде, сродни словарю. В результате вставка в них осуществляется медленнее,
чем в последовательные контейнеры, но когда дело доходит до поиска, преимущества
ассоциативных контейнеров оказываются существенными.
Библиотека STL предоставляет следующие ассоциативные контейнеры.
■ s t d : : s e t . Уникальные значения хранятся в контейнере в отсортированном по­
рядке; вставка в контейнер и поиск в нем являются операцией с логарифмической
сложностью.
■ s t d : : u n o r d e r e d _ s e t . Уникальные значения хранятся в данном контейнере
неотсортированными, но вставка и поиск осуществляются за время, близкое к кон­
стантному. Этот контейнер доступен начиная с версии С ++11.
■ std : :шар. Хранит пары “ключ-значение” с уникальными ключами, отсортирован­
ными по значениям ключей; вставка в контейнер и поиск в нем являются операци­
ей с логарифмической сложностью.
■ s t d : :unordered_m ap. Хранит пары “ключ-значение” с уникальными ключами в
неотсортированном порядке, но вставка и поиск осуществляются за время, близкое
к константному. Этот контейнер доступен, начиная с версии С ++11.
■ s td : : m u l t i s e t . Похож на контейнер s e t ; дополнительно обеспечивает возмож­
ность хранить несколько элементов с одинаковыми значениями, т.е. значение не
обязательно должно быть уникальным.
■ s td : :u n o r d e r e d _ m u ltis e t. Похож на контейнер u n o rd ered _ set; дополнительно
обеспечивает возможность хранить несколько элементов с одинаковыми значения­
ми, т.е. значение не обязательно должно быть уникальным. Этот контейнер досту­
пен начиная с версии С ++11.
■ s t d : :m ultim ap. Похож на контейнер тар; дополнительно обеспечивает возмож­
ность хранить пары “ключ-значение” с одинаковыми ключами.
■ s td : :unordered_m ultim ap. Похож на контейнер unordered_map; дополнительно
обеспечивает возможность хранить пары “ключ-значение” с одинаковыми ключа­
ми. Этот контейнер доступен начиная с версии С ++11.

430

|

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов

ПРИМЕЧАНИЕ

Сложность в данном случае является показателем производительности
контейнера с учетом количества содержащихся в нем элементов. Говоря
о константной сложности, как в случае s t d : : u n o r d e r e d map, мы под­
разумеваем, что производительность контейнера не связана с количеством
содержащихся в нем элементов. Такой контейнер, содержащий тысячу эле­
ментов, потребует столько же времени на выполнение операции, как и кон­
тейнер с миллионом элементов.
Логарифмическая сложность (как в случае с s t d : :map) указывает, что
время выполнения операции пропорционально логарифму количества эле­
ментов, содержащихся в контейнере. Время выполнения операции таким
контейнером с тысячью элементов будет в два раза меньше времени рабо­
ты контейнера с миллионом элементов.
Линейная сложность означает, что время выполнения операции пропор­
ционально количеству элементов в контейнере. Такой контейнер будет в
тысячу раз медленнее при обработке миллиона элементов, чем при обра­
ботке тысячи элементов.
У одного и того же контейнера сложность может быть разной для различных
операций. Например, вставка элемента может иметь константную слож­
ность, в то время как операция поиска элемента - линейную сложность.
Таким образом, знание, помимо доступных операций, как именно контей­
нер может их выполнять, является ключом к выбору контейнера, наилуч­
шим образом подходящего для вашей задачи.

Сортировка контейнеров STL может быть настроена программистом посредством
написания соответствующих предикатных функций.

СОВЕТ_________

Некоторые реализации библиотеки STL предоставляют и такие ассоциатив­
ные контейнеры, как h a s h

s e t , h a s h m u l t i s e t , h a s h map и h a s h

m u lt im a p . Они подобны контейнерам u n o r d e r e d

*, которые поддер­

живаются в соответствии со стандартом. В некоторых сценариях варианты
hash

* и u n o rd e re d

* могут оказаться лучше при поиске элемента, по­

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

Итераторы STL

|

431

Адаптеры контейнеров
Адаптеры контейнеров (container adapter) — это версии последовательных и ассо­
циативных контейнеров с ограниченными функциональными возможностями, пред­
назначенные для специфических целей. Основные классы адаптеров приведены ниже.
■ s t d : : s t a c k . Хранит элементы в порядке LIFO (Last-In-First-Out — последним во­
шел, первым вышел), позволяя вставлять и извлекать элементы из вершины стека.
■ s t d : : q u e u e . Хранит элементы в порядке FIFO (First-In-First-Out — первым вошел,
первым вышел), позволяя извлекать элементы в порядке их вставки в очередь.
■ s t d : : p r i o r i t y _ q u e u e . Элементы хранятся в отсортированном порядке, так что
первым в очереди всегда располагается элемент, значение приоритета которого
считается самым высоким.
Более подробная информация по этой теме приведена на занятии 24, “Адаптивные
контейнеры: стек и очередь”.

Итераторы STL
Самый простой пример итератора (iterator) — это указатель на первый элемент
в массиве. Вы можете выполнить инкремент этого указателя, и он будет указывать на
следующий элемент массива.
Итераторы библиотеки STL — это шаблоны классов, которые в определенной
степени являются обобщ ением указателей. Такие шаблоны классов предоставляют
разработчикам средство, позволяющее работать с элементами в контейнерах STL и
выполнять над ними те или иные операции. Заметим, что эти операции могут быть ал­
горитмами STL, которые представляют собой шаблонные функции. Итераторы — это
своего рода мост, позволяющий шаблонным функциям единообразно и согласованно
работать с самыми разными контейнерами.
Предоставляемые библиотекой STL итераторы глобально можно классифициро­
вать следующим образом.
■ Итератор ввода (input iterator). Такой итератор может быть разыменован для получе­
ния ссылки на объект. Этот объект может, например, находиться в коллекции. Клас­
сический итератор ввода гарантирует только доступ для чтения значения объекта.
■ Итератор вывода (output iterator). Этот итератор обеспечивает запись в коллекцию.
Классический итератор вывода гарантирует доступ только для записи.
Основные типы итераторов, упомянутые в предыдущем списке, можно подразде­
лять на следующие разновидности.
■ Однонаправленный итератор (forward iterator). Усовершенствованный итератор,
обеспечивающий как ввод, так и вывод. Такие итераторы могут быть константны­
ми, обеспечивающими доступ только для чтения к объекту, на который указывает
итератор, либо неконстантными, обеспечивающими операции чтения и записи. Как
правило, однонаправленный итератор используется в односвязном списке.

432

|

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов

■ Двунаправленный итератор (bidirectional iterator). Усовершенствованный однонаправ­
ленный итератор, допускающий переход как к следующему, так и к предыдущему эле­
ментам. Двунаправленный итератор, как правило, используется в двусвязном списке.
■ Итератор произвольного доступа (random access iterator). Усовершенствованный
итератор, допускающий прибавление и вычитание смещения, а также вычитание
одного итератора из другого для поиска относительного смещения (дистанции)
между двумя объектами коллекции. Итератор произвольного доступа, как правило,
используется с массивами.

ПРИМЕЧАНИЕ

На уровне реализации усовершенствование можно рассматривать как на­
следование или специализацию.

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



s t d : : f in d .



s t d : : f i n d i f . Позволяет найти значение в коллекции с применением пользова­
тельского предиката.



s t d : :re v e rse .



s t d : : r e m o v e _ if . Позволяет удалить элемент из коллекции с применением пользо­
вательского предиката.



s t d : : t r a n s f o r m . Позволяет применить определенную пользователем функцию
преобразования к элементам контейнера.

Обращает порядок элементов в коллекции.

Эти алгоритмы представляют собой шаблоны функций из пространства имен
Для их применения требуется включить в код стандартный заголовочный файл

s td .

< a lg o r it h m > .

Взаимодействие контейнеров
и алгоритмов с использованием
итераторов
Рассмотрим конкретный пример, как использование итераторов соединяет контей­
неры и алгоритмы STL. Программа, представленная в листинге 15.1, использует по­
следовательный контейнер STL s t d : : v e c t o r , работающий как динамический массив,

Взаимодействие контейнеров и алгоритмов с использованием итераторов

433

хранящий несколько целых чисел, а затем использует алгоритм s t d : : f in d для поиска
одного из них. Обратите внимание, как итераторы соединяют контейнеры и алгорит­
мы STL. Не обращайте внимания на сложности синтаксиса или функциональность.
Контейнеры, такие как s t d : : v e c t o r , и алгоритмы, такие как s t d : : f in d , еще будут
подробно рассматриваться на занятиях 17, “Классы динамических массивов библио­
теки STL”, и 23, “Алгоритмы библиотеки STL”, соответственно. Если эта часть по­
кажется вам слишком сложной, можете ее пропустить.
ЛИСТИНГ 15.1. Поиск элемента по его позиции в векторе_____________________________
1:
2:
3:
4:
5:
6:
7:
8:
9:

iinclude
iinclude
iinclude
using namespace std;
int main()
{
// Динамический массив целых чисел
vector intArray;

10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

// Вставить примеры целых чисел в массив
intArray.push_back(50);
intArray.push_back(2991);
intArray.push_back(23);
intArray.push_back(9999);
cout «

"Содержимое вектора: " «

endl;

// Обход вектора и чтение значений с помощью итератора
vector ::iterator arrlterator = intArray.begin();

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:

while(arrlterator != intArray.end())
{
// Вывод значения на экран
cout « *arrIterator « endl;
// Инкремент итератора для доступа к следующему элементу
++arrIterator;
}
// Поиск элемента (скажем, 2991) с помощью алгоритма 'find'
vector ::iterator elFound =
find(intArray.begin(), intArray.end(), 2991);
// Проверить, найдено ли значение
if (elFound != intArray.end())
{
// Значение найдено. Определяем позицию вмассиве:
int elPos = distance(intArray.begin(), elFound);
cout « "Значение " « *e 1Found;
cout « " находится в позиции " « elPos «
endl;

434
42:
43:
44:
45: }

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов
}
return 0;

Результат
Содержимое вектора:
50
2991
23
9999
Значение 2991 находится в позиции 1

Анализ
В листинге 15.1 показано применение итераторов для обхода вектора и в качестве
интерфейса, позволяющего использовать такие алгоритмы, как fin d , с разными кон­
тейнерами, например v e c t o r . Объект итератора a r r l t e r a t o r объявлен в строке 20
и инициализирован начальной позицией в контейнере (возвращаемым значением
функции-члена b e g in () контейнера v e c t o r ) . Строки 2 2 -2 9 демонстрируют исполь­
зование этого итератора в цикле отображения элементов вектора таким же способом,
как и элементов статического массива. Итераторы используются совершенно одина­
ково всеми контейнерами STL. Все контейнеры предоставляют функцию b e g in (),
указывающую на первый элемент, и функцию end () , указывающую на конец кон­
тейнера — на позицию после последнего элемента. Вот почему цикл w h ile в стро­
ке 22 останавливается на элементе перед указанным функцией end () , а не на нем.
В строке 32 показано использование алгоритма f in d для поиска значения в контей­
нере v e c t o r . Результат операции поиска — итератор, а ее успешность проверяется
путем сравнения итератора с итератором конца контейнера (строка 36). Если элемент
найден, он может быть отображен с помощью разыменования итератора (как и при ис­
пользовании указателя). Алгоритм d is t a n c e () применяется для вычисления позиции
(смещения) найденного элемента.
Если не глядя заменить в листинге 15.1 все слова v e c t o r словами deque, код все
равно будет компилироваться и прекрасно работать благодаря итераторам, которые
обеспечивают взаимосвязь между контейнерами и алгоритмами.

Использование ключевого слова auto
для определения типа
В листинге 15.1 использовано несколько объявлений итератора. Они выглядят по­
добно следующему:
20:

vector ::iterator arrlterator = intArray.begin();

Определение типа итератора выглядит пугающе. Если вы используете компилятор,
совместимый со стандартом С++11, то можете упростить эту строку до следующей:
20:

auto arrlterator = intArray.begin(); // Компилятор выводит тип

Выбор правильного контейнера

435

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

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

ТАБЛИЦА 15.1. Свойства контейнерных классов STL
Контейнер

Преимущества

s t d : : v e c to r
(Последовательный
контейнер)

Быстрая (константная по про­
должительности) вставка в
конец.
Доступ, как у массива

s t d : :deque
(Поеледовател ьный
контейнер)

s td ::lis t
(Последовательный
контейнер)

s td ::fo r w a r d _ lis t
(Последовательный
контейнер)

Недостатки

Изменение размеров может
привести к потере производи­
тельности.
Время поиска пропорциональ­
но количеству элементов в
контейнере.
Вставка только в конец контей­
нера
Все преимущества контейнера Недостатки вектора по про­
v e c to r , а также постоянная по изводительности и поиску
относятся также к деку.
продолжительности вставка в
В отличие от вектора, дек не
начало контейнера
обязан предоставлять функцию
r e s e r v e (), которая резерви­
рует область памяти, позволяя
избежать изменения размеров
для повышения производитель­
ности
К элементам нельзя обращать­
Константная продолжитель­
ся произвольно по индексу, как
ность вставки в любое место
в массиве.
списка.
Доступ к элементам может
Константная продолжитель­
ность удаления элементов из
быть медленнее, чем у вектора,
списка независимо от позиции. поскольку они хранятся не в
Вставка и удаление элементов смежных областях памяти.
Время поиска пропорциональ­
не влияют на итераторы, ука­
зывающие на другие элементы но количеству элементов в
контейнере
списка
Односвязный список, допуска­ Допускает вставку только в на­
ющий итерацию только в одном чало списка с помощью метода
направлении
p u sh _ fr o n t ()

436

|

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов
П родолж ен и е табл. 15.1

Контейнер

Преимущества

Недостатки

std ::se t
(Ассоциативный
контейнер)

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

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

s td : :unordered__
s e t (Ассоциативный
контейнер)
s td ::m u ltis e t
(Ассоциативный
контейнер)

s t d : :u n ord ered
m u ltis e t
(Ассоциативный
контейнер)

std ::m a p
(Ассоциативный
контейнер)

s td ::u n o r d e r e d _
map (Ассоциативный
контейнер)
s t d : :m ultim ap
(Ассоциативный
контейнер)

Когда необходимо содержать
не уникальные значения, пред­
почтительнее использования
контейнера u n o r d e r e d _ se t.
Производительность, как у
контейнера u n o r d e r e d _ se t,
а именно: постоянное среднее
время поиска, вставки и уда­
ления элементов не зависит от
размера контейнера
Контейнер пар "ключзначение", поиск в котором
пропорционален не количеству
элементов в контейнере, а его
логарифму, а потому зачастую
значительно быстрее, чем в по­
следовательных контейнерах
Поиск, вставка и удаление в
контейнере этого типа почти не
зависят от количества элемен­
тов
Используется, когда отобра­
жение должно содержать не
уникальные значения ключей

s t d :: u n o rd ered _
Когда необходимо содержать
m ultim ap (Ассоциатив- не уникальные значения
ный контейнер)
ключей, предпочтительнее
использования контейнера
unordered_map.

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

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

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

Классы строк библиотеки STL

437

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

Контейнер

Преимущества

Недостатки

Поиск, вставка и удаление в
контейнере этого типа почти не
зависят от количества элементов

Классы строк библиотеки STL
Библиотека STL предоставляет шаблон класса, специально предназначенного для
строковых операций. Шаблон s t d : : b a s ic _ s t r in g < T > используется обычно в двух
своих специализациях.
■ s t d : : s t r in g . Специализация шаблона s t d : : b a s ic _ s t r in g для типа char, исполь­
зуемая для работы с простыми символьными строками.
■ s t d : :w s tr in g . Специализация шаблона s t d : : b a s i c _ s t r i n g для типа w ch ar_t,
используемая для работы с широкосимвольными строками, обычно для хранения
символов Unicode.
Эти вспомогательные классы подробно обсуждаются на занятии 16, “Класс строки
библиотеки STL”; вы увидите, насколько они упрощают работу со строками.

Резюме
На сегодняшнем занятии рассматривались фундаментальные концепции библиоте­
ки STL, такие как контейнеры, итераторы и основные алгоритмы. Вы познакомились
также с шаблоном b a s ic _ s tr in g < T > , который подробно обсуждается на следующем
занятии. Контейнеры, итераторы и алгоритмы — это одни из самых важных концеп­
ций библиотеки STL, и их понимание поможет вам эффективно использовать библио­
теку STL в своих приложениях. Более подробная информация об этих концепциях и
их применении приводится на занятиях 17-25.

Вопросы и ответы
■ Мне нужно использовать массив, но количество элементов, которые он дол­
жен содержать, заранее неизвестно. Какой контейнер STL мне следует исполь­
зовать?
Вам отлично подойдут контейнеры s t d : : v e c t o r и s t d : :deque. Их самостоятель­
ное управление памятью и динамическое масштабирование улучшат приложение.

■ В моем приложении довольно часто используется поиск. Какой контейнер мне
следует выбрать?
Для частых поисков лучше всего подойдут такие ассоциативные контейнеры, как
s td : :map и s t d : : s e t , или их неупорядоченные варианты.

438

|

ЗАНЯТИЕ 15. Введение в стандартную библиотеку шаблонов

■ Я должен хранить пары “ключ-значение” для быстрого поиска. Но может слу­
читься так, что ключи будут не уникальными. Какой контейнер мне следует
выбрать?
Вам подойдет ассоциативный контейнер типа s td : :multimap. Контейнер multimap
может содержать не уникальные пары “ключ-значение” и в состоянии обеспечить
быстрый поиск, характерный для ассоциативных контейнеров.

■ Мое приложение предназначено для разных платформ и компиляторов. Мне
необходим контейнер с быстрым поиском на основании ключа. Должен ли я
использовать контейнер std : :шар или std : :hash_xnap?
Переносимость — важный ограничивающий фактор, поэтому вам необходимо ис­
пользовать только стандартные контейнеры, h a sh map не является частью стан­
дарта С ++11 и может не поддерживаться на всех платформах, для которых предна­
значено ваше приложение. Если на всех интересующих платформах используются
компиляторы, совместимые со стандартом С++11, вы могли бы использовать кон­
тейнер s t d : :unordered_map.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления полу­
ченных знаний, а также упражнения, которые помогут применить на практике по­
лученные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выпол­
нить задания, а потом сверьте полученные результаты с ответами в приложении Д,
“Ответы”. Если остались неясными хотя бы некоторые из предложенных ниже во­
просов, не приступайте к изучению материала следую щ его занятия.

Контрольные вопросы
1. Какой контейнер вы выберете, если хранимый массив объектов требует воз­
можности вставки и в начало, и в конец?
2. Необходимо хранить элементы для быстрого поиска. Какой контейнер вы вы­
брали бы в этом случае?
3. Необходимо хранить элементы в контейнере s t d : : s e t , но при этом необходима
возможность изменения критериев поиска на основании условия, которое не
обязательно связано со значением элементов. Возможно ли удовлетворить этим
требованиям?
4. Какая возможность библиотеки STL позволяет соединить алгоритмы с контей­
нерами?
5. Выбрали бы вы контейнер h a sh s e t для приложения, которое должно быть
перенесено на различные платформы и компилироваться разными компилято­
рами C++?

ЗАНЯТИЕ 16

Класс строки
библиотеки STL
Стандартная библиотека шаблонов (STL) предоставляет
программистам контейнерный класс, облегчающий опера­
ции со строками и манипулирование ими. Класс s t r in g не
только динамически изменяет свои размеры, чтобы удовлет­
ворить требованиям приложения, но и предоставляет полез­
ные вспомогательные функции (или методы), помогающие
манипулировать строками. Таким образом, он позволяет
программистам использовать стандартные, переносимые и
проверенные функциональные возможности в своих прило­
жениях.
На этом занятии...
а

Зачем нужны классы обработки строк

■ Как работать с классом s t r in g библиотеки STL
■ Как библиотека STL облегчает такие операции со строка­
ми, как конкатенация, добавление, поиск и др.
■ Как использовать шаблонную реализацию строк библио­
теки STL
■ Оператор ""s, поддерживаемый STL s t r in g (со стандарта
С++1 4)

440

ЗАНЯТИЕ 16. Класс строки библиотеки STL

Потребность в классах обработки строк
Строка в языке C++ — это массив символов. Как вы уже видели на занятии 4,
“Массивы и строки”, простейший символьный массив может быть определен следую­
щим образом:
char staticName[20];

Здесь объявляется символьный массив (именуемый также строкой) фиксирован­
ной (статический) длины в 20 элементов. Очевидно, что этот массив может содержать
строку ограниченной длины; он окажется переполненным при попытке сохранить в
нем большее количество символов. Изменение размеров такого статического массива
невозможно. Для преодоления этого ограничения язык C++ предоставляет динамиче­
ское распределение памяти для данных. Вот более динамичное представление стро­
кового массива:
char* dynamicName = new char[arrayLen];

Это динамически распределенный символьный массив, длина экземпляра которого
может быть задана при создании значением переменной arrayLen, определяемым во
время выполнения, а следовательно, способным содержать данные переменной дли­
ны. Но если понадобится изменить длину массива во время выполнения, то придется
сначала освободить распределенную память, а затем повторно выделить ее для содер­
жания необходимых данных.
Ситуация усложняется, если такие символьные строки используются как данныечлены класса. В ситуациях, когда объект такого класса присваивается другому, при
отсутствии грамотно созданного копирующего конструктора и оператора присваива­
ния оба эти объекта будут содержать копии указателя, указывающего на один и тот же
строковый буфер, т.е. на одну и ту же область памяти. В результате удаления одного
объекта указатель в другом объекте оказывается недействительным (указывающим на
освобожденную область памяти, которая может быть использована для других нужд),
а ваша программа сталкивается с нешуточными неприятностями.
Строковые классы решают эти проблемы самостоятельно. Строковый класс
s t d : : s t r in g библиотеки STL моделирует символьную строку, а класс std : :w strin g —
широкосимвольную строку, помогая вам следующим образом.
■ Сокращает усилия по созданию строк и управлению ими.
■ Увеличивает стабильность приложения за счет инкапсуляции подробностей рас­
пределения памяти.
■ Встроенный копирующий конструктор и оператор присваивания автоматически га­
рантируют корректность копирования строковых членов классов.
■ Предоставляет полезные вспомогательные функции, помогающие в копировании,
усечении, поиске и удалении.
■ Предоставляет операторы для сравнения.
■ Позволяет сосредоточить усилия наосновных требованиях вашего приложения, а
не на подробностях обработки строк.

Работа с классом строки STL

ПРИМЕЧАНИЕ

|

441

s t d : : s t r i n g и s td : :w str in g являются специали­
s t d :: b a s ic _ s tr in g < T > для
типов ch ar и wchar t соответственно. Изучив его использование под­
Фактически классы

зациями одного и того же шаблона класса

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

Давайте на примере класса s t d : : s t r i n g изучим некоторые из вспомогательных
функций, предоставляемых строковыми классами библиотеки STL.

Работа с классом строки STL
Наиболее популярные строковые функции приведены ниже.
■ Копирование
■ Конкатенация
■ Поиск символов и подстрок
■ Усечение
■ Обращение строк и смены регистра символов с использованием алгоритмов, пре­
доставляемых стандартной библиотекой
Для использования строковых классов STL необходимо включить в код заголовоч­
ный файл < s tr in g > .

Создание экземпляров и копий строк STL
Класс s t r i n g предоставляет множество перегруженных конструкторов, а потому
его экземпляр может быть создан и инициализирован различными способами. Напри­
мер, можно инициализировать объект класса s t d : : s t r in g строкой или присвоить ему
постоянный символьный строковый литерал:
const char* constCStyleString = "Hello String!";
std::string strFromConst(constCStyleString);

или
std::string strFromConst = constCStyleString;

Приведенный выше фрагмент аналогичен следующему коду:
std::string str2("Hello String!");

Как можно заметить, создание объекта класса s t r i n g и его инициализация значе­
нием не требовали указания длины строки или подробностей распределения памя­
ти — конструктор класса s t r i n g делает все это автоматически.
Точно так же вполне возможно использовать один объект класса s t r i n g для ини­
циализации другого:
std::string str2Copy(str2);

442

ЗАНЯТИЕ 16. Класс строки библиотеки STL

Вы можете также указать конструктору класса s t r i n g , что для инициализации
строки следует принять только п первых символов передаваемой исходной строки:
// Инициализировать строку первыми 5 символами другой строки
std::string strPartialCopy(constCStyleString, 5);

Можно также инициализировать строку некоторым количеством определенного
символа:
// Инициализировать строку 10 символами ’а'
std::string strRepeatChars(10, 'а');

В листинге 16.1 анализируются некоторые наиболее популярные способы создания
экземпляров класса s t d : : s t r i n g и копирования строк.
Л И СТИ Н Г 1 6 .1 .

0:
1:
2:
3:
4:
5:
6:
7:

Создание экземпляров строк STL и их копирование___________________

#include
#include
int main()
{
using namespace std;
const char* constCStyleString = "Hello String!";
cout « "Константная строка: " « constCStyleString «

endl;

8:

9:
10:

std::string strFromConst(constCStyleString); // Конструктор
cout « "strFromConst: " « strFromConst « endl;

11 :
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25: }

std::string str2("Hello String!");
std::string str2Copy(str2);
cout « "str2Copy: " « str2Copy «

endl;

// Инициализировать строку первыми 5 символами другой строки
std::string strPartialCopy(constCStyleString, 5);
cout « "strPartialCopys: " « strPartialCopy « endl;
// Инициализировать строку 10 символами ’a'
std::string strRepeatChars(10, 'a');
cout « "strRepeatChars: " « strRepeatChars «
return 0;

Результат
Константная строка: Hello String!
strFromConst: Hello String!
str2Copy: Hello String!
strPartialCopy: Hello
strRepeatChars: aaaaaaaaaa

endl;

Работа с классом строки STL

443

Анализ
Приведенный выше код демонстрирует способы создания экземпляров класса
и его инициализации другой строкой, частичной копией и набором повторяю­
щихся символов. Символьная строка c o n s t C S t y l e S t r i n g в стиле С инициализиро­
вана значением в строке 6. Строка 9 демонстрирует, насколько просто конструктор
класса s t d : : s t r i n g позволяет создать копию этого значения. Строка 12 копирует в
объект s t r 2 класса s t d : : s t r i n g другую постоянную строку, а в строке 13 представ­
лен другой перегруженный конструктор класса s t d : : s t r i n g , позволяющий скопи­
ровать объект класса s t d : : s t r i n g и получить новую строку s t r 2 C o p y , являющуюся
точной копией исходной. Строка 17 демонстрирует частичное копирование, а стро­
ка 21 возможность создания экземпляра класса s t d : : s t r i n g и его инициализацию
повторяющимся символом. Этот пример кода демонстрирует отнюдь не все способы
того, как класс s t d : : s t r i n g и его многочисленные копирующие конструкторы облег­
чают разработчику создание строк, их копирование и отображение.
s t r in g

ПРИМЕЧАНИЕ

Если бы вы должны были использовать для подобного копирования строки
в стиле С, то эквивалент строки 9 листинга 16.1 выглядел бы следующим
образом:

const char* constCStyleString = "Hello World!";
// Выделение памяти для создания строки
char * pszCopy = new char[strlen(constCStyleString)+1];
strcpy(pszCopy, constCStyleString); // Копирование

// Освобождение памяти после использования pszCopy
delete!] pszCopy;
Как видите, здесь куда больше строк кода и выше вероятность ошибки.
Кроме того, необходимо позаботиться об управлении памятью и ее осво­
бождении. Класс s t r i n g библиотеки STL делает все это - и еще многое вместо вас!

Доступ к символу в строке s t d : : s t r i n g
К символьному содержимому строки STL можно обратиться с помощью итератора
или синтаксиса в стиле массива, в котором используется оператор индексации [ ]. По­
лучить представление строки в стиле С можно с помощью функции-члена c _ s t r ()
(листинг 16.2).
ЛИСТИНГ 16.2. Два способа обращения к символу строки STL_________________________
0: #include
1: #include
2
3 int main()

444
4: {
5:

|

ЗАНЯТИЕ 16. Класс строки библиотеки STL

using namespace std;

6:

7:

string stlString("Hello String"); // Пример строки

8:

9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38: }

// Доступ к содержимому строки: синтаксис обращения к массиву
c o u t « "Синтаксис обращения к массиву:" « endl;
for(size_t charCounter = 0;
charCounter < stlString.length();
++charCounter )
{
cout « "Символ!" « charCounter « "] = ";
cout « stlString[charCounter] « endl;
}
cout « endl;
// Доступ к содержимому строки с использованием итератора
cout « "Вывод с использованием итератора:" « endl;
int charOffset = 0;
string::const_iterator charLocator;
for(auto charLocator = stlString.cbegin();
charLocator != stlString.cend();
++charLocator )
{
cout « "Символ!" « charOffset++ «
"] = ";
cout « *charLocator « endl;
}
cout « endl;
// Обращение к содержимому строки в стиле С
cout « "Представление строки как char* = ";
cout « stlString.c_str() « endl;
return 0;

Результат
Синтаксис обращения к массиву:
Символ[0] = Н
Символ[1] = е
Символ [2] = 1
Символ [3] = 1
Символ[4] = о
Символ[5] =
Символ[6] = S
Символ[7] = t
Символ[8] = г
Символ[9] = i
Символ[10] = п
Символ[11] = g

Работа с классом строки STL

| 445

Вывод с использованием итератора:
Символ[0] = Н
Символ[1] = е
Символ [2] = 1
Символ[3] = 1
Символ[4] = о
Символ[5] =
Символ[б] = S
Символ[7] = t
Символ[8] = г
Символ[9] = i
Символ[10] = п
Символ[11] = g
Представление строки как char* =

Hello String

Анализ
Код демонстрирует несколько способов обращения к содержимому строки. Ите­
раторы важны в том смысле, что большинство функций-членов класса s t r i n g воз­
вращают свои результаты в форме итераторов. Строки 11-17 отображают символы
строки с использованием предоставляемого классом s t d : : s t r i n g оператора индек­
сации [ ], как у массива. Обратите внимание, что этому оператору нужно передавать
смещение символа от начала массива, как это делается в строке 16. Очень важно не
пересечь границы строки, т.е. вы не должны читать символы со смещением, большим,
чем длина строки. Строки 2 4 -3 0 также посимвольно отображают содержимое строки,
но уже с использованием итератора.

СОВЕТ

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

auto, тем самым поручая

компилятору вывести тип переменной на основе возвращаемого значения
метода

s t d : : s t r i n g :: c b e g in (), как это сделано в строке 24.

Конкатенация строк
Конкатенация строк может быть осуществлена с помощью либо оператора +=, либо
функции-члена append ():
string sampleStrl("Hello");
string sampleStr2(" String!");
sampleStrl += sampleStr2; // Использование std::string::operator+=
// Альтернативный вариант - функция std::string::append()
sampleStrl.append(sampleStr2); // (Перегружена также для char*)

В листинге 16.3 демонстрируется применение этих двух вариантов.

446

|

ЗАНЯТИЕ 16. Класс строки библиотеки STL

ЛИСТИНГ 16.3. Конкатенация строк с использованием оператора
сложения с присваиванием (+=) или метода append ()______________________
0: #include
1: #include
2:

3: int main ()

4: {
5:

using namespace std;

6:

7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23: }

string sampleStrl("Hello”);
string sampleStr2(" String!");
// Конкатенация
sampleStrl += sampleStr2;
cout « sampleStrl « endl «

endl;

string sampleStr3(" Указатели можно не использовать!");
sampleStrl.append(sampleStr3);
cout « sampleStrl « endl « endl;
const char* constCStyleString = " Но можно и использовать!";
sampleStrl.append(constCStyleString) ;
cout « sampleStrl « endl;
return 0;

Результат
Hello String!
Hello String! Указатели можно не использовать!
Hello String! Указатели можно не использовать! Но можно и использовать!

Анализ
Строки 11, 15 и 19 демонстрируют различные способы конкатенации строк STL.
Обратите внимание на использование оператора += и на возможность функции
append () (у которой есть множество перегруженных версий) получать как строковые
объекты (как показано в строке 11), так и символьные строки в стиле С.

Поиск сим вола или подстроки в строке
Класс s t r i n g библиотеки STL предоставляет несколько перегруженных версий
функции-члена f i n d ( ) , которая позволяет найти символ или подстроку в данном
объекте класса s t r in g .

Работа с классом строки STL

447

// Найти подстроку "day" в строке sampleStr, начиная поиск с позиции О
size_t charPos = sampleStr.find("day", 0);
// Удостовериться, что подстрока найдена, сравнивая с string::npos
if (charPos != string::npos)
cout « "Подстрока \"day\" найдена в позиции " « charPos;
else
cout « "Подстрока не найдена." « endl;

В листинге 16.4 демонстрируется удобство применения метода s t d : : s t r i n g : :
f i n d ().
ЛИСТИНГ 16.4. Использование метода s t r i n g : : f in d ()
для поиска подстроки или символа
0: iinclude
1: #include
2:

3: int main()
4: {
5:
using namespace std;
6:

7:
8:
9

string sampleStr("Good day String! Today is beautiful!");
cout « "Исходная строка:" « "\n" « sampleStr « "\n\n";

:

10:
11:

// Поиск "day"
size_t charPos

- find() возвращает позицию
= sampleStr.find("day", 0);

12:
13:
14:
15:
16:
17:
18:
19:
20:

// Проверка, что подстрока найдена...
if (charPos != string::npos)
cout « "\"day\" найдено в позиции " « charPos «
else
cout « "Подстрока не найдена." « endl;

endl;

cout « "Поиск всех подстрок \"day\"" « endl;
size_t subStrPos = sampleStr.find("day", 0);

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33: }

while(subStrPos != string::npos)
{
cout « "Найден \"day\" в позиции " «

subStrPos «

// Продолжаем поиск со следующего символа
size_t searchOffset = subStrPos + 1;
subStrPos = sampleStr.find("day", searchOffset);
)
return 0;

endl;

448

|

ЗАНЯТИЕ 16. Класс строки библиотеки STL

Результат
Исходная строка:
Good day String! Today is beautiful!
"day" найдено в позиции 5
Поиск всех подстрок "day"
Найден "day" в позиции 5
Найден "day" в позиции 19

Анализ
Строки 11-17 демонстрируют простейший случай применения функции f in d () —
поиск в строке определенной подстроки. Результат вызова метода fin d () сравнивает­
ся со значением s t d : : s t r i n g : :n p os (фактически это значение -1); если они равны,
искомый элемент в строке не найден. Если функция f in d () возвращает значение, не
равное n p os, это значение является смещением, указывающим позицию найденных
подстроки или символа в строке.
Далее код демонстрирует применение функции f in d () в цикле w h ile для поиска
всех вхождений символа или подстроки в строку STL. Здесь используется версия функ­
ции f in d (), получающая два параметра: искомую подстроку или символ и смещение
поиска, означающее точку, начиная с которой осуществляется поиск. В строке 29 в
качестве второго параметра для поиска очередного вхождения подстроки мы переда­
ем увеличенную на единицу позицию предыдущего вхождения искомой подстроки.

Строки STL предоставляют также функции, родственные функции f i n d (),
такие как f i n d _ f i r s t _ o f () , f i n d _ f i r s t _ n o t _ o f ( ) , f i n d _ l a s t _
o f () и f i n d _ l a s t _ n o t _ o f () , и предоставляющие программисту до­
полнительные возможности поиска.

Усечение строк STL
Класс s t r i n g библиотеки STL предоставляет функцию-член e r a s e (), осущ ест­
вляющую удаление:
■ некоторого количества символов, если заданы смещение позиции и количество уда­
ляемых символов:
string sampleStr("Hello String! Wake up to a beautiful day!");
sampleStr.erase(13, 28); // Hello String!

■ отдельного символа при наличии указывающего на него итератора:
sampleStr.erase(iCharS); // Итератор указывает удаляемый символ

■ множества символов, находящихся между двух итераторов:
// Удалить все символы от начала до конца
sampleStr.erase(sampleStr.begin(), sampleStr.end());

Работа с классом строки STL

449

Пример в листинге 16.5 демонстрирует применение различных версий функции
s t r i n g : : erase ().
ЛИСТИНГ 16.S. Использование функции s t r i n g : : erase () для усечения строки_______
0:
#include
1: #include
2:
#include
3:
4: int main()
5: {
6:
7:
8:
9:
10:

using namespace std;
string sampleStr("Hello String! Wake up to a beautiful day!");
cout « "Исходная строка: "«
endl;
cout « sampleStr «
endl « endl;

11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37: }

// Удалить из строки символы, заданные позицией и количеством
cout « "Удаление второго предложения: " « endl;
sampleStr.erase(13, 28);
cout « sampleStr « endl « endl;
// Найти в строке символ 'S', используя алгоритм поиска
string:-.iterator iCharS = find (sampleStr .begin (),
sampleStr.end(), 'S');
// Если символ найден, удаляем его
cout « "Удаление ’S' из исходной строки:" «
if (iCharS != sampleStr.end())
sampleStr.erase(iCharS);
cout «

sampleStr «

endl «

endl;

endl;

// Удаление диапазона символов
cout « "Удаление символов от begin() до end(): " « endl;
sampleStr.erase(sampleStr.begin(), sampleStr.end());
// Проверка длины строки после операции erased
if (sampleStr.length() == 0)
cout « "Строка пуста" « endl;
return 0;

Результат
Исходная строка:
Hello String! Wake up to a beautiful day!

450

|

ЗАНЯТИЕ 16. Класс строки библиотеки STL

Удаление второго предложения:
Hello String!
Удаление 'S' из исходной строки:
Hello tring!
Удаление символов от begin() до end():
Строка пуста

Анализ
Листинг демонстрирует три версии функции e r a s e (). Одна версия удаляет на­
бор символов, заданных начальным смещением и количеством, как показано в стро­
ке 14. Вторая версия удаляет определенный символ, заданный указывающим на него
итератором, как показано в строке 24. Последняя версия удаляет диапазон символов,
заданных парой итераторов, определяющих границы этого диапазона (строка 30).
Поскольку границы этого диапазона предоставлены функциями-членами b e g i n ()
и e n d () класса s t r i n g , диапазон включает все содержимое строки, и вызов метода
e r a s e () для него полностью удаляет все содержимое строки. Обратите внимание, что
класс s t r i n g предоставляет также функцию c l e a r (), которая эффективно очищает
внутренний буфер и выполняет сброс объекта класса s t r i n g .

СОВЕТ

Стандарт C++11 позволяет упростить пространное объявление итератора,
представленное в листинге 16.5:

string::iterator iCharS = find(sampleStr.begin(),
sampleStr.endO , 'S');
Чтобы сократить его, можно использовать ключевое слово a u t o , как было
продемонстрировано на занятии 3, “Использование переменных и кон­
стант”:

auto iCharS = find (sampleSt г. begin (), sampleStr.endO,

'S');

Компилятор автоматически выводит тип переменной iC h a r S , получая ин­
формацию о типе возвращаемого значения от функции s t d : : f i n d .

Обращение строки
Иногда необходимо изменить порядок символов в строке на обратный. Предполо­
жим, необходимо определить, не является ли введенная пользователем строка палин­
дромом, т.е. строкой, одинаково читаемой как с начала, так и с конца. Один из спосо­
бов сделать это подразумевает изменение порядка букв в копии содержимого строки
на обратный и сравнение с оригиналом. Обобщенный алгоритм s t d : : r e v e r s e () биб­
лиотеки STL позволяет обратить содержимое строки:
string sampleStr("Hello String! We will reverse you!");
reverse(sampleStr.begin(), sampleStr.endO);

Работа с классом строки STL

|

451

В листинге 16.6 показано применение алгоритма s t d : : r e v e r s e () к объекту класса
s t d : : s t r in g .

ЛИСТИНГ 16.6, Обращение строки с использованием алгоритма s t d : : r e v e r s e ()______
0:
#include
1:
#include
2:
#include
3:
4: int main()
5: {
6:
7:
8:
9:
10:

using namespace std;
string sampleStr("Hello String! We will reverse you!");
cout « "Исходная
строка: " « endl;
cout « sampleStr
« endl « endl;

11:
12:
13:
14:
15:
16:
17:
18: }

reverse(sampleStr.begin(), sampleStr.end());
cout « "После применения алгоритма std::reverse: " «
cout « sampleStr «
endl;

endl;

return 0;

Результат
Исходная строка:
Hello String! We will reverse you!
После применения алгоритма std::reverse:
!uoy esrever lliw eW JgnirtS olleH

Анализ
Алгоритм s t d : : r e v e r s e (), использованный в строке 12, работает в контейнере в
пределах, заданных двумя входными параметрами. В нашем случае эти пределы —
это начало и конец строкового объекта, так что обращается порядок символов всей
строки. Строку можно обратить и частично, задавая соответствующие границы. Об­
ратите внимание: границы не должны превышать значение end ().

Смена регистра символов
Для смены регистра символов используется алгоритм s t d : : t r a n s f o r m ! ), при­
меняющий определенную пользователем функцию к каждому элементу коллекции.
В данном случае коллекция — это не что иное, как объект класса s t r i n g . Пример в
листинге 16.7 демонстрирует смену регистра символов в строке.

452

|

ЗАНЯТИЕ 16. Класс строки библиотеки STL

ЛИСТИНГ 16.7. Преобразование строки в верхний регистр
с использованием алгоритма s t d : : tr a n s fo r m ()____________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include
#include
#include
int main ()
{
using namespace std;
cout « "Введите строку для преобразования:” «
cout « "> ";

endl;

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24: }

string inStr;
getline(cin, inStr);
cout « endl;
transform(inStr.begin(),inStr.end(),inStr.begin(), ::toupper);
cout « "Преобразованная в верхний регистр строка:" « endl;
cout « inStr « endl
« endl;
transform(inStr.begin(),inStr.end(),inStr.begin(), ::tolower);
cout « "Преобразованная в нижний регистр строка:" « endl;
cout « inStr « endl
« endl;
return 0;

Результат
Введите строку для преобразования:
> ConverT t h i s StrINg!
Преобразованная в верхний регистр строка:
CONVERT THIS STRING!
Преобразованная в нижний регистр строка:
convert this string!

Анализ
Строки 15 и 19 демонстрируют, насколько эффективно можно применить алгоритм
s t d : : tr a n sfo r m () для изменения регистра содержимого строки.

Реализация строки на базе шаблона STL

|

453

Реализация строки на базе шаблона STL
Как уже упоминалось, класс s t d : : s t r i n g фактически представляет собой спе­
циализацию шаблонного класса STL s t d : : b a s ic _ s tr in g < T > . Объявление шаблона
контейнерного класса b a s i c _ s t r i n g имеет следующий вид:
template
basic_string

В этом определении шаблона крайне важен первый параметр: _Е 1ет. Это тип
объектов, хранимых коллекцией b a s ic _ s t r in g . Таким образом, класс s t d : : s t r in g —
это специализация шаблона b a s i c _ s t r i n g для _E lem =char, в то время как класс
w str in g — это специализация того же шаблона для _Elem =wchar_t.
Другими словами, класс s t r i n g библиотеки STL определяется так:
typedef basic_stringname < itemToCompare.name);

}
// Используется в displayAsContents для вывода в cout
operator const char*() const

{
return displayAs.c_str();

}
};
bool SortOnphoneNumber(const ContactltemS iteml,
const ContactItem& item2)
{
return (iteml.phone < item2.phone);
}
int main ()
{
list contacts;
contacts.push_back(ContactItem("Jack Welsch","+17889879879”));
contacts.push_back(ContactItem("Bill Gates","+197789787998"));
contacts.push_back(ContactItem("Angi Merkel","+49234565466")) ;
contacts.push_bacк (ContactItem("Dim Medvedev","+766454564797"))
contacts.push_back(ContactItem("Ben Affleck","+1745641314"));
contacts.push_back(ContactItem("Dan Craig","+44123641976"));
cout « "Список в исходном порядке: " «
displayAsContents(contacts);

endl;

66:

67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:

contacts.sort();
cout « "Сортировка с помощью оператора из листин­
га 21.1 делает весь код компактнее, включая определение структуры и ее
применение в трех строках функции m a in (), заменяя строки 2 1 -2 4 сле­
дующими:

Типичные приложения функциональных объектов

541

// Вывод элементов с использованием лямбда-выражения
for_each(numsInVec.begin(), 11 Начало диапазона
numsInVec.end(),
/ / Конец диапазона
[] (int& element) {cout«element«' ’;});
/ / В предыдущей строке - лямбда-выражение
Лямбда-выражения - фантастическое усовершенствование C++, и вы не­
пременно должны изучить их на занятии 22, “Лямбда-выражения языка
С++11”. Листинг 22.1 демонстрирует использование лямбда-функции в ал­
горитме f o r _ e a c h () для отображения содержимого контейнера вместо
функционального объекта, как в листинге 21.1.

Реальное преимущество использования функционального объекта, реализованного
в структуре, становится очевидным, когда объект структуры используется для хране­
ния информации. Это то, чего не может обеспечить функция F u n c D i s p la y E l e m e n t ( ) .
Дело в том, что структура способна не только реализовать o p e r a t o r ( ) , но и иметь
данные-члены. Вот несколько измененная версия кода, в которой используются атри­
буты структуры:
template ctypename elementType>
struct DisplayElementKeepCount
{
int count;
DisplayElementKeepCount() // Конструктор

{
count = 0;

}
void operator ()(const elementType& element)

{
++count;
cout « element «

1 ’;

}
};
В приведенном фрагменте структура D is p la y E le m e n t K e e p C o u n t немного моди­
фицирована по сравнению с предыдущей версией. Оператор o p e r a t o r () больше не
является константной функцией-членом, поскольку он выполняет инкремент значения
переменной-члена c o u n t (а следовательно, изменяет ее), используемой для хранения
количества вызовов для отображения данных. Такой подсчет возможен благодаря от­
крытому атрибуту c o u n t . В листинге 21.2 показано применение функционального
объекта, способного хранить состояние.
ЛИСТИНГ 21.2. Использование функционального объекта, хранящего состояние_______
0: #include
1: #include
2: #include

542
3:
4:
5:
6:
7:
8:

ЗАНЯТИЕ 21. Понятие о функциональных объектах

|

using namespace std;
templatectypename elementType>
struct DisplayElementKeepCount
{
int count;

9 :

10:

DisplayElementKeepCount() : count(0) {} // Конструктор

11 :
12:
13:
14:
15:
16:
17: };
18:
19: int
20: {
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32: }

void operator()(const elementTypefc element)
{
++count;
cout « element« '
}

main()
vector numsInVec{ 22, 2011, -1, 999, 43, 901 };
cout « "Вывод содержимого вектора: " « endl;
DisplayElementKeepCount result;
result = for_each(numsInVec.begin(),
numsInVec.end(),
DisplayElementKeepCount());
cout «

endl «

"Функтор вызван " «

result.count «

" раз.";

return 0;

Результат
Вывод содержимого вектора:
22 2017 -1 999 43 901
Функтор вызван 6 раз.

Анализ
Самое большое различие между этим примером и листингом 21.1 заключается
в использовании структуры D is p l a y E l e m e n t K e e p C o u n t в качестве возвращаемого
значения алгоритма f o r _ e a c h (). Оператор o p e r a t o r (), реализованный в структуре
D is p la y E le m e n t K e e p C o u n t , вызывается алгоритмом f o r _ e a c h () для каждого элемен­
та в контейнере. Этот оператор выводит на экран элемент и увеличивает внутренний
счетчик, хранящийся в переменной c o u n t . После завершения работы алгоритма f o r _
e a c h () возвращенный им объект используется в строке 29 для вывода количества об­
работанных элементов. Обратите внимание, что использование в этом случае обычной
функции вместо реализованного в структуре оператора не позволило бы использовать
счетчик так просто.

Типичные прилож ения ф ункциональны х об ъ ектов

|

543

Унарный предикат
Унарная функция, которая возвращает значение типа b o o l, является предикатом и
помогает алгоритмам STL принимать решения. В листинге 21.3 приведен пример пре­
диката, определяющий, является ли вводимый элемент кратным исходному значению.
Л И С Т И Н Г 2 1 .3 . У н а р н ы й предикат, о п р е д е л я ю щ и й ,
яв л яе тся ли о д н о ч и сл о к р а т н ы м д р у го м у ___________________________________________________

0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

// Структура, выступающая унарным предикатом
template
struct IsMultiple
{
numberType Divisor;
IsMultiple(const numberType& divisor)
{
Divisor = divisor;
}

10:
11:

bool operator ()(const numberType& element) const

12 :

{

13:
14:
15:
16: };

// Проверка, кратен ли аргумент делителю
return ((element % Divisor) == 0);
}

Диализ
Здесь оператор o p e r a t o r ( ) возвращает тип b o o l и может работать в качестве
унарного предиката. Структура имеет конструктор и инициализируется значением
делителя. Это значение хранится в объекте, а затем используется для определения,
делится ли на него переданный в качестве аргумента элемент. Как можно заметить, в
реализации оператора () используется оператор деления по модулю %, который воз­
вращает остаток от деления на значение D i v i s o r . Предикат сравнивает этот остаток с
нулем, чтобы проверить кратность чисел.
В листинге 21.4, как и в листинге 21.3, предикат используется для определения
кратности чисел заданному пользователем делителю.
Л И С Т И Н Г 2 1 .4 . И с п о л ь з о в а н и е у н а р н о го п р е д и к а та I s M u l t i p l e с а л го р и т м о м
s t d : : f i n d _ i f () для п о и с к а э л е м е н т а , к р а т н о го з а д а н н о м у п о л ь з о в а т е л е м д е л и те л ю

0:
1:
2:
3:
4:
5:

#include
linclude
#include
using namespace std;
// Вставка кода из листинга 21.3

544

|

ЗАНЯТИЕ 21. Понятие о функциональных объектах

6: int main ()
7: {
8:
vector numsInVec{ 25, 26, 27, 28, 29, 30, 31 };
9:
cout « "Вектор содержит: 25, 26, 27, 28, 29, 30, 31" «

endl;

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

cout « "Введите делитель (> 0): ";
int divisor = 2;
cin » divisor;

21 :

{

// Поиск первого кратного делителю
auto element = find_if(numsInVec.begin(),
numsInVec.endf),
IsMultiple(divisor));
if(element != numsInVec.endf))

22:
23:
24:
25:
26:
27: }

cout «
cout «

"Первый кратный " « divisor;
" элемент - " « ^element « endl;

}
return

0;

Результат
Вектор содержит: 25, 26, 27, 28, 29, 30, 31
Введите делитель (> 0): 4
Первый кратный 4 элемент - 28

Анализ
Пример начинается с простого контейнера — вектора целых чисел. Применение
унарного предиката осуществляется в алгоритме поиска f i n d _ i f (), показанного в
строках 16-18. Функциональный объект I s M u l t i p l e < i n t > () инициализируется пре­
доставляемым пользователем значением делителя, которое сохраняется в переменнойчлене D i v i s o r . Алгоритм f i n d _ i f () вызывает оператор I s M u l t i p l e : : o p e r a t o r ()
унарного предиката для каждого элемента в указанном диапазоне. Когда o p e r a t o r ()
возвращает значение t r u e (что происходит, когда элемент делится без остатка на 4),
алгоритм f i n d _ i f () возвращает итератор e le m e n t , указывающий на этот элемент.
Результат вызова f i n d i f () сравнивается с результатом вызова метода e n d () контей­
нера, чтобы удостовериться в успеш ности поиска элемента (строка 20). Затем полу­
ченный итератор e le m e n t используется для отображения значения (строка 23).

СОВЕТ

Чтобы увидеть, как применение лямбда-выражений повышает компактность
программы, представленной в листинге 21.4, взгляните на листинг 22.3 за­
нятия 22, “Лямбда-выражения языка C++И ”.

Типичные приложения функциональных объектов

|

545

Унарные предикаты применяются в большом количестве алгоритмов STL, таких
как s t d : : p a r t i t i o n (), позволяющий разделить диапазон с помощью предиката, или
s t a b l e _ p a r t i t i o n (который делает то же самое с сохранением относительного по­
рядка разделяемых элементов). Еще одним примером могут служить функции поиска,
такие как s t d : : fin d _ _ if (), и функции наподобие s t d : : r e m o v e _ if (), позволяющие
удалять из определенного диапазона элементы, удовлетворяющие предикату.

Бинарные функции
Функции типа f (х, у) полезны, в частности, когда они возвращают некоторое зна­
чение, вычисляемое на основании полученных аргументов. Такие бинарные функции
применяются для вычисления арифметических действий с двумя операндами, напри­
мер таких, как сложение, умножение, вычитание и т.д. Типичная бинарная функция,
возвращающая произведение входных аргументов, может быть написана следующим
образом:
template
class Multiply
{
public:
elementType operator ()(const elementTypefc eleml,
const elementType& elem2)

{
return (eleml * elem2);

}
};
Представляющий интерес код находится в o p e r a to r (), который получает два ар­
гумента и возвращает их произведение. Подобные бинарные функции используются
в таких алгоритмах, как s t d : : t r a n s f o r m (), в которых их можно, например, исполь­
зовать для перемножения содержимого двух контейнеров. В листинге 21.5 показано
применение такого бинарного функтора в алгоритме s t d : : tra n sfo rm ().
Л И СТИ Н Г 2 1 .5 ,

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

0:
1:
2:
3:
4:
5:

template
class Multiply

6:

{

7:
8:
9:

linclude
#include
#include

public:
elementType

10:

{

11:

12 :
13: };

operator()(const elementType& eleml,
const elementType& elem2)

return (eleml * elem2);
}

546
14
15
16
17
18
19

20

|

ЗАНЯТИЕ 21. Понятие о функциональных объектах

int main()

{
using namespace std;
vector multiplicands{ 0, 1, 2, 3, 4 };
vector multipliers} 100, 101, 102, 103, 104 };

21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

// Третий контейнер хранит результат умножения
vector vecResult;
// Готовим место для результата умножения
vecResult.resize (multipliers .sized);
transform(multiplicands.begin(),// Диапазон множимых
multiplicands.end(), // Конец диапазона
multipliers.begin(), // Диапазон множителей
vecResult.begin(),
// Результаты
Multiply () );
// Операция умножения
cout « "Первый вектор:" « endl;
for(size_t index = 0; index < multiplicands.size(); ++index)
cout « multiplicands[index] « '
cout « endl;
cout « "Второй вектор:" « endl;
for(size_t index = 0; index < multipliers.size(); t+index)
cout « multipliers[index] « ’ ?;
cout « endl;
cout « "Результат умножения:" « endl;
for(size_t index = 0; index < vecResult.size(); ++index)
cout « vecResult[index] « 1 ’;
return 0;

Результат
Первый вектор:
0 12 3 4
Второй вектор:
100 101 102 103 104
Результат умножения:
0 101 204 309 416

Типичные приложения функциональных объектов

|

547

Анализ
В строках 4 -13 содержится класс M u l t i p l y , показанный в приведенном выше фраг­
менте кода. В данном примере алгоритм s t d : : t r a n s f o r m () используется для поэле­
ментного перемножения содержимого двух диапазонов и сохранения результата вычис­
ления в третьем. Рассматриваемые диапазоны содержатся в объектах m u l t i p l i c a n d s ,
m u l t i p l i e r s и v e c R e s u l t , все из которых представляют собой объекты класса
s t d : : v e c t o r . Другими словами, функция s t d : : t r a n s f o r m ( ) в строках 27-31 исполь­
зуется для умножения каждого элемента вектора m u l t i p l i c a n d s на соответствующий
элемент вектора m u l t i p l i e r s и сохраняет результат умножения в векторе v e c R e s u l t .
Само умножение осуществляется бинарной функцией M u l t i p l y : : o p e r a t o r (), кото­
рая вызывается для каждого элемента исходных диапазонов векторов. Возвращаемое
значение o p e r a t o r () сохраняется в векторе v e c R e s u l t .
Таким образом, этот пример демонстрирует применение бинарных функций для
выполнения арифметических операций с элементами в контейнерах STL. Пример, при­
веденный далее, также использует алгоритм s t d : : t r a n s f o r m (), но применяет его для
преобразования строки в строку строчных символов с помощью функции t o l o w e r ( ) .

Бинарный предикат
Бинарным предикатом обычно называется функция, которая получает два аргу­
мента и возвращает значение типа b o o l . Эти функции находят применение в таких
алгоритмах STL, как s t d : : s o r t ( ). Листинг 21.6 демонстрирует применение бинар­
ного предиката, который сравнивает две строки после их перевода в нижний регистр.
Такой предикат может применяться, например, при выполнении не зависящей от ре­
гистра сортировки вектора строк.
ЛИСТИНГ 21,6. Бинарный предикат для сортировки строк, не зависящей от регистра
0:
1:
2:
3:
4:
5:
6:
7:

8:
9:

#include
#include
using namespace std;
class CompareStringNoCase
{
public:
bool operator ()(const strings strl, const strings str2) const

{
string strlLowerCase;

10 :
11:
12:
13:
14:
15:
16:
17:

// Выделение памяти
strlLowerCase.resize(strl.size());
// Преобразование всех символов в нижний регистр
transform(strl.begin(), strl.endf),
strlLowerCase.begin(), ::tolower);

548

I

ЗАН ЯТИ Е 21.

18:
19:
20:
21:

Понятие о функциональных объектах

string str2LowerCase;
str2LowerCase.resize(str2.size());
transform(str2.begin(),str2.end (),
str2LowerCase.begin(), ::tolower);

22:
23:
24:
25: };

return (strlLowerCase < str2LowerCase);
}

Анализ
Бинарный предикат, реализованный в o p e r a to r (), сначала переводит введенные
строки в нижний регистр, используя алгоритм s t d : : t r a n s f o r m (), как показано в
строках 15 и 20, а затем использует оператор сравнения строк < для возврата резуль­
тата сравнения.
Вы можете использовать этот бинарный предикат с алгоритмом s t d : : s o r t () для
сортировки динамического массива, содержащегося в векторе строк, как показано в
листинге 21.7.
Использование функционального объекта класса
Com pareStringNoCase для не зависящей от регистра сортировки вектора строк_______

Л И СТИ Н Г 2 1 .7 .

0:
1:
2:
3:
4:
5:

// Здесь вставьте код класса CompareStringNoCase из листинга 21.6
#include
#include
template ctypename T>
void DisplayContents(const T& container)

6: {
7:
8:
9:
10:

for(auto element = container.cbegin();
element != container.cend();
t+element )
cout « *element « endl;

11: }
12 :
13: int main()
14: {
15:
// Определение вектора строк
16:
vector vecNames;
17:
18:
// Вставка в вектор нескольких имен
19:
vecNames.push_back("jim");
20:
vecNames.push_back("Jack”);
21:
vecNames.push_back("Sam");
22:
vecNames.push_back("Anna");
23
24
cout « "Имена в порядке вставки:" «

endl;

Типичные приложения функциональных объектов
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36: }

|

549

DisplayContents(vecNames);
cout « ’’Имена после сортировки по умолчанию:” «
sort(vecNames.begin(), vecNames.end());
DisplayContents(vecNames);

endl;

cout « "Имена после сортировки с предикатом:" « endl;
sort(vecNames.begin(), vecNames.end(), CompareStringNoCase());
DisplayContents(vecNames);
return 0;

Результат
Имена в порядке вставки:

jim
Jack
Sam
Anna
Имена после сортировки по умолчанию:
Anna
Jack
Sam
jim
Имена после сортировки с предикатом:
Anna
Jack
jim
Sam

Анализ
Вывод отображает содержимое вектора на трех этапах. На первом содержимое
отображается в порядке вставки. На втором этапе, после сортировки в строке 28 с
использованием заданного по умолчанию предиката сортировки le s s < T > , вывод
демонстрирует, что j i m располагается не после J a c k , поскольку эта сортировка за­
висит от регистра благодаря оператору s t r i n g : : o p e r a t o r s Последняя сортировка
в строке 32 использует класс предиката C o m p a r e S t r i n g N o C a s e o (реализованный в
листинге 21.6), который гарантирует, что j im будет следовать после J a c k несмотря на
различие в регистре.
Бинарные предикаты применяются во множестве алгоритмов STL. Например, в
алгоритме s t d : : u n i q u e (), удаляющем совпадающие соседние элементы, в алгорит­
мах s t d : : s o r t () и s t d : : s t a b l e _ s o r t ( ), выполняющих сортировку, в алгоритме
s t d : : t r a n s f o r m ( ) , позволяющем выполнить операцию над двумя диапазонами, и во
многих других алгоритмах STL, нуждающихся в бинарном предикате.

550

|

ЗАНЯТИЕ 21. Понятие о функциональных объектах

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

Вопросы и ответы
■ Предикат — это специальная категория функциональных объектов. Что дела­
ет его таким особенным?
Предикаты всегда возвращают логическое значение.

■ Какой функциональный объект следует использовать при вызове такой функ­
ции, как remove_if () ?
Вы должны использовать унарный предикат, который может получить в конструк­
торе дополнительную информацию, используемую при вычислении оператора
o p e r a t o r ().

■ Какой функциональный объект я должен использовать для контейнера тар?
Бинарный предикат.

■ Может ли простая функция без возвращаемого значения использоваться как
функтор?
Да. Функция без возвращаемых значений вполне может делать что-то полезное,
например выводить на экран входные данные.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Как называется унарная функция, возвращающая значение типа b o o l?
2. Есть ли польза от функционального объекта, который не изменяет данные и не
возвращает значения типа b o o l? Можете привести пример?
3. Каково определение термина функциональный объект ?

Коллоквиум

|

551

Упражнения
1. Напишите унарную функцию, которая применяется в алгоритме s t d : : f o r _
each () для вывода удвоенного входного параметра.
2. Дополните этот предикат так, чтобы можно было вывести количество его вы­
зовов.
3. Напишите бинарный предикат, обеспечивающий сортировку в порядке возрас­
тания.

ЗАНЯТИЕ 22

Лямбда-выражения
языка С++11
Лямбда-выражения (lambda expressions) — это компакт­

ное средство определения и создания функциональных
объектов без имени, введенный в стандарте С + +1 1.
На этом занятии...

■ Как создать лямбда-выражение
■ Использование лямбда-выражений в качестве предикатов
■ Обобщенные лямбда-выражения С + + 1 4
■ Как создать лямбда-выражение, способное хранить со­
стояние, и работать с ним

554

|

ЗАНЯТИЕ 22. Лямбда-выражения языка C++11

Что такое лямбда-выражение
Лямбда-выражение (или просто лямбда) можно считать компактной версией безы­
мянной структуры (или класса) с открытым оператором o p e r a to r (). В этом смысле
лямбда-выражение — это функциональный объект, подобный представленным на за­
нятии 21, “Понятие о функциональных объектах”. Прежде чем переходить к анализу
разработки лямбда-функций, рассмотрим функциональный объект из листинга 21.1:
// Структура, ведущая себя как унарная функция
template
struct DisplayElement
{
void operator ()(const elementType& element) const
{
cout «

element «

’ ';

};
Этот функциональный объект отображает на экране с использованием потока co u t
элемент и обычно используется в таких алгоритмах, как s t d : : f o r each ():
// Отобразить массив целых чисел
for_each(vecIntegers.begin(),
// Начало диапазона
veclntegers.end(),
// Конец диапазона
DisplayElement()); // Унарный функциональный объект

Лямбда-выражение позволяет компактно записать код, включив в вызов определе­
ние функционального объекта:
// Отобразить массив целых чисел, используя лямбда-выражения
for_each(vecIntegers.begin(),
//Начало диапазона
veclntegers.end(),
//Конец диапазона
[] (const int&element) { c o u t « element«' ';}); //Лямбда-выражение

Когда компилятор встречает лямбда-выражение, в данном случае это
[] (const int&element) {cout«element«'

';}

он автоматически разворачивает его в представление, п одобн ое структуре

D isp lay E lem en t< in t> :
struct NoName
{
void operator ()(const int& element) const
{
cout «

element «

’ ';

}
};

СОВЕТ

Лямбда-выражения называют также лямбда-функциями.

Как определить лямбда-выражение

|

555

Как определить лямбда-выражение
Определение лямбда-выражения должно начинаться с квадратных скобок ([ ]). Эти
скобки, по существу, говорят компилятору, что началось лямбда-выражение. За ними
следует список параметров, являющийся таким же списком параметров, как и тот,
который вы предоставили бы своей реализации оператора o p e r a to r (), если бы не
использовали лямбда-выражение.

Лямбда-выражение
для унарной функции
Лямбда-версия унарного оператора o p e r a to r (Туре), получающего один параметр,
имела бы следующий вид:
[](Type paramName)

{ /* Код лямбда-выражения */ }

Обратите внимание, что при необходимости параметр можно передать по ссылке:
[](Туре& paramName)

{ /* Код лямбда-выражения */ }

Листинг 22.1 демонстрирует применение лямбда-функции для отображения со­
держимого контейнера стандартной библиотеки шаблонов (STL) с использованием
алгоритма fo r _ e a c h ().
ЛИСТИНГ 22.1. Вывод элементов контейнера с помощью
алгоритма fo r _ e a c h () с лямбда-выражением
0:
1:
2:
3:
4:
5:

#include
#include
linclude
#include






using namespace std;

6:

7: int main()

8: {
9:

vector numsInVec{ 101, -4, 500, 21, 42, -1 };

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

21
22

list charsInList{ 'a', 'h 1, 'z ', 'k', fl' };
cout « "Вывод вектора с использованием лямбды:" « endl;
// Вывод массива целых чисел
for_each(numsInVec.cbegin(),
numsInVec.cend(),
[](const int& element){cout «

element «

cout « endl;
cout « "Вывод списка с использованием лямбды:" «
// Вывод списка символов

' ';});

endl;

556

|

23:
24:
25:
26:
27:
28: }

ЗАНЯТИЕ 22. Лямбда-выражения языка С++11
for_each(charsInList.cbegin(),
charsInList.cend(),
[](auto& element){cout «

element «

'

});

return 0;

Результат
Вывод вектора с использованием лямбды:
101 -4 500 21 42 -1
Вывод списка с использованием лямбды:
a h z k 1

Анализ
Интерес представляют два лямбда-выражения в строках 17 и 25. Они очень похо­
жи, если не учитывать тип входного параметра, поскольку они приспособлены к типу
элементов контейнеров, с которыми работают. Первое лямбда-выражение получает
один параметр типа i n t , поскольку оно используется для поэлементного вывода век­
тора целых чисел, тогда как второе получает параметр типа c h a r (автоматически вы­
водимого компилятором), поскольку предназначено для отображения элементов типа
c h a r , хранящихся в контейнере s t d : : l i s t .

СОВЕТ

Вы можете заметить, что второе лямбда-выражение в листинге 22.1 не­
много отличается от первого:

for_each(charsInList.cbegin(),
charsInList.cend(),
[] (auto& element) {cout «

element «

'

} );

Это лямбда-выражение использует возможности автоматического выво­
да типа компилятором, которое вызывается с помощью ключевого слова
a u to .

Это усовершенствование лямбда-выражений, поддерживаемое

С++14-совместимыми компиляторами. Компилятор трактует это лямбдавыражение как

for_each(charsInList.cbegin(),
charsInList.cend(),
□ (const char& element) {cout «

ПРИМЕЧАНИЕ

element «

' ';});

Вывод листинга 22.1 аналогичен выводу листинга 21.1. Фактически эта
программа - просто лямбда-версия листинга 21.1, в котором был исполь­
зован функциональный объект D is p la y E le m e n t < T > .
Сравнивая эти два листинга, можно прийти к выводу, чтолямбда-функции
обладают серьезным потенциалом, позволяющим сделать код C++ проще
и компактнее.

Лямбда-выражение для унарного предиката

| 557

Лямбда-выражение
для унарного предиката
Предикат позволяет принимать решения. Унарный предикат — это унарное выра­
жение, которое возвращает значение типа b o o l (tr u e или f a l s e ) . Лямбда-выражения
также могут возвращать значения. Например, следующий код представляет собой
лямбда-выражение, которое возвращает значение tr u e для четных чисел:
[](int& num) {return ((num % 2) == 0); }

Природа возвращаемого значения в данном случае указывает компилятору, что
лямбда-выражение возвращает тип b o o l.
Вы можете использовать данное лямбда-выражение, являющееся унарным преди­
катом, в таких алгоритмах, как s t d : : f in d i f (), для поиска четных чисел в коллек­
ции. Соответствующий пример приведен в листинге 22.2.

ЛИСТИНГ 22.2. Поиск четных чисел в коллекции с использованием
лямбда-выражения в качестве унарного предиката__________________________________
0:
1:
2:
3:
4:
5:

#include
#include
#include
using namespace std;

6:

{

int main()

7:

vector numsInVec{ 25, 101, 2017, -50 };

8:
9:
10:
11:

auto evenNum = find_if(numsInVec.cbegin(),
numsInVec.cend(),
[](const int& num){return ((num % 2) == 0); } );

12 :
13:
14:
15:
16:
17: }

if (evenNum != numsInVec.cend())
cout « "Четное число найдено: " «

*evenNum «

endl;

return 0;

Результат
Четное число найдено: -50

Анализ
Лямбда-функция, работающая как унарный предикат, представлена в строке 11. Ал­
горитм f i n d _ i f () вызывает унарный предикат для каждого элемента диапазона. Ког­
да предикат возвращает значение tr u e , алгоритм f i n d _ i f () сообщает о найденном

558

|

ЗАНЯТИЕ 22. Лямбда-выражения языка С++11

элементе, возвращая итератор, указывающий на найденный элемент. В нашем случае
предикат (лямбда-выражение) возвращает значение t r u e , когда алгоритм f i n d i f ()
встречает четное целое число (т.е. остаток от его деления на 2 равен нулю).

ПРИМЕЧАНИЕ

В листинге 22.2 демонстрируется не только лямбда-выражение, работаю­
щее как унарный предикат, но и использование ключевого слова c o n s t в
лямбда-выражениях.
Не забывайте использовать его для входных параметров, особенно если
это ссылки, чтобы избежать внесения непреднамеренных изменений в зна­
чения элементов в контейнере.

Лямбда-выражения с состоянием
и списки захвата [ . . . ]
В листинге 22.2 был создан унарный предикат, который возвращал значение t r u e ,
если целое число делилось на 2, т.е. было четным. Но что если нужна некоторая более
обобщенная функция, которая возвращает значение t r u e , если число делится на зна­
чение, предоставленное пользователем? Это значение необходимо хранить в лямбдавыражении как “состояние”:
int Divisor = 2 ;

// Исходное значение

auto element = find_if {Нача ло_диа па зона,

Конец_диапазона,
[Divisor](int dividend){return (dividend % Divisor) == 0; } );

Список аргументов, переданный в виде переменных состоянш (state variable) в
квадратных скобках ( [ . . . ] ) , называется также списком захвата (capture list) лямбдавыражения.

ПРИМЕЧАНИЕ

Такое лямбда-выражение - это однострочный эквивалент 16 строк кода в ли­
стинге 21.3, определяющем унарный предикат s t r u c t

I s M u lt ip le o .

Таким образом, введенные стандартом С++11 лямбда-выражения резко повы­
шают эффективность программирования и выразительность программ на C++.

В листинге 22.3 демонстрируется применение унарного предиката с переменной
состояния для поиска в коллекции числа, кратного предоставленному пользователем
делителю.
ЛИСТИНГ 22.3. Использование лямбда-выражений с сохранением состояния
0
1
2

#include
#include
iinclude

Лямбда-выражения с состоянием и списки захвата [...]

559

3: using namespace std;
4:
5: int m a i n ()

6: {
7:

vector numsInVec{25,

8:

cout «

"Вектор:

26,

21,

28, 29, 30, 31};

{25, 26, 27, 28, 29, 30, 31}";

9:
10:

cout «

11:

int divisor = 2;

endl «

12:

cin »

"Введите делитель {>

0): ";

divisor;

13:
14:

// Поиск первого элемента, кратного divisor

15:

vector ::iterator element;

16:

element = find_if(numsInVec.begin(),

17:

n umsInVec.endO,

18:

[divisor](int dividend){return

(dividend % divisor) == 0;});

19:
20:

if(element != numsInVec.endO )

21:

{

22:

cout «

"Первый элемент, делящийся на " «

23:

cout «

": " «

24:

^element «

divisor;

endl;

}

25:
26:
27: }

return

0;

Результат
Вектор:

{25, 26, 27, 28, 29, 30, 31}

Введите делитель

(> 0):

4

Первый элемент, делящийся на 4: 28

Анализ
Лямбда-выражение, хранящ ее состояние и работаю щ ее как предикат, на­
ходится в строке 18. Переменная состояния d i v i s o r аналогична переменной
I s M u lt ip le :: D iv is o r , которую вы видели в листинге 21.3. Следовательно, перемен­
ные состояния сродни членам класса функционального объекта, который вы исполь­
зовали до С++11. Таким образом, теперь вы можете передать состояние в лямбдафункцию и настроить ее применение с использованием этого состояния.
Листинг 22.3 использует лямбда-зквивалент функционального объекта из
листинга 21.4, но без класса. Возможности лямбда-выражений C++И сэ­
кономили нам 16 строк кода.

560

|

ЗАНЯТИЕ 22. Лямбда-выражения языка С-м-11

Обобщенный синтаксис
лямбда-выражений
Лямбда-выражение всегда начинается с квадратных скобок и может быть настрое­
но так, чтобы получать несколько переменных состояния, разделенных запятыми в
списке захвата [ . . . ] :
[Переменная1, Переменна я 2] [Тип^ Параметр) {/* Код лямбда-выражения */}

Если эти переменные состояния должны изменяться в пределах тела лямбдавыражения, добавьте ключевое слово m utable:

[Переменная1, Переменная2] [Тип& Параметр) mutable {/* Код */}
Обратите внимание, что в этом случае передаваемые в списке захвата [ ] перемен­
ные могут изменяться в пределах лямбда-выражения, но вовне эти изменения не пере­
даются. Если необходимо, чтобы внесенные в пределах лямбда-выражения изменения
переменных распространялись также вовне, следует использовать ссылки:
[SiПеременна я 1, &Переменна я 2 ] (Типк Параметр)

{/* Код */}

Лямбда-выражения могут получать несколько входных параметров, отделяемых
запятыми:

[Переменная] (Тип1 & Параметр1, Тип2^ Параметр2) {/* Код */}
Если вы хотите указать тип возвращаемого значения и не создавать неоднознач­
ности для компилятора, используйте оператор -> так, как показано далее:

[Переменная 1, Переменна я 2} [ТипЬ Параметр) -> Возвращаемый_Тип
{/* Код лямбда-выражения */}

И наконец составная инструкция {} может содержать несколько инструкций, раз­
деленных точкой с запятой (; ) , как показано ниже:

[Переменная 1, Переменна я 2] [ТипЬ Параметр) -> Возвращаемый_Тип
{ Инструкция1} Инструкция2; Инструкция3; return Значение ; }

ПРИМЕЧАНИЕ

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

Таким образом, лямбда-функция — это компактная, полнофункциональная замена
функционального объекта, например такого, как следующий:
template
struct IsNowTooLong
{
// Переменные состояния

Лямбда-выражение для бинарной функции

|

561

Тип1 varl;
Тип2 var2;
// Конструктор
IsNowTooLong(const Тия1& ini, Тип2к in2):varl(ini),var2(in2){};
// Фактическое предназначение

Возвращаемый_тип operator ()
{

Инструкция! ;
Инструкция2 ;
return (Зна чение_или_выражение) ;
}

Лямбда-выражение
для бинарной функции
Бинарная функция получает два параметра и (необязательно) возвращает значение.
Эквивалентное лямбда-выражение выглядит следующим образом:

[...](Тип1$1 Параметр!, Тип2ь Параметр2) { /* Код */ }
В листинге 22.4 показана лямбда-функция, поэлементно перемножающая два век­
тора равных размеров. Она использует алгоритм s t d : : tr a n s fo r m () и сохраняет ре­
зультат в третьем векторе.
ЛИСТИНГ 22,4. Лямбда-выражение в качестве бинарной функции
0
1
2

#include
#include
#include

3
4

int main()

5

{

6

using namespace std;

7
8

9

vector vecMultiplicand{ 0, 1, 2, 3, 4 };
vector vecMultiplier{ 100, 101, 102, 103, 104 };

10
11

12

13
14
15
16
17
18
19

// Для хранения результата умножения
vector vecResult;
// Подготовка места для размещения результата
vecResult.resize(vecMultiplier.size());
transform(vecMultiplicand.begin(), // Диапазон множимых
vecMultiplicand.end(),
// Конец диапазона
vecMultiplier.begin(),
// Множители

562

|

ЗАНЯТИЕ 22. Лямбда-выражения языка С++11

20:
21:

vecResult.begin(),
// Результаты
[](int a, int b){return a*b;});

22 :
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38: }

cout « "Содержимое первого вектора:" « endl;
for(size_t index=0; index
void DisplayContents(const T& container)
{
for(auto element = container.cbegin();
element != container.cend();
++element)
cout « ^element « '

12 :
13:

cout «

"I Количество элементов: " «

container.size() «

endl;

Использование алгоритмов STL
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:

|

589

}
int main()
{
vector numsInVec(6);
// Заполнение элементами 8 8 8 5 5 5
fill(numsInVec.begin(), numsInVec.begin() + 3, 8);
fill_n(numsInVec.begin() + 3, 3, 5);
// Перемешивание содержимого контейнера
random_shuffie(numsInVec.begin(), numsInVec.end());
cout « "Исходное содержимое вектора:" «
DisplayContents(numsInVec);

endl;

cout « endl « "Замена 5 значением 8" « endl;
replace(numsInVec.begin(), numsInVec.end(), 5, 8);
cout « "Замена четных значений значением -1" « endl;
replace_if(numsInVec.begin(), numsInVec.endf),
[](int element) {return ((element % 2) == 0); }, —1);
cout « endl « "Вектор после замен:" «
DisplayContents(numsInVec);

endl;

return 0;
)

Результат
Исходное содержимое вектора:
5 8 5 8 8 5 | Количество элементов: 6
Замена 5 значением 8
Замена четных значений значением -1
Вектор после замен:
-1 -1 -1 -1 -1 -1 | Количество элементов: 6

Анализ
Код заполняет вектор numsInVec типа v e c to r < in t> несколькими значениями 5 и 8,
затем перемешивает их, используя алгоритм STL s t d : : random sh u f f l e (), как пока­
зано в строке 25. Строка 31 демонстрирует применение функции r e p la c e () для заме­
ны всех значений 5 значениями 8. Поэтому, когда в строке 34 функция r e p l a c e _ i f ()
заменяет все четные числа значением -1 , в результате получается, что все элементы
коллекции становятся равными -1 , как и показывает вывод.

590

|

ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

Сортировка, поиск в отсортированной
коллекции и удаление дубликатов
Сортировка и поиск в отсортированном диапазоне (для повышения производитель­
ности) встречаются в практических приложениях очень часто. Как правило, имеется
массив информации, которая должна быть отсортирована. Для сортировки контейнера
можно воспользоваться алгоритмом STL s o r t ():
sort(numsInVec.begin(), numsInVec.endO); // В порядке возрастания

Эта версия функции s o r t () применяет бинарный предикат s t d : : l e s s o , исполь­
зующий оператор o p e r a to r c , реализованный типом содержащихся в векторе элемен­
тов. Вы можете предоставить собственный предикат, чтобы изменить порядок сорти­
ровки, используя следующ ую перегруженную версию алгоритма:
sort(numsInVec.begin(), numsInVec.endO,
[](int lhs, int rsh){return (lhs > rhs);}); // В порядке убывания

Кроме того, зачастую требуется удалить дубликаты из коллекции. Для удаления
расположенных рядом повторяющихся значений используется алгоритм unique ():
auto newEnd = unique (numsInVec.begin (), numsInVec.endO);
numsInVec.erase(newEnd,numsInVec.end()); // Изменить размер

Для быстрого поиска STL предоставляет алгоритм b in a r y _ se a r c h (), который ра­
ботает только в отсортированном контейнере:
bool elementFound = binary_search(numsInVec.begin(),
numsInVec.endO, 2011);
if (elementFound)
cout « "Элемент найден!" «

endl;

В листинге 23.10 показаны упомянутые выше алгоритмы STL — s t d : : s o r t (),
который способен отсортировать диапазон, s td : :b in a r y _ se a r c h (), обеспечивающий
поиск в отсортированном диапазоне, и s t d : : u n iq u e (), удаляющий расположенные
рядом совпадающие элементы (они становятся смежными после сортировки).
ЛИСТИНГ 23.10. Использование функций s o r t ( ) , b in a r y _ se a r c h () и unique ()
0:
1:
2:
3:
4:

#include
#include
#include
#include
using namespace std;

5:
6: template ctypename T>
7: void DisplayContents(const T& container)

8:
9:
10:

{

for(auto element = container.cbegin();
element != container.cend();

Использование алгоритмов STL
11:
12:
13: }
14:
15: int
16: {
17:
18:
19:
20:

++element)
cout « ^element «

endl;

mint)
vector vecNames{"John", "jack", "sean", "Anna"};
// Вставка дубликата
vecNames.push_back("jack");

21 :
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46: }

cout « "Исходный вектор:" «
DisplayContents(vecNames);

endl;

cout « "Отсортированный вектор:" « endl;
sort(vecNames.begin(), vecNames.end());
DisplayContents(vecNames);
cout « "Поиск \"John\" с помощью 'binary_search':" « endl;
bool elementFound = binary_search(vecNames.begin(),
vecNames.end(), "John");
if (elementFound)
cout « "\"John\" найден в векторе!" «
else
cout « "Элемент не найден." « endl;

endl;

// Удаление смежных дубликатов
auto newEnd = unique(vecNames.begin(), vecNames.end());
vecNames.erase(newEnd, vecNames.end());
cout « "Вектор после применения 'unique’:" «
DisplayContents(vecNames);
return 0;

Результат
Исходный вектор:
John
jack
sean
Anna
jack
Отсортированный вектор:
Anna
John

endl;

|

591

592

|

ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

jack
jack
sean
Поиск "John” с помощью 'binary_search':
"John" найден в векторе!
Вектор после применения ’unique':
Anna
John
jack
sean

Анализ
Приведенный выше код сначала сортирует вектор vecNames (строка 26), а затем
(строка 30) использует алгоритм b in a r y _ s e a r c h () для поиска в нем элемента John
Doe. Точно так же в строке 39 используется алгоритм s t d : : u n iq u e () для удаления
смежных дубликатов. Обратите внимание, что функция un iq u e (), как и функция r e ­
move (), не изменяет размер контейнера. Это приводит к переносу значений, но не к
сокращению общего количества элементов. Чтобы избавиться от нежелательных или
неизвестных значений в конце контейнера, после функции u n iq u e () всегда следует
вызывать функцию v e c t o r : : e r a s e (), используя итератор, возвращенный функцией
u n ique (), как показано в строке 40.

ВНИМАНИЕ!

Такие алгоритмы, как

b in a r y _ s e a r c h ( ) , работают только в отсортиро­

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

ПРИМЕЧАНИЕ

s t a b l e _ s o r t () используется точно так же, как представленная
ранее функция s o r t ( ). Функция s t a b le _ s o r t () сохраняет относитель­

Функция

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

Разделение диапазона
Функция s t d : p a r t i t i o n () позволяет разделить исходный диапазон на две час­
ти: в одной элементы удовлетворяют унарному предикату, а в другой — не удовлет­
воряют:
bool IsEven(const int& num) // Унарный предикат
{
return ((num % 2) == 0);

}
partition(numsInVec.begin(), numsInVec.end(), IsEven);

Использование алгоритмов STL

|

593

Однако функция s t d : : p a r t i t i o n () не гарантирует сохранение относительного
порядка элементов в пределах каждого раздела. Когда это важно, следует использо­
вать функцию s t d : : s t a b l e j p a r t i t i o n ():
stablejpartition(numsInVec.begin(), numsInVec.end(), IsEven);

В листинге 23.11 показано применение этих алгоритмов.
ЛИСТИНГ 23.11. Использование алгоритмов p a r t i t i o n ( ) и s t a b l e j p a r t i t i o n ()
для разделения диапазона целых чисел на четные и нечетные значения________________
0:
1:
2:
3:
4:
5:

tinclude
#include
#include
using namespace std;
bool IsEven(const int& num) // Унарный предикат

6:

{

7:

8:

return ((num % 2) == 0);
}

9:
10: template ctypename T>
11: void DisplayContents(const T& container)

12 :

{

13:
14:
15:
16:
17:
18:
19: }

for(auto element = container.cbegin();
element != container.cend();
++element)
cout « ^element « '
cout «

"I Количество элементов: " «

container.size() «

20 :
21: int main()
22: {
23:
vector numsInVec{2017, 0, -1, 42, 10101, 25, 34, 19};
24:
25:
cout « "Исходный вектор: " « endl;
26:
DisplayContents(numsInVec);
27:
28:
vector vecCopy(numsInVec);
29:
30:
cout « "Результат partition():" « endl;
31:
partition(numsInVec.begin(), numsInVec.end(), IsEven);
32:
DisplayContents(numsInVec);
33:
34:
cout « "Результат stablejpartition():" « endl;
35:
stable_partition(vecCopy.begin(), vecCopy.end(), IsEven);
36:
DisplayContents(vecCopy);
37:
38:
return 0;
39: }

endl;

594

|

ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

Результат
Исходный вектор:
2017 0 -1 42 10101 25 34 19 | Количество элементов: 8
Результат partition():
34 0 42 -1 10101 25 2017 19 | Количество элементов: 8
Результат stable_partition():
0 42 34 2017 -1 10101 25 19 | Количество элементов: 8

Анализ
Код делит диапазон целых чисел, содержащийся в векторе n u m s I n V e c , на четные
и нечетные значения. Сначала разделение осуществляется с использованием функ­
ции s t d : : p a r t i t i o n ( ), как показано в строке 3 1 , а затем с использованием функции
s t a b l e j p a r t i t i o n () в строке 3 5 . Для сравнения результатов вектор n u m s I n V e c ко­
пируется в вектор v e c C o p y ; первый разделяется с использованием функции p a r t i ­
t i o n (), а последний — с использованием s t a b l e _ p a r t i t i o n (). Различие в результа­
тах использования функций s t a b l e _ p a r t i t i o n () и p a r t i t i o n () вполне очевидно в
приведенном выводе программы. Алгоритм s t a b l e j p a r t i t i o n () обеспечивает отно­
сительный порядок элементов в каждом разделе. Обратите внимание, что поддержка
этого порядка может сказываться на производительности, и это влияние может быть
как незначительным, так и существенным в зависимости от типа содержащихся в диа­
пазоне объектов.

ПРИМЕЧАНИЕ

Функция s t a b l e j p a r t i t i o n () работает медленнее, чем p a r t i t i o n (),
а потому ее следует использовать только тогда, когда важен относительный
порядок элементов в контейнере.

Вставка элементов в отсортированную коллекцию
Для отсортированной коллекции важно, чтобы элементы вставлялись в правиль­
ную позицию. Библиотека STL предоставляет такие функции, как low er_bound () и
upper bound (), позволяющие решить эту задачу:
auto minlnsertPos = lower_bound(listNames.begin(), listNames.end(),
"Brad Pitt" );
auto maxInsertPos = upperjoound(listNames.begin(), listNames,end(),
"Brad Pitt" );

Функции lo w erjo o u n d () и upper_bound () возвращают итераторы, указывающие
минимальную и максимальную позиции в отсортированном диапазоне, в который эле­
мент может быть вставлен без нарушения порядка сортировки.
В листинге 23.12 показано применение функции lo w erjoou n d () для вставки эле­
мента в минимальную позицию отсортированного списка имен.

Использование алгоритмов STL

|

Л И С Т И Н Г 2 3 . 1 2 . И с п о л ь з о в а н и е ф ун кц и й lo w e r _ b o u n d () и u p p e r J o o u n d ()
для в ст а в к и в о т с о р т и р о в а н н у ю к о л л е к ц и ю

0:
1:
2:
3:
4:
5:
6:
7:

#include
#include
#include
#include
using namespace std;

8:

{

9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:

template
void DisplayContents(const T& container)
for(auto element = container.cbegin();
element != container.cend();
++element)
cout « ^element « endl;
}
int main()
{
list names{ "John", "Brad", "jack", "sean", "Anna" };
cout « "Отсортированный список:" «
names.sort();
DisplayContents(names);

endl;

cout « "Нижний индекс, в который может быть вставлен \"Brad\": ";
auto minPos = 1ower_bound(names.begin(), names.end(), "Brad");
cout « distance(names.begin(), minPos) « endl;
cout « "Верхний индекс, в который может быть вставлен \"Brad\": ";
auto maxPos = upper_bound(names.begin(), names.end(), "Brad");
cout « distance(names.begin(), maxPos) « endl;
cout «

endl;

cout « "Список после вставки \"Brad\": " «
names.insert(minPos, "Brad");
DisplayContents(names);
return 0;
}

Результат
Отсортированный список:
Anna
Brad
John
jack

endl;

595

596

ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

sean
Нижний индекс, в который может быть вставлен "Brad": 1
Верхний индекс, в который может быть вставлен "Brad": 2
Список после вставки "Brad":
Anna
Brad
Brad
John
jack
sean

Анализ
Новый элемент может быть вставлен в отсортированную коллекцию в нескольких
потенциальных позициях, причем самую ближнюю к началу коллекции указывает ите­
ратор, возвращенный функцией lower_bound (), а наиболее близкую к концу коллек­
ции указывает итератор, возвращенный функцией upper_bound ( ) . В листинге 23.12 в
отсортированную коллекцию вставляется строка "Brad", уже имеющаяся в ней. Верх­
няя и нижняя границы различны (они совпадали бы, если бы в коллекции не было та­
кого элемента). Применение этих функций показано в строках 24 и 29 соответственно.
Как показывает вывод программы, при использовании для вставки строки в список
итератора, возвращенного функцией lower_bound () (строка 35), список сохраняет от­
сортированное состояние. Таким образом, данные алгоритмы позволяют осуществлять
вставку в коллекцию, не нарушая порядок отсортированного содержимого. Итератор,
возвращенный функцией upper bound (), сработал бы не менее корректно.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Используйте

Не забывайте сортировать содержимое контей­
нера, прежде чем вызывать метод unique ()

erase () класса контей­
нера после алгоритмов remove (), remove^
i f () и unique () для изменения размера
метод

чений. Функция

контейнера.

Проверяйте

для удаления повторяющихся смежных зна­

корректность итератора, возвра­

щаемого функциями

f i n d () ,

f i n d _ i f () ,

s e a r c h () и s e a r c h j i ( ) , сравнивая его с
возвращаемым значением метода e n d () кон­
тейнера, прежде чем использовать его для об­
ращения к элементу.

Предпочитайте функцию s ta b le jp a rtit io n () функции p a r t it io n ( ) , а функцию
stable_sort () функции sort () только
тогда, когда важно сохранение относительно­
го порядка отсортированных или разделенных
элементов, поскольку версии

stable^*

снизить производительность приложения.

могут

sort

() гарантирует, что все

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

unique ().

Не забывайте, что функцию binary^
search () следует вызывать только для отсо­
ртированного контейнера.

Резюме

|

597

Резюме
На сегодняшнем занятии рассматривался один из самых важных аспектов STL:
алгоритмы. Вы изучили различные типы алгоритмов, а примеры помогли вам лучше
понять их применение.

Вопросы и ответы
■ Могу ли я применить изменяющий алгоритм, такой как s t d : : transform (),
к ассоциативному контейнеру, такому как s t d : : set?
Даже если бы это было возможно, поступать так не нужно. Элементы в ассоциа­
тивном контейнере s e t следует рассматривать как константные. Дело в том, что ас­
социативные контейнеры сортируют свои элементы при вставке, и относительные
позиции элементов играют важную роль в таких функциях, как f in d (), а также
для эффективности работы контейнера. Поэтому изменяющие алгоритмы, такие
как s t d : : tr a n s fo r m (), не должны использоваться с множествами STL.

■ Я должен присвоить определенное значение каждому элементу после­
довательного контейнера. Могу ли я использовать для этого алгоритм
s t d : : transform () ?
Хотя алгоритм s t d : : tr a n s fo r m !) для этого вполне применим, лучше использовать
алгоритм f i l l () или f i l l _ n ().

■ Изменяет ли алгоритм copy_backward() расположение элементов контейнера
на обратное?
Нет, он этого не делает. Алгоритм STL cop y backw ard () изменяет на обратный
порядок элементов при копировании, но не порядок хранимых элементов, т.е. ко­
пирование начинается с конца диапазона и продолжается к началу. Чтобы обратить
содержимое коллекции, используйте алгоритм s t d : : r e v e r s e ().

■ Могу ли я использовать алгоритм s t d : : s o r t () для списка?
Алгоритм s t d : : s o r t () применяется к списку так же, как и к любому другому по­
следовательному контейнеру. Однако у списка есть особое свойство: существую­
щие итераторы остаются корректными при операциях со списком, в то время как
функция s t d : : s o r t () не может этого гарантировать. Поэтому список STL предо­
ставляет собственный алгоритм s o r t () в форме функции-члена l i s t :: s o r t О , ко­
торый и следует использовать, поскольку он гарантирует, что итераторы на элемен­
ты списка останутся допустимыми, даже если их относительные позиции в списке
изменятся.

■ Почему так важно использовать такие функции, как lowerJbound () или
upper_bound(), при вставке в отсортированный диапазон?
Эти функции предоставляют соответственно первую и последнюю позиции в от­
сортированной коллекции, в которую может быть вставлен элемент без нарушения
порядка сортировки.

598

|

ЗАНЯТИЕ 23. Алгоритмы библиотеки STL

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления полу­
ченных знаний, а также упражнения, которые помогут применить на практике по­
лученные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выпол­
нить задания, а потом сверьте полученные результаты с ответами в приложении Д,
“Ответы”. Если остались неясными хотя бы некоторые из предложенных ниже во­
просов, не приступайте к изучению материала следую щ его занятия.

Контрольные вопросы
1. Необходимо удалить из списка элементы, удовлетворяющие некоторому за­
данному условию. Какую функцию вы используете: s t d : :r e m o v e _ if () или
l i s t :: remove__if () ?
2. У вас есть список элементов типа C o n ta c tlte m . Как функция l i s t :: s o r t ()
отсортирует элементы списка этого типа в отсутствии явно определенного би­
нарного предиката?
3. Как часто алгоритм STL g e n e r a te () вызывает функцию g e n e r a to r () ?
4. Чем функция s t d : : t r a n s fo r m () отличается от функции s t d : : fo r each () ?

Упражнения
1. Напишите бинарный предикат, получающий в качестве аргументов строки и
возвращающий значение, представляющее результат сравнения строк, не зави­
сящего от регистра символов.
2. Продемонстрируйте, как алгоритмы STL, такие как s t d : : сору (), используют
итераторы для выполнения своих задач, не нуждаясь в знании природы целевой
коллекции при копировании двух последовательностей, содержащихся в двух
разных по типу контейнерах.
3. Вы пишете приложение, которое записывает характеристики звезд, видимых
на горизонте в порядке их восхождения. В астрономии важна информация
о размере звезды и о ее относительной высоте и порядке. Сортируя эту кол­
лекцию звезд по размерам, вы использовали бы функцию s t d : : s o r t () или
s td ::s ta b le so r t()?

ЗАНЯТИЕ 24

Адаптивные
контейнеры:
стек и очередь
Стандартная библиотека шаблонов (STL) предоставляет
шаблонные классы, способные адаптировать другие контей­
неры для моделирования поведения очереди и стека. Кон­
тейнеры, которые внутренне используют другой контейнер
и обеспечивают иное поведение, называются адаптивными
контейнерами (adaptive container) или просто адаптерами.
На этом занятии...

■ Поведенческие характеристики стеков и очередей
■ Использование адаптера STL s ta c k
■ Использование адаптера STL queue
■ Использование адаптера STL p r io r ity _ q u e u e

600

|

ЗАНЯТИЕ 24. Адаптивные контейнеры: стек и очередь

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

Стеки
Стек (stack) — это структура в памяти, действующая по принципу последним во­
шел, первым вышел (Last-In-First-Out — LIFO), элементы которой могут быть встав­
лены или извлечены из вершины контейнера. Стек можно представить как стопку
тарелок. Последняя положенная в стопку тарелка будет взята первой. К тарелкам в
середине и в основании доступа нет. Этот способ организации элементов, подразуме­
вающий “добавление и извлечение из вершины”, представлен на рис. 24.1.

РИС. 24.1. Работа со стеком
Такое поведение стопки тарелок моделирует обобщенный контейнер s t d : : s t a c k STL.

СОВЕТ

Чтобы использовать класс s t d : : s t a c k , в исходный текст программы не­
обходимо включить соответствующий заголовочный файл:

#include

Очереди
Очередь (queue) — это структура в памяти, действующая по принципу первым во­
шел, первым вышел (First-In-First-Out — FIFO), элементы которой могут вставляться
после уже находящихся в ней и извлекаться в том же порядке, в котором были встав­
лены. Очередь можно представить как очередь ожидающих людей, в которой, кто пер­
вым встал в очередь, тот раньше всех ее покинет (в такой очереди никто не пробира­
ется без очереди). Этот способ организации элементов, подразумевающий вставку в
конец и извлечение из начала, представлен на рис. 24.2.

Использование класса STL stack

Вставка
(в конец)

Элемент
N

Элемент

Элемент
1

Элемент
0

601

Извлечение
(из начала)

РИС. 24.2. Работа с очередью
Такое поведение очереди моделирует обобщенный контейнер STL s t d : : queue.

СОВЕТ

Чтобы использовать класс

s t d : : queue, в исходный текст программы не­

обходимо включить соответствующий заголовочный файл:

#include

Использование класса STL

s ta c k

Стек STL представляет собой шаблон класса s t a c k , для использования которо­
го необходимо включить в код заголовочный файл < sta ck > . Это обобщенный класс,
обеспечивающий вставку элементов в вершину контейнера и извлечение их оттуда и
не разрешающий доступ к элементам в средине стека или их просмотр. В некотором
смысле поведение класса s t d : : s t a c k очень похоже на стопку тарелок.

Создание экземпляра стека
Некоторые реализации STL определяют шаблон класса s ta c k так:
template <
class Тип_Элемента,
class Контейнер = deque
> class stack;

Параметр Тип_Элемента указывает тип элементов, которые будут храниться в сте­
ке. Второй параметр шаблона, Контейнер, — это класс контейнера, на основе которого
реализован стек. По умолчанию для внутреннего хранения данных стека используется
класс s t d : : deque, но он может быть заменен классом s t d : : v e c t o r или s t d : : l i s t .
Таким образом, создание экземпляра стека целых чисел имеет следующий вид:
std::stack numsInStack;

Если необходимо создать стек объектов какого-нибудь иного типа, например клас­
са Tuna, то можно использовать следующий синтаксис:
std::stack tunasInStack;

Для создания стека на базе другого контейнера используйте следующий синтаксис:
std::stack . При этом в очереди наивысший
приоритет имеет наименьшее число, которое и будет доступно с помощью метода

t o p ().
ЛИСТИНГ 24,7.
0:
1:
2:
3:
4:
5:

С о з д а н и е э к з е м п л я р а о ч е р е д и с п р и о р и те та м и ___________________________

#include
#include
#include
int main()
{
using namespace std;

6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

// Очередь priority_queue с предикатом greater
priority_queue , показанный в строке 8. В результа­
те целое число с наименьшим значением считается имеющим наивысший приоритет,
а потому помещается в первую позицию очереди. Поэтому функция to p (), использо­
ванная в строке 19, всегда выводит наименьшее целое число перед его извлечением в
строке 20 из очереди с помощью метода pop ().
Таким образом, когда элементы извлекаются из рассмотренной очереди с приори­
тетами, целые числа извлекаются из нее в порядке увеличения их значений.

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

Вопросы и ответы
■ Может ли быть изменен элемент в середине стека?
Нет, это противоречило бы самому предназначению стека.

■ Могу ли я итерировать все элементы очереди?
Очередь не предоставляет итераторы на свои элементы; обратиться можно только
к конечным элементам очереди.

■ Могут ли алгоритмы STL работать с адаптивными контейнерами?
Алгоритмы STL используют итераторы. Поскольку ни класс sta c k , ни класс queue
не предоставляют итераторы, использование алгоритмов STL с этими контейнера­
ми невозможно.

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

ЗАНЯТИЕ 24. А д а п т и в н ы е

614

контейнеры : стек и оч ер едь

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

Контрольные вопросы
1. Можно ли изменить поведение контейнера p r i o r i t y j q u e u e так, чтобы послед­
ним извлекался элемент с самым большим значением?
2. Имеется очередь с приоритетами, элементами которой являются объекты клас­
са C o i n (монета). Какой оператор-член этого класса необходимо определить,
чтобы в первой позиции очереди с приоритетами оказывались монеты с наи­
большим номиналом?
3. Имеется стек элементов класса C o in , содержащий шесть объектов. Можно ли
обратиться к элементу, вставленному первым, или извлечь его из стека?

Упражнения
1. Очередь людей (класс P e r s o n ) выстроилась к почтовому отделению. Класс
P e r s o n имеет атрибуты, хранящие возраст и пол, и определен следующим об­
разом:
class Person
{
public:
int age;
bool isFemale;

};
Напишите бинарный предикат для использования в p r i o r i t y q u e u e , который
позволит сотруднику обслужить сначала стариков и женщин (в указанном по­
рядке).
2. Напишите программу, которая, используя класс s t a c k , меняет порядок симво­
лов во введенной пользователем строке на обратный.

ЗАНЯТИЕ 25

Работа
с битовыми
флагами
при использовании
библиотеки STL
Биты могут быть очень эффективным средством хранения
параметров и флагов. Стандартная библиотека шаблонов
(STL) предоставляет классы, способные организовать инфор­
мацию на уровне отдельных битов и работать с ней.
На этом занятии...

■ Класс b i t s e t
■ Класс v e c to r < b o o l>

616

|

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

Класс b i t s e t
Класс STL s t d : :b i t s e t (множество битов) предназначен для обработки инфор­
мации в виде битов и битовых флагов. Класс s t d : :b i t s e t не является контейнерным
классом библиотеки STL, поскольку не способен изменять свои размеры. Это вспомо­
гательный класс, оптимизированный для работы с последовательностью битов, длина
которой известна на момент компиляции.

СОВЕТ

Чтобы использовать класс

s t d : :b i t s e t , в исходный текст программы

необходимо включить соответствующий заголовочный файл:

#include

И нстанцирование класса s t d : :b it s e t
Данному шаблону класса требуется только один параметр, содержащий количество
битов, которое должно храниться в объекте этого класса:
bitset fourBits; / / 4 бита, инициализированных 0000

Множество битов можно инициализировать последовательностью битов, пред­
ставленной в виде строки char*:
bitset fiveBits("10101"); // 5 битов 10101

При инстанцировании можно копировать одно множество битов в другое:
bitset eightBitsCopy(eightbits);

В листинге 25.1 представлены некоторые из способов создания экземпляра класса
b its e t.
ЛИСТИНГ 25.1. С о з д а н и е э к з е м п л я р а к л а с с а s t d : :b i t s e t ___________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:

#include
#include
#include
int main()
{
using namespace std;
bitset fourBits; // 4 бита, инициализированные 0000
cout « "fourBits: " « fourBits « endl;

10 :
11:
12:
13:
14:
15:

bitset fiveBits("10101"); // 5 битов - 10101
cout « "fiveBits: " « fiveBits « endl;
bitset sixBits(OblOOOOl); // Бинарный литерал C++14
cout « "sixBits: " « sixBits « endl;

Использование класса std::bitset и его членов
16:
17:
18:
19:
20:
21:

|

6 17

bitset eightBits(255); // Инициализация значением long int
cout « "eightBits: " « eightBits « endl;
// Инстанцирование как копия другого экземпляра
bitset eightBitsCopy(eightBits);

21 :
23:
24: }

return 0;

Результат
fourBits: 0000
fiveBits: 10101
sixBits: 100001
eightBits: 11111111

Анализ
В этом листинге продемонстрированы четыре способа создания объекта класса
b i t s e t . Конструктор по умолчанию инициализирует битовую последовательность
нулями, как показано в строке 9. Строка в стиле С, содержащая строковое представле­
ние битовой последовательности, используется для инициализации в строке 11. Тип
u n sig n ed lo n g , который содержит десятичное значение двоичной последовательно­
сти, использован в строке 14, а копирующий конструктор использован в строке 21.
Обратите внимание на то, что в каждом из этих случаев вы обязаны указать в качестве
параметра шаблона количество битов, которые будет содержать это множество. Дан­
ное значение фиксируется во время компиляции и динамически во время выполнения
не изменяется. В отличие от вектора, во множество нельзя вставить битов больше,
чем было определено во время компиляции.

СОВЕТ

Обратите внимание на бинарный литерал

OblOOOOl в строке 14. Префикс

0Ь или 0В указывает компилятору, что последующие цифры представля­
ют собой бинарное представление целого числа. Эта новинка появилась в
C++, начиная со стандарта C++14.

Использование класса
s t d : :b i t s e t и его членов
Класс b i t s e t предоставляет функции-члены, позволяющие осуществить установ­
ку и сброс битов, их чтение и запись в поток. Он предоставляет также операторы,
позволяющие выводить содержимое множества битов и выполнять побитовые логи­
ческие операции.

618

|

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

П олезны е операторы , п редоставляем ы е
классом s t d : : b i t s e t
Операторы рассматривались на занятии 12, “Типы операторов и их перегрузка”,
на котором вы узнали, что важнейшая задача операторов заключается в обеспечении
удобства и простоты использования класса. Класс s t d : r b i t s e t предоставляет не­
сколько весьма полезных операторов, представленных в табл. 25.1 и существенно об­
легчающих его использование. Примеры, объясняющие использование операторов,
подразумевают множество битов f o u r B i t s из листинга 25.1.

ТАБЛИЦА 25.1. Операторы, поддерживаемые классом s t d
Оператор
o p e ra to r«

: :b i t s e t

Описание
П ере д а е т те к сто в о е п р е д ста в л е н и е би то в о й п осл е д о ва те л ьн ости в поток
вы вода
cout «

f o u r B it s ;

o p e ra to r»

П ере д а е т ст р о к у в о б ъ е к т класса b i t s e t

o p e ra to rs

В ы п ол няе т п оби тов ую о п е р а ц и ю И

"0101" »

f o u r B it s ;

b it s e t < 4 >
o p e ra to r|

r e s u lt ( f o u r B it s l

b it w is e < 4 >
o p e ra to rA

r e s u lt ( f o u r B it s l

| f o u r B it s 2 ) ;

В ы п ол н яе т п оби товую о п е р а ц и ю И СКЛ Ю Ч АЮ Щ ЕГО ИЛИ
b it w is e < 4 >

o p e ra to r-

& f o u r B it s 2 ) ;

В ы п ол н яе т п оби тов ую о п е р а ц и ю ИЛИ

r e s u lt ( f o u r B it s l

А f o u r B it s 2 ) ;

В ы п ол н яе т п оби товую о п е р а ц и ю НЕ
b it w is e < 4 >

r e s u lt ( - f o u r B it s l) ;

o p e ra to r» =

В ы п ол н яе т о п е р а то р б и то в о го сд в и га в п р а во

o p e ra to r« =

В ы п ол няе т о п е р а то р б и то в о го сд вига влево

o p e r a t o r [N]

В о зв ра щ а е т ссы л ку на б и т н о м е р N в п о сл е д о ва те л ьн ости

fo u r B it s
fo u r B it s

»=
«=

f o u r B i t s [2]

(2 );
(2 );

//

С д ви г на два би та

//

= 0; / /

С д ви г на два бита
Установить

b o o l bNum = f o u r B i t s [ 2 ] ;

//

вправо
влево

третий бит в 0

Читать

третий бит

В дополнение к ним класс s t d : : b i t s e t предоставляет такие присваивающие опе­
раторы, как | =, &=, А= и -=.

Методы класса

s td : : b it s e t

Биты могут находиться в двух состояниях: они либо установлены (1), либо сбро­
шены (0). Для работы с содержимым множества битов можно воспользоваться
функциями-членами класса b i t s e t (табл. 25.2), позволяющими работать с отдельны­
ми (или со всеми) битами множества.

Использование класса std::bitset и его членов
ТАБЛИЦА 25.2. Методы класса s t d
Описание

s e t ()

У станавливает все биты п осл е д о ва те л ьн о сти р а в н ы м и 1
f o u r B it s . s e t ();
v a l= l)

619

: :b i t s e t

Функция

s e t (N,

|

//

Т еперь м н о ж ество содерж ит

1111

П р и св аи в ае т б и ту н о м е р О з н а ч е н и е v a l (по у м о л ч а н и ю — 1)
f o u r B it s . s e t (2 ,0 );

//

Установить

т р е т и й би т равны м 0

r e s e t ()

С б ра сы в а е т все биты п о сл е д о ва те л ьн о сти в 0

r e s e t {N)

С б ра сы в а е т б и т н о м е р N

f l i p ()

И н в е рти р уе т все би ты п о сл е д о ва те л ьн о сти

s i z e ()

Возв ра щ а е т к о л и ч е ство би тов п о сл е д о ва те л ьн о сти

c o u n t ()

В о зв ра щ а е т к о л и ч е ство уста н о в л е н н ы х би тов

f o u r B it s . r e s e t ();

//

f o u r B it s . r e s e t (2 );
f o u r B i t s . f lip ();

//

Теперь м но ж ество содерж ит

//

Теперь

третий бит равен

0101 и зм е н и л о сь

s i z e _ t N u m B it s = f o u r B i t s . s i z e ( ) ;

на

//

0000
0

1010

Возвр ащ ает 4

s i z e _ t N u m B it s S e t = f o u r B i t s . c o u n t ( ) ;
s iz e

t N u m B it s R e s e t = f o u r B i t s . s i z e O

-

f o u r B it s . c o u n t ();

П р и м ен е н и е п р и вед ен н ы х в ы ш е операторов и м етодов п ро д ем о н стр и р ован о в л и с­
т и н г е 25 .2 .

ЛИСТИНГ 25.2. Л о ги ч е с к и е о п е р а ц и и с м н о ж е с т в о м б и то в
0:
1:
2:
3:
4:
5:
6:
7:
8:

#include
#include
tinclude
int main()
{
using namespace std;
bitset inputBits;
cout « "Введите последовательность

из 8 битов: ";

9:
10:

cin »

inputBits; // Пользовательский ввод множества

11:
12:
13:
14:
15:
16:
17:
18:
19:

cout «
cout «
cout «

"Вы ввели: единиц " « inputBits.count() « endl;
"
: нулей ";
inputBits.size() - inputBits.count() « endl;

bitset inputFlipped(inputBits);
inputFlipped.flip();
cout «

// Копирование
// Инверсия битов

"Инвертированное множество: " «

inputFlipped «

endl;

20 :
21:
22:
23:

cout «
cout «
cout «

"Выполнение логических операций:" « endl;
inputBits « " & " « inputFlipped « " = ";
(inputBits & inputFlipped) « endl; // Побитовое

И

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

620
24:
25:
26:
27:
28:
29:
30:
31:
32: }

cout
cout

« inputBits « м | " « inputFlipped « " =
« (inputBits | inputFlipped) « endl; // Побитовое

ИЛИ

cout
cout

« inputBits « " л " « inputFlipped « " =
« (inputBits л inputFlipped) « endl; // Побитовое

XOR

return 0;

Результат
Введите последовательность из 8 битов: 1 0 1 1 0 1 1 0
Вы ввели: единиц 5
: нулей 3
Инвертированное множество: 01001001
Выполнение логических операций:

10110110
10110110
10110110

&
01001001=00000000
| 01001001 = 11111111
Л
01001001=11111111

Анализ
Эта интерактивная программа демонстрирует не только простые бинарные операции
между двумя последовательностями битов с использованием класса s t d : : b i t s e t , но и
удобство его потоковых операторов. Операторы сдвига ( » и « ) , реализованные клас­
сом s t d : : b i t s e t , позволяют выводить последовательности битов на экран и читать
небольшие последовательности, вводимыепользователем. Множество битов i n p u t B i t s
получает введенную пользователем последовательность (строка 10). Используемый в
строке 12 метод c o u n t () сообщает количество единиц в последовательности, а коли­
чество нулей вычисляется как разность между возвращаемыми значениями метода
s i z e (), возвращающего количество битов в множестве, и метода c o u n t (), как пока­
зано в строке 14. Множество битов i n p u t F l i p p e d , которое изначально представляет
собой копии множества i n p u t B i t s , далее инвертируется с использованием метода
f l i p () в строке 17. Теперь оно содержит последовательность инвертированных битов,
в котором биты, имевшие нулевые значения, стали единичными, а единичные — ну­
левыми. Остальная часть программы демонстрирует результат выполнения побитовых
операций И, ИЛИ и ИСКЛЮЧАЮЩЕЕ ИЛИ между этими двумя множествами битов.

ПРИМЕЧАНИЕ

Одним из недостатков шаблона класса b i t s e t o

библиотеки STL явля­

ется его неспособность динамически изменять свои размеры. Вы можете
использовать класс b i t s e t только там, где количество хранимых битов
известно во время компиляции.
Библиотека STL снабжает программиста классом v e c t o r < b o o l> (в неко­
торых реализациях STL именуемым также b i t _ v e c t o r ) , который преодо­
левает этот недостаток.

Класс vector

| 621

Класс v e c t o r < b o o l>
Класс v e c t o r < b o o l > является частичной специализацией класса s t d : : v e c t o r ,
предназначенной для хранения логических данных. Этот класс в состоянии динами­
чески измерить свой размер, поэтому программист может не знать заранее количества
логических флагов во время компиляции.
Чтобы использовать класс s t d : : v e c t o r < b o o l> , в исходный текст про­

СОВЕТ

граммы необходимо включить соответствующий заголовочный файл:

#include

Создание экземпляра класса

v e c t o r < b o o l>

Экземпляр класса v e c t o r < b o o l> создается подобно вектору:
vector boolFlagsl;

Например, можно создать вектор с 10 логическими флагами, инициализированны­
ми значением 1 (т.е. t r u e ) :
vector boolFlags2(10, true);

Вы можете также создать объект как копию другого объекта:
vector boolFlags2Copy(boolFlags2);

Некоторые из способов создания экземпляра класса v e c t o r < b o o l> представлены
в листинге 25.3.

ЛИСТИНГ 25.3, С о з д а н и е

э к з е м п л я р а к л а с с а v e c t o r < b o o l> _____________________________

0: #include

1:
2: int main()
3: {
4:
5:
6:
7:

using namespace std;
// Создание экземпляра объекта конструктором по умолчанию
vector boolFlagsl;

8:
9:
10:

// Инициализация вектора 10 элементами true
vector boolFlags2(10,true);

11 :
12:
13:
14:
15:
16: }

// Создание объекта как копии другого объекта
vector vecBool2Copy(vecBool2);
return 0;

622

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

|

Анализ
Здесь продемонстрированы некоторые из сп особов создания объекта класса
v e c t o r c b o o l x В строке 7 используется конструктор по умолчанию. В строке 10 по­
казано создание объекта, который изначально содержит 10 логических флагов, ини­
циализированных значением tr u e . Строка 13 демонстрирует, как один объект класса
v e c to r < b o o l> может быть создан как копия другого.

Функции и операторы класса v e c to r< b o o l>
Класс v e c t o r < b o o l > предоставляет функцию f l i p ( ) , которая инвертиру­
ет состояние логических значений в последовательности п одобно функции
b i t s e t o : : f l i p ().
В остальном этот класс очень похож на класс s t d : : v e c to r в том смысле, что мож­
но применить функцию p u sh _b ack () к флагам последовательности. Пример в лис­
тинге 25.4 демонстрирует применение этого класса в подробностях.

ЛИСТИНГ 25.4, Использование класса v e c to r < b o o l> ________________________________
0: #include
1: #include
2: using namespace std;
3:
4 : int main ()
5: {
6:
7:

vector boolFlags(3); // 3 логических флага
boolFlags[0] = true;

8:

boolFlags[1] = true;

9:

boolFlags[2] = false;

10:
11:

boolFlags.push_back(true); // Добавить четвертый флаг

12 :
13:

cout «

14:
15:
16:

for(size_t index = 0; index < boolFlags.size(); ++index)
cout « boolFlags[index] «
' ’;

"Содержимое вектора:

17:

cout «

18:

boolFlags.flip();

" «

endl;

endl;

19:
20:

cout «

21:

for(size_t index = 0; index < boolFlags.size(); ++index)

22:

"Содержимое вектора:

cout «

23:
24:

cout «

endl;

25:
26:
27:

return 0;
}

" «

boolFlags[index] «

endl;
' ';

Резюме

623

Результат
Содержимое вектора:

110 1
Содержимое вектора:

0 0 10

Анализ
Здесь для обращения к логическим флагам в векторе используется оператор o p er­
a to r [ ] (строки 7 -9 ), как и в обычном векторе. Функция f l i p () используется в стро­
ке 18 для инверсии индивидуальных битовых флагов, по существу преобразовывая все
0 в 1 и обратно. Обратите внимание на применение функции push_back () в строке 11.
Хотя изначально вектор b o o lF la g s был создан для хранения трех флагов (строка 6), в
строке 11 к нему добавляется еще один. Добавление большего количества флагов, чем
было определено вначале, а также возможность выбрать их количество динамически
уже после компиляции отличают v e c to r < b o o l> от класса s t d : r b i t s e t .

СОВЕТ

Начиная с C++11 вы можете инстанцировать
с помощью инициализации списком:

b o o lF la g s из листинга 25.4

vector boolFlags {true, true, false };

Резюме
На сегодняшнем занятии рассмотрен весьма эффективный инструмент для работы
с битовыми последовательностями и флагами: класс s t d : : b i t s e t . Вы также узнали
о классе v e c to r < b o o l> , который позволяет хранить логические флаги, количество ко­
торых необязательно знать во время компиляции.

Вопросы и ответы
■ В ситуации, в которой применимы оба класса, std : : b it s e t и vector,
какой из них вы предпочли бы для хранения бинарных флагов?
Класс s t d : : b i t s e t , поскольку он лучше всего подходит для этого требования.

■ У меня есть объект myBitSeq класса std : :b its e t, в котором содержится опре­
деленное количество битов. Как мне определить количество битов со значени­
ем 0 (или fa lse )?
Метод b i t s e t :: cou n t () возвращает количество битов со значением 1. Вычтя это
значение из значения, возвращенного методом b i t s e t :: s i z e () (общее количество
хранимых битов), мы получим количество 0 в последовательности.

■ Могу ли я использовать итераторы для доступа к индивидуальным элементам
в объекте класса vector?
Да. Поскольку класс v e c t o r < b o o l > — это частичная специализация класса
s t d : : v e c to r , а этот класс поддерживает итераторы.

624

|

ЗАНЯТИЕ 25. Работа с битовыми флагами при использовании библиотеки STL

■ Могу ли я задать количество элементов, которые будут храниться в объекте
класса vector, во время компиляции?
Да, либо указав их количество в перегруженном конструкторе, либо использовав
функцию v e c to r < b o o l> : : r e s i z e () позднее.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Может ли множество битов расширять свой внутренний буфер для хранения
переменного количества элементов?
2. Почему класс b i t s e t не считается контейнерным классом STL?
3. Стоит ли использовать класс s t d : : v e c t o r для хранения фиксированного коли­
чества битов, известного на момент компиляции?

Упражнения
1. Создайте объект класса b i t s e t , содержащий четыре бита. Инициализируй­
те его числом, отобразите результат и сложите его с другим множеством би­
тов. (Предостережение: множества битов не допускают синтаксис b it s e t A =
b i t s e t X -i- b it s e t Y .)
2. Покажите, как бы вы инвертировали биты в множестве битов.

Часть V

Сложные
концепции C++
В ЭТОЙ ЧАСТИ...
ЗАН ЯТИЕ 26. Понятие интеллектуальных указателей
ЗАН ЯТИЕ 27. Применение потоков для ввода и вывода
ЗАНЯТИЕ 28. Обработка исключений
ЗАН ЯТИЕ 29. Что дальше

ЗАНЯТИЕ 26

Понятие
интел л ектуа л ьн ых
указателей
Программисты C + + не обязаны применять простые типы
указателей при работе с динамической памятью. Вместо это­
го они могут использовать интеллектуальные указатели.
На этом занятии...
л

Что такое интеллектуальный указатель и зачем он нужен

■ Как реализуются интеллектуальные указатели
■ Типы интеллектуальных указателей
■ Почему не следует использовать устаревший тип
std ::a u to j? tr
■ Интеллектуальный указатель STL s t d : :u n iq u e _ p tr
■ Популярные библиотеки интеллектуальных указателей

628

|

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

Что такое интеллектуальный указатель
Попросту говоря, интеллектуальный указатель (smart pointer) C++ — это класс с
перегруженными операторами, который ведет себя, как обычный указатель. В то же
время он предоставляет дополнительные возможности в обеспечении надлежащего и
своевременного освобождения динамически создаваемых данных и облегчает управ­
ление жизненным циклом объекта.

Проблемы обычных указателей
В отличие от других современных языков программирования, C++ предоставляет раз­
работчику полную свободу в выделении, освобождении и управлении памятью. К сожа­
лению, эта свобода — палка о двух концах. С одной стороны, она придает языку C++ его
мощь, но с другой — обеспечивает возможность таких связанных с памятью проблем, как
утечка памяти, когда динамически создаваемые объекты оказываются не освобожденными.
Например:
CData *pData = mObject .GetData () ;

/*

Вопрос : был ли объект, на который указывает указатель pData,
динамически выделен с использованием оператора new?
Кто его освобождает: вызывающая сторона или вызываемая?
Ответ: Неизвестно!

*/
pData->Display();

В приведенном выше примере кода нет никакого очевидного способа выяснить
информацию об объекте, на который указывает указатель pData.
■ Создан ли он в динамически выделенной для него памяти (а поэтому в конечном
счете должен быть освобожден)?
■ Несет ли ответственность за его освобождение вызывающая сторона?
■ Будет ли он автоматически освобожден деструктором объекта?
Хотя частично такие двусмысленности могут быть решены за счет вставки коммен­
тариев и соблюдения общепринятых правил написания кода, эти механизмы слишком
свободны, чтобы эффективно предотвратить все ошибки, причиной которых является
неправильное обращение с динамически выделенными данными и указателями.

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

Как реализованы интеллектуальные указатели

629

smartj?ointer spData = mObject.GetData();
// Используем интеллектуальный указатель, как обычный!
spData->Display();
(*spData).Display();
// Можно не заботиться об освобождении памяти (деструктор
// интеллектуального указателя сделает это самостоятельно)

Таким образом, интеллектуальные указатели ведут себя, как обычные указатели
(будем называть их простыми указателями (raw pointer)), но предоставляют полезные
возможности с помощью перегруженных операторов и деструкторов , гарантируя
своевременное освобождение динамически выделенных данных.

Как реализованы
интеллектуальные указатели
Пока что этот вопрос может быть упрощен до вопроса “Почему интеллектуаль­
ный указатель spData способен функционировать, как обычный указатель?” Ответ та­
ков: чтобы позволить программисту использовать их, как обычные указатели, классы
интеллектуальных указателей перегружают оператор разыменования (*) и оператор
обращения к члену (-> ). Перегрузка операторов обсуждалась ранее, на занятии 12,
“Типы операторов и их перегрузка”.
Кроме того, чтобы позволить вам управлять объектами в динамической памяти,
тип которых вы выбираете сами, почти все хорошие классы интеллектуальных ука­
зателей являются шаблонными и содержат обобщ енную реализацию своих функцио­
нальных возможностей. Будучи шаблонами, они обладают универсальностью и могут
быть специализированы для управления объектами с выбранным вами типом.
В листинге 26.1 содержится типичная реализация простого класса интеллектуаль­
ного указателя.
ЛИСТИНГ 2в.1. Минимально необходимые компоненты
класса интеллектуального указателя___________________
0:
1:
2:
3:
4:
5:
6:
7:

template ctypename Т>
class smart_pointer
{

private:
T* rawPtr;
public:
smart_pointer(T*pData):rawPtr(pData){} // Конструктор
~smart_pointer(){ delete pData; };
// Деструктор

8:
9:
10:
11:
12:

// Копирующий конструктор
smart_pointer(const smart_pointer S anotherSP);
// Оператор копирующего присваивания
smart_pointers operator=(const smart_pointers anotherSP);

630
13:
14:
15:
16:
17:
18:
19:
20:
21:

|

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

Т& operator*() const
{
return *(rawPtr);
}

// Оператор разыменования

Т* operator->() const // Оператор обращения к члену
{

return rawPtr;

22 :

}

23: };

Анализ
Показанный выше класс интеллектуального указателя демонстрирует реализацию
операторов * и -> , объявленных в строках 14-17 и 19-22. Они позволяют этому клас­
су функционировать как “указатель” в обычном смысле. Например, чтобы использо­
вать интеллектуальный указатель на объект класса Tuna, вы создаете его экземпляр
следующим образом:
smart_pointer smartTuna(new Tuna);
smartTuna->Swim();
// Альтернативный вариант:
(*pSmartDog).Swim();

Данный класс s m a r t_ p o in te r пока еще не имеет и не реализует функциональ­
ных возможностей, которые сделали бы его достаточно интеллектуальным классом
и обеспечили бы его преимущество перед обычным указателем. Конструктор (стро­
ка 6) получает указатель, который сохраняется в классе интеллектуального указа­
теля как внутренний объект. Деструктор освобож дает этот указатель, обеспечивая
автоматическое освобождение памяти.

ПРИМЕЧАНИЕ

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

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

Типы интеллектуальных указателей

|

631

реализации зачастую приводят к проблемам производительности, тогда как самые
быстрые могут не удовлетворять требованиям всех приложений. В конце концов, раз­
работчик должен понимать, как функционирует тот или иной интеллектуальный ука­
затель, прежде чем решит использовать его в своем приложении.
Классификация интеллектуальных указателей фактически основана на их страте­
гии управления ресурсами памяти.
■ Глубокое копирование
■ Копирование при записи
■ Подсчет ссылок
■ Список ссылок
■ Деструктивное копирование
Давайте бегло рассмотрим каждую из этих стратегий, прежде чем переходить к
изучению интеллектуального указателя s t d : :u n iq u e _ p tr , предоставляемого стан­
дартной библиотекой C++.

Глубокое копирование
Каждый экземпляр интеллектуального указателя, реализующего глубокое копи­
рование, содержит полную копию объекта, которым он управляет. Всякий раз, когда
копируется интеллектуальный указатель, копируется и объект, на который он указы­
вает (т.е. осуществляется глубокое копирование). Интеллектуальный указатель, вы­
ходя из области видимости, освобождает память, на которую указывает (с помощью
деструктора).
Преимущество такого интеллектуального указателя становится очевидным при ра­
боте с полиморфными объектами, как демонстрирует приведенный далее код, в кото­
ром интеллектуальный указатель позволяет избежать срезки (slicing):
// Пример срезки при передаче полиморфных объектов по значению
// Fish - базовый класс для классов Tuna и Carp, а
// Fish::Swim() - виртуальная функция
void MakeFishSwim(Fish aFish) // Обратите внимание на тип параметра
{
aFish.Swim();

// Виртуальная функция

// ... Некая функция
Carp freshWaterFish;
MakeFishSwim(freshWaterFish); // Срезка: функции MakeFishSwimf) пере77 дается только часть Fish объекта Carp
Tuna marineFish;
MakeFishSwim(marineFish);

// Снова срезка

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

632

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

ЛИСТИНГ 26.2. Использование интеллектуального указателя на основе глубокого
копирования для передачи полиморфных объектов через их базовые типы_______
0: template
1: class deepcopy_smart_ptr
2: {
3:
private:
4:
T*
object;
5:
public:
6:
//... прочие функции
7:
8:
// копирующий конструктор указателя
9:
deepcopy_smart_ptr(const deepcopy_smartjptr& source)

10 :

{

11:
12:
13:
14:
15:
16:
17:
18:
19:

// Clone() - виртуальная функция
,гарантирующая глубокое
object = source->Clone();
//
копирование
}
// Оператор копирующего присваивания
deepcopy_smart_ptr& operator=(const deepcopy_smart_ptr& source)
{
if (object)
delete object;

20 :
21:
22:
23: };

object = source->Clone();

)

Анализ
Как можно видеть, класс d e e p c o p y j s m a r t j p o i n t e r в строках 9 -1 3 реализует ко­
пирующий конструктор, который обеспечивает глубокое копирование полиморфного
объекта с помощью функции C lo n e ( ) , которую должен реализовать класс. Точно так
же реализуется оператор копирующего присваивания в строках 16-22. Для простоты
в этом примере подразумевается, что виртуальная функция C lo n e () реализована ба­
зовым классом F i s h . Как правило, интеллектуальные указатели, реализующие модель
глубокого копирования, получают данную функцию либо как параметр шаблона, либо
как функциональный объект.
Таким образом, когда интеллектуальный указатель передается как указатель на ба­
зовый класс F i s h , часть C a r p не срезается:
deepcopy_smart_ptr freshWaterFish(new Carp);
MakeFishSwim(freshWaterFish);
// Carp не будет срезан

Глубокое копирование, реализованное в конструкторе интеллектуального указате­
ля, обеспечивает передачу объекта без срезки, даже при том что синтаксически функ­
ции M a k e F is h S w im () требуется только его базовая часть.

Типы интеллектуальных указателей

633

Недостаток механизма глубокого копирования — в низкой производительности.
Для некоторых приложений это не проблема, но в ряде случаев потеря производи­
тельности может не позволить программисту использовать такой интеллектуальный
указатель в своем приложении. В такой ситуации программист может просто передать
функции наподобие MakeFishSwim () указатель на базовый класс (обычный указатель
Fish*). Другие интеллектуальные указатели пытаются решить проблему снижения
производительности иными методами.

Механизм копирования при записи
Идиома копирования при записи (Copy on Write — COW) пытается оптимизировать
производительность интеллектуальных указателей с глубоким копированием за счет
совместного использовании указателей до первой попытки записи объекта. При пер­
вой попытке вызова неконстантной функции такой указатель обычно создает копию
объекта, для которого вызвана эта неконстантная функция, в то время как другие эк­
земпляры указателя продолжают совместно использовать исходный объект.
Такие указатели с копированием при записи имеют множество приверженцев.
Поклонники указателей COW полагают реализацию константных и неконстантных
версий операторов * и -> ключевым моментом, обеспечивающим функциональность
таких указателей. Неконстантные версии операторов создают копии.
Главное при выборе для своего проекта указателя, реализующего идиому копиро­
вания при записи, — это ясное понимание подробностей его реализации до его при­
менения в своих программах. В противном случае вы рискуете оказаться в ситуации,
когда копий окажется или слишком мало, или слишком много.

Интеллектуальные указатели со счетчиком ссылок
Идиома счетчика ссылок (reference counting) в общем случае представляет собой
механизм подсчета количества пользователей объекта. Когда их количество сокраща­
ется до нуля, объект освобождается. Счетчик ссылок — это очень хороший механизм
для совместного использования объектов без необходимости их копирования. Если
вы когда-либо работали с технологией Microsoft под названием “СОМ”, то концепция
подсчета ссылок, определенно, должна быть вам знакома.
У таких интеллектуальных указателей должен быть счетчик ссылок, увеличиваю­
щийся при копировании объекта указателя. Есть по крайней мере два популярных
способа хранения этого счетчика.
■ Счетчик ссылок содержится в объекте, на который указывает указатель.
■ Счетчик ссылок поддерживается классом указателя и представляет собой совмест­
но используемый объект.
Первый вариант, когда счетчик ссылок содержится в объекте, называется внедрен­
ным счетчиком ссылок (intrusive reference counting), поскольку при этом должен быть
изменен сам объект. В этом случае объект содержит счетчик ссылок, осуществляет его
инкремент и предоставляет его значение любому классу интеллектуального указате­
ля, управляющему им. Кстати, именно этот подход используется в технологии СОМ.

634

|

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

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

Интеллектуальный указатель со списком ссылок
Интеллектуальный указатель со списком ссылок (reference-linked) не подсчитывает
количество ссылок; ему надо только знать, когда количество ссылок достигнет нуля,
чтобы можно было освободить объект.
Такие интеллектуальные указатели называются указателями со списком ссылок по­
тому, что их реализация основана на двухсвязном списке. Когда новый интеллектуаль­
ный указатель создается как копия существующего, он добавляется в список. Когда
интеллектуальный указатель выходит из области видимости или удаляется, деструк­
тор удаляет этот указатель из этого списка. Такой интеллектуальный указатель, как и
интеллектуальный указатель со счетчиком ссылок, страдает от проблем, вызванных
циклической зависимостью.

Деструктивное копирование
Деструктивное копирование (destructive сору) — это механизм, который при копи­
ровании интеллектуального указателя передает получателю полное владение храни­
мым объектом, а сам сбрасывается:
destructive_copy_smartptr smartPtr(new SampleClass ()) ;
SomeFunc(smartPtr); // Владение передается в SomeFunc
// He используйте больше smartPtr в вызывающей функции!

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

Типы интеллектуальных указателей

| 635

Реализация указателей с деструктивным копированием отличается от рекомендо­
ванных стандартных подходов программирования на языке C++ (листинг 26.3).

ВНИМАНИЕ!

Указатель s t d : : a u t o _ p t r является, безусловно, наиболее популярным
(или известным, в зависимости от вашей точки зрения) указателем де­
структивного копирования. Такой интеллектуальный указатель бесполезен
после того, как он был передан функции или скопирован в другой.
Использование указателя s t d : : a u t o _ p t r в языке С++11 не рекомен­
довано. Вместо него следует использовать указатель s t d : : u n i q u e _ p t r ,
который не может быть передан по значению благодаря закрытым копи­
рующему конструктору и оператору копирующего присваивания; он может
быть передан как аргумент только по ссылке.

ЛИСТИНГ 26.3. Типичный интеллектуальный указатель деструктивного копирования
0:
1:
2:
3:
4:
5:
6:
7:

template
class destructivecopyjptr
{

private:
T* object;
public:
destructivecopyjptr(T* input):object(input)
^destructivecopyjptr() { delete object; }

{)

8:
9:
10:

11:
12:
13:
14:
15:
16:
17:
18:
19:
20:

21 :

// Копирующий конструктор
destructivecopyjptr(destructivecopyjptr& source)
{

// Получение копии во владение
object = source.object;
// Удаление источника
source.object = 0;
}
// Оператор копирующего присваивания
destructivecopyjptrS operator=(destructivecopy_ptr& rhs)
{

22:
if
23:
{
24:
25:
26:
27:
}
28:
}
29: };
30
31 int main()

(object != source.object)
delete object;
object = source.object;
source.object = 0;

636

|

32: {
33:
34:
35:
36:
37:
38: }

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

destructivecopy_ptr num(new int);
destructivecopy_ptr copy = num;
// num теперь является некорректным указателем
return 0;

Анализ
В листинге 2 6 .3 показана самая важная часть реализации интеллектуального ука­
зателя с деструктивным копированием. Строки 1 0 -1 7 и 2 0 - 2 8 содержат копирующий
конструктор и оператор копирующего присваивания соответственно. Эти функции де­
лают копируемый указатель недействительным, т.е. после копирования исходный ука­
затель получает значение n u l l p t r , оправдывая тем самым название д е с т р у к т и в н о е
к о п и р о в а н и е . Оператор присваивания делает то же самое. Таким образом, указатель
num фактически становится некорректным в строке 3 4 после присваивания другому
указателю. Такое поведение для операции присваивания контринтуитивно.

ВНИМАНИЕ!

Копирующий конструктор и оператор копирующего присваивания, которые
критически важны для реализации интеллектуальных указателей с деструк­
тивным копированием, продемонстрированные в листинге 26.3, вызывают
массу критических замечаний. В отличие от большинства классов C++, у
этого класса не может быть копирующего конструктора и оператора при­
сваивания, получающих константные ссылки, поскольку они должны изме­
нять исходный объект, делая его недействительным после копирования. Это
не только является отклонением от традиционной семантики копирующего
конструктора и оператора присваивания, но и делает использование клас­
са интеллектуального указателя непонятным интуитивно. Мало кто может
ожидать, что оригинал после копирования окажется недействительным. Тот
факт, что такие интеллектуальные указатели уничтожают исходный объект
копирования, делает их неподходящими для использования в контейнерах
STL, таких как s t d : : v e c t o r или любой другой коллекции, которую вы
могли бы использовать. Эти контейнеры должны уметь внутренне копиро­
вать свое содержимое, а это приводит к тому, что указатели оказываются
недействительными.
В силу всех этих причин многие разработчики боятся интеллектуальных ука­
зателей с деструктивным копированием как чумы.

СОВЕТ

Начиная со стандарта С++11 применение указателя s t d : : a u t o _ p t r не
рекомендуется; вместо него следует использовать интеллектуальный указа­
тель s t d : : u n i q u e _ p t r .

Типы интеллектуальных указателей

| 637

Использование интеллектуального
указателя s t d : : u n iq u e j p t r
Класс s t d : :u n iq u e _ p tr — нововведение стандарта С++11; он несколько отлича­
ется от класса a u to _ p tr тем, что не допускает копирование и присваивание.

СОВЕТ

Чтобы использовать класс

s t d : :u n iq u e jp tr , в исходный текст програм­

мы необходимо включить соответствующий заголовочный файл:

#include

Класс u n iq u e _ p tr — это простой интеллектуальный указатель, подобный пред­
ставленному в листинге 26.1, но с закрытыми копирующим конструктором и опера­
тором присваивания, чтобы запретить копирование при передаче его в функции по
значению или при присваивании. Его применение демонстрируется в листинге 26.4.
ЛИСТИНГ 26.4. Использование класса s t d : : u n iq u e p tr _____________________________
0:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

#include
#include
// Для использования std::unique_ptr
using namespace std;

11:

};

12 :
13:
14:
15:
16:
17:
18:
19:
20:

class Fish
{
public:
Fish() {cout « "Fish: создан!" « endl;}
-Fish() {cout « "Fish: уничтожен!" « endl;}
void Swim() const {cout «

"Плавает в воде" «

endl;}

void MakeFishSwim{const unique_ptr& inFish)
{
inFish->Swim();
}
int main()
{
unique_ptr smartFish(new Fish);

21 :
22:
23:
24:
25:
26:
27:
28:
29: }

smartFish->Swim();
MakeFishSwim(smartFish); // OK, MakeFishSwim использует ссылку
unique_ptr copySmartFish;
// copySmartFish = smartFish; // Ошибка: operator= закрытый
return 0;

638

|

ЗАНЯТИЕ 26. Понятие интеллектуальных указателей

Результат
Fish: создан!
Плавает в воде
Плавает в воде
Fish: уничтожен!

Анализ
Рассмотрим последовательность вывода. Обратите внимание: при том что объект,
на который указывает указатель s m a r t F is h , был создан в функции m a in (), он был, как
и ожидалось, освобожден (автоматически) без явного вызова оператора d e l e t e . Тако­
во поведение класса u n i q u e _ p t r : выходящий из области видимости указатель осво­
бождает объект, которым владеет, с помощью деструктора. Обратите внимание, как в
строке 23 можно передать указатель s m a r t F i s h в качестве аргумента функции М а к е F i s h S w i m ( ). Здесь нет копирования, поскольку функция M a k e F is h S w im f ) получает
параметр по ссылке (см. строку 13). Если вы удалите символ ссылки &из строки 13,
то немедленно получите ошибку компиляции, поскольку копирующий конструктор
закрыт и недоступен. Аналогично присваивание одного объекта класса u n i q u e p t r
другому, как показано в строке 26, также не разрешено благодаря закрытому операто­
ру копирующего присваивания.
Таким образом, указатель класса u n i q u e p t r безопаснее, чем указатель класса
a u t o j p t r (который ныне не рекомендован к употреблению), поскольку не делает не­
действительным исходный объект интеллектуального указателя во время копирования
или присваивания. Тем не менее он же обеспечивает простое управление памятью,
освобождая объект во время уничтожения интеллектуального указателя.

СОВЕТ

В листинге 26.4 показано, что u n i q u e j p t r не поддерживает копирование:

copySmartFish = smartFish; // Ошибка: оператор = закрытый
Однако семантика перемещения поддерживается. Таким образом, следую­
щий вариант кода работает:

unique_ptr sameFish(std::move(smartFish));
// Теперь указатель smartFish пуст
Если вы хотите написать лямбда-выражение с захватом u n i q u e _ p t r , ис­
пользуйте в захвате s t d : :m ove ( ) , что поддерживается начиная со стан­
дарта C++14:

std::unique_ptr alphabet(new char);
*alphabet = 's’;
auto lambda = [capture = std::move(alphabet)]()
{ std::cout « *capture « endl; };
// Теперь alphabet пуст, так как его содержимое перемещено
lambda();
Не огорчайтесь, если приведенный выше код кажется вам слишком экзоти­
ческим; он и в самом деле сложен, а использованная в нем конструкция,
скорее всего, большинству профессиональных программистов в их практи­
ке никогда не встретится.

Популярные библиотеки интеллектуальных указателей

ПРИМЕЧАНИЕ

При

написании

многопоточного

s t d : :s h a r e d _ p tr

и

приложения

используйте

639

классы

s t d : :w eak jp tr, предоставляемые С++11-

совместимыми библиотеками. Они облегчают безопасное с точки зрения
многопоточности совместное использование объектов на основе счетчиков
ссылок.

Популярные библиотеки
интеллектуальных указателей
Очевидно, что версия интеллектуального указателя, предоставляемого стандартной
библиотекой C++, не в состоянии удовлетворить требования каждого программиста.
Поэтому имеется множество библиотек интеллектуальных указателей.
Библиотека Boost (w w w .b o o st.o r g ) предоставляет набор хорошо проверенных и
документированных классов интеллектуальных указателей, а также многих других
полезных вспомогательных классов. Получить подробную информацию об интеллек­
туальных указателях Boost и загрузить их можно по адресу h t t p : //w w w .b o o s t .o r g /
lib s /s m a r t _ p t r /s m a r t _ p t r ,htm.

Резюме
На этом занятии рассмотрено, как использование подходящих интеллектуальных
указателей способно сократить количество выделений памяти и помочь в решении
вопросов, связанных с владением объектами. Вы изучили различные типы интеллек­
туальных указателей и, что важнее всего, их поведение в конкретных приложениях.
Теперь вы знаете, что не должны использовать указатель s t d : :a u to _ p tr , поскольку
он делает недействительным исходный объект при копировании и присваивании. Вы
также узнали о новом классе интеллектуального указателя s t d : :u n iq u e _ p tг, совмес­
тимом со стандартом С ++11.

Вопросы и ответы
■ Мне нужен вектор указателей. Могу ли я выбрать класс au to jp tг в качестве
типа объекта, который будет содержаться в векторе?
Вы вообще не должны использовать класс s td : :a u to j? tr . Это не рекомендуется.
Единственной операции копирования или присваивания достаточно, чтобы сделать
исходный объект недействительным.

■ Какие два оператора должен реализовать класс, чтобы называться классом
интеллектуального указателя?
Это операторы разыменования * и выбора члена ->. Они позволяют использовать
объекты класса интеллектуального указателя с использованием семантики обыч­
ного указателя.

640

|

ЗАНЯТИЕ 26- Понятие интеллектуальных указателей

■ У меня есть приложение, в котором классы C lass 1 и C lass2 содержат атри­
буты, указывающие один на другой. Должен ли я использовать в этом случае
указатель со счетчиком ссылок?
Вероятно, нет — из-за циклической зависимости, которая не позволит обнулить
счетчик ссылок, а следовательно, оставит объекты двух классов в выделенной па­
мяти, не освобождая их.

■ Класс str in g также управляет символьным массивом, расположенным в дина­
мической памяти. Является ли класс s tr in g интеллектуальным указателем?
Нет. Такие классы обычно не реализуют операторы * и -> , а поэтому не могут счи­
таться интеллектуальными указателями.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. Где стоит поискать готовый интеллектуальный указатель, прежде чем присту­
пить к написанию собственного класса интеллектуального указателя для своего
приложения?
2. Может ли интеллектуальный указатель существенно замедлить ваше приложение?
3. Где интеллектуальные указатели со счетчиками ссылок могут хранить эти счетчики?
4. Должен ли связанный список, применяемый в интеллектуальных указателях со
списком ссылок, быть односвязным или двухсвязным?

Упражнения
1. Отладка. Найдите ошибку в следующем коде:
std::auto_ptr object(new SampleClass());
std::auto_ptr anotherObject(object);
object->DoSomething();
anotherObject->DoSomething();

2. Используйте класс u n iq u e _ p tr для создания экземпляра класса Carp, наслед­
ника класса F ish . Передайте объект как указатель на F ish и отметьте коммен­
тарием срезку, если таковая имеет место.

3. Отладка. Укажите ошибку в следующем коде:
std::unique_ptr myTuna(new Tuna);
unique_ptr copyTuna;
copyTuna = myTuna;

ЗАНЯТИЕ 27

Применение
потоков для
ввода и вывода
Фактически вы использовали потоки на всем протяжении
этой книги начиная с первого же занятия, где на экран вы­
водилась строка “Hello World” с помощью потока s t d : :c o u t.
Пришло время обратить внимание на эту часть языка C + + и
изучить потоки с практической точки зрения.
На этом занятии...
я

Что такое потоки и как они используются

■ Как записывать и читать файлы, используя потоки
■ Вспомогательные операции с потоками C ++

642

|

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

Концепция потоков
Предположим, вы разрабатываете программу, которая читает данные с диска, вы­
водит их на экран, читает пользовательский ввод с клавиатуры и сохраняет данные на
диске. Разве не было бы хорошо, если бы вы могли выполнять все действия чтения и
записи с использованием одинаковой схемы независимо от устройства или местопо­
ложения, откуда поступают данные? Именно это и обеспечивают потоки C++.
Потоки (stream) C++ — это обобщенная реализация логики чтения и записи (дру­
гими словами, ввода и вывода), позволяющая использовать единообразные схемы
чтения и записи данных. Эти схемы одинаковы независимо от того, читаете ли вы
данные с диска, с клавиатуры или записываете их на диск или на экран. Нужно только
использовать соответствующий потоковый класс, а уж его реализация позаботится о
подробностях, специфических для устройства или операционной системы.
Давайте обратимся к соответствующей строке из вашей первой программы на C++
(см. листинг 1.1):
std::cout «

"Hello World!" «

std::endl;

Здесь s t d : :c o u t — это потоковый объект класса ostream , предназначенный для
вывода информации на консоль. Чтобы использовать класс s t d : :c o u t, необходимо
включить в исходный текст заголовочный файл < io strea m > , который предоставляет
эту и другие функциональные возможности, такие как объект s t d : : c in , позволяю­
щий читать из потока.
Что я подразумеваю, когда говорю, что потоки обеспечивают единообразный и
специфический для устройств доступ? Например, если бы необходимо было записать
текст “Hello World” в текстовый файл, то можно было бы использовать такой синтак­
сис объекта файлового потока f sH e llo :
fsHello «

"Hello World!" «

endl; // Запись в файловый поток

Как можно заметить, после выбора соответствующего потокового класса запись
текста “Hello World” в файл практически не отличается от вывода на экран.

СОВЕТ

Оператор

o p e r a t o r « , используемый при записи в поток, называется

оператором вывода в поток (stream insertion operator). Он используется при
выводе на экран, в файл и т.д.
Оператор

o p e r a t o r » , используемый при записи потока в переменную,

называется оператором извлечения из потока (stream extraction operator).
Он используется при чтении данных, вводимых с клавиатуры, из файла и т.д.

Заметим, что на этом занятии потоки рассматриваются с практической точки зрения.

Важнейшие классы и объекты потоков C++

643

Важнейшие классы
и объекты потоков C++
Язык C++ предоставляет набор стандартных классов и заголовочных файлов, по­
зволяющих выполнять ряд наиболее важных и часто используемых операций ввода и
вывода. В табл. 27.1 содержится список наиболее часто используемых классов.

ТАБЛИЦА 27.1. Наиболее часто используемые классы потоков C++
в пространстве имен s t d
Класс или объект Описание
co u t
c in

Стандартны й п оток вы вода, как п равил о, п ер е а д р е су е м ы й на к о н со л ь
Станд артны й п оток ввода, как п равил о, и сп о л ьзуе м ы й для чтения
д ан н ы х в п ер е м е н н ы е

cerr
fstr e a m

Стандартны й поток в ы вода для с о о б щ е н и й о б о ш и б к а х
Класс п отока ввода и вы вода для ф айло вы х оп ерац и й; п р о и з в о д н ы й
от к ла ссо в

o fstr e a m

o fs tr e a m и if s t r e a m

Класс потока вы во да для ф айло вы х оп е р а ц и й , о б ы ч н о и сп о льзуе тся
д ля со зд а н и я ф айлов

if s t r e a m

Класс потока ввода для ф айло вы х оп е р а ц и й , о б ы ч н о и спо льзуется
д ля чтения из файла

s tr in g s t r e a m

Класс потока ввода и вы вода для стр о к о в ы х оп ераци й; п р о и зв о д н ы й
от классов is t r in g s t r e a m и o s tr in g str e a m ; о б ы ч н о используется
для вы п о л н е н и я п р е о б р а зо в а н и я в стр о ки (или из строк) д р уги х ти по в

ПРИМЕЧАНИЕ

Объекты

co u t, c i n и c e r r являются глобальными объектами потоко­
ostream , is tr e a m и o strea m соответственно. Будучи

вых классов

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

m a in ().

При использовании потокового класса есть возможность использовать манипуля­
торы (manipulator), которые выполняют определенные действия по настройке пото­
ков. Одним из них является манипулятор s t d : : e n d l, который использовался нами для
вывода символа новой строки:
std::cout «

"Строка заканчивается здесь ->" «

std::endl;

Некоторые другие манипуляторы и флаги приведены в табл. 27.2.

644

I

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

ТАБЛИЦА 27.2. Наиболее часто используемые манипуляторы
для работы с потоками
Манипуляторы
вывода

Задача

en d l
ends

Вставляет си м в о л но в ой стр о ки

Манипуляторы систем
счисления

Задача

d ec

Вставляет нулевой си м во л

Вы н уж д ает п оток и н те р п р е ти р о в а ть ввод или отображ ать
в ы во д как д е ся ти ч н о е ч и сл о

hex

Вы н уж д ает п оток и н те р п р е ти р о в а ть ввод или отображ ать
в ы во д как ш е стн а д ц ате р и чн ое ч и сл о

oct

Вы н уж д ает п оток и н те р п р е ти р о в а ть ввод или отображ ать
в ы во д как в о сь м е р и ч н о е ч и сл о

Манипуляторы
представления значений
с плавающей запятой
fix e d

Задача

Вы н уж дает поток о то бра ж ать значен ия в ф ор м е с ф и кси ро­
ван но й точ кой

s c ie n tific

Вы нуж д ает п оток ото браж ать значен ия в научн ом (экспо­

s e tp r e c is io n

У станавливает то ч н о сть д е ся ти ч н о го п редставления, пере­

setw
s e tfill

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

se tb a se

У станавливает о с н о в а н и е си сте м ы сч исл ени я, и спо льзуя

s e tio s fla g

У станавливает флаги с и сп о л ь зо в а н и е м в ход н ого

не н ц и а л ьн ом ) п ред ста в ле н и и
д ан ную как п ара м е тр
У станавливает си м в о л зап ол нени я, п ере д а н н ы й как пара­
м етр

dec, hex или o c t в качестве парам етра
п ар а м е тр а-м аски с ти п о м

r e s e tio s fla g

s td : : io s _ b a s e : :fm t f l a g s

В о сстанавли вает значен ия по ум о л ч ан и ю для о п р е д е ­
л е н н о го типа, у ка зы в а е м о го со д е р ж и м ы м

s t d : :io s

b a s e : : f m t f la g s

Использование std: :cout для вывода
форматированных данных на консоль
Объект s t d : : co u t, используемый для записи в поток стандартного устройства вы­
вода, является, вероятно, самым используемым потоком в этой книге (и не только).
Сейчас пришло время вернуться к потоку c o u t и использовать некоторые из манипу­
ляторов для изменения способа выравнивания и отображения данных.

Использование std::cout для вывода форматированных данных на консоль

645

Изменение формата представления чисел
Поток s td : :c o u t можно заставить отображать целые числа в шестнадцатеричной
или восьмеричной записи. В листинге 27.1 демонстрируется использование потока
co u t для отображения введенных чисел в различных системах счисления.
Л И С Т И Н Г 2 7 .1 . О т о б р а ж е н и е ц е л о го ч и сл а с и с п о л ь з о в а н и е м
п отока

c o u t и ф л а го в ____________________________________________________

0 : #include
1 : #include
2 : using namespace std;

3:
4: int main()
5: {
6:
cout « "Введите
7:
int input = 0 ;
8:
cin » input;

целое число: ";

9:
10:
11:

cout «
cout «

"Восьмеричная запись
:
"Шестнадцатеричная запись:




oct «
hex «

input
input

«
«

endl;
endl;

12 :
13:
14:
15
16:
17:
18:
19
20:
21:

22 :

cout « "Шестнадцатеричная запись с указанием основания: ";
cout « setiosflags(ios_base::hex|ios_base::showbase|
ios_base::uppercase) « input « endl;
cout « "После сброса флагов ввода-вывода
: ";
cout « resetiosflags(ios_base::hex|ios_base::showbase|
ios_base::uppercase) « input « endl;
return 0 ;
}

Результат
Введите целое число: 2 5 3
Восьмеричная запись
: 375
Шестнадцатеричная запись: fd
Шестнадцатеричная запись с указанием основания: 0XFD
После сброса флагов ввода-вывода
: 253

Анализ
В примере использованы представленные в табл. 27.2 манипуляторы для изменения
способа отображения потоком c o u t введенного пользователем целого числа. Обратите
внимание на использование манипуляторов o c t и hex в строках 10 и 11. В строках 14
и 15 функция s e t i o s f l a g s () используется для задания отображения числа пропис­
ными буквами в шестнадцатеричном формате. В результате поток c o u t отображает

|

646

ЗАНЯТИЕ 27.

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

введенное целое число 253 как 0XFD. В результате использования функции r e s e t i o f l a g s () в строках 18 и 19 поток c o u t снова отображает целое число в десятичном
виде. Вот другой способ смены отображения целых чисел в десятичном виде:
cout «

dec «

input «

endl; // Отображать в десятичном формате

При отображении потоком c o u t таких чисел, как я, можно также задавать точ­
ность, т.е. определять количество знаков десятичного числа после запятой, которое
будет представлено, либо задать отображение числа в экспоненциальном представле­
нии (листинг 27.2).

ЛИСТИНГ 27.2.
0
1
2

И спользование

c o u t для о т о б р а ж е н и я чи сл а я и п л ощ ад и круга

tinclude
#include
using namespace std;

3
4
5

int main()

{

6

const double Pi = 3.1415926535898;
cout « "Pi = " « Pi « endl;

7

8
9

10

11

12
13
14
15
16
17
18
19

cout
cout
cout
cout
cout

«
«
«
«
«

endl « "Точность = 7 : " « endl;
setprecision(7);
"Pi = " « Pi « endl;
fixed « "Фиксированная запись Pi = " « Pi « endl;
scientific « "Научная запись Pi = " « Pi « endl;

cout
cout
cout
cout
cout

«
«
«
«
«

endl « "Точность = 10 : " « endl;
setprecision(1 0 );
"Pi = " « Pi « endl;
fixed « "Фиксированная запись Pi = " « Pi « endl;
scientific « "Научная запись Pi = " « Pi « endl;

20

21

cout « endl « "Введите радиус: ";
double Radius = 0.0;
cin » Radius;
cout « "Площадь круга: " « 2*Pi*Radius*Radius «

22
23
24
25
26
27

return 0 ;

}

Результат
Pi = 3.14159
Точность = 7:
Pi = 3.141593

endl;

Использование std::cout для вывода форматированных данных на консоль

647

Фиксированная запись Pi = 3.1415927
Научная запись Pi = 3.1415927е+00
Точность = 10:
Pi = 3.1415926536е+00
Фиксированная запись Pi = 3.1415926536
Научная запись Pi = 3.1415926536е+00
Введите радиус: 9.99
Площадь круга: 6.2706252198е+02

Анализ
Вывод демонстрирует, что при увеличении точности до 7 в строке 10 и до 10 в
строке 16 представление значения числа Pi изменяется. Обратите также внимание на
то, что после применения манипулятора s c i e n t i f i c результат вычисления площади
круга отображается как 6.2706252198е+02.

Выравнивание текста и установка ширины поля
Для установки ширины поля в символах можно использовать такой манипулятор,
как s e t w ( ). В результате любая вставка в поток осуществляется с выравниванием
по правому краю с указанной шириной. Аналогично манипулятор s e t f i l l () при­
меняется для определения символа, заполняющего пустое пространство в ситуации,
показанной в листинге 27.3.
Л И СТИ Н Г 2 7 ,3 .

0
1
2

Установка ширины поля и символа заполнения

#include
#include
using namespace std;

3
4
5

int main()

{

6

cout «

"По умолчанию!" «

endl;

cout «
cout «

setw(35); // установка поля шириной 35 символов
"Выравнивание вправо!" « endl;

cout «
cout «

setw(35) « setfill(’*');
"Выравнивание вправо!" « endl;

cout «

"Опять по умолчанию!" «

7

8
9

10

11

12
13
14
15
16
17

return 0 ;

}

endl;

648

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

Результат
По умолчанию!
Выравнивание вправо!
***************2 Ь1р а в н и в а н И 0 вправо!
Опять по умолчанию!

Анализ
Вывод демонстрирует результат применения манипулятора se tw (3 5 ) в строке 8
и манипулятора s e t f i l l ( 1* 1) в строке 11 для объекта c o u t. Как можно видеть, в
предпоследней строке вывода свободное пространство, предшествующее тексту, за­
полнено звездочками, как и определено манипулятором s e t f i l l ().

Использование s t d :

:c in

для ввода

Поток s t d : : c i n универсален — он позволяет читать данные простых типов, та­
ких как i n t , d o u b le или char*, а также читать с экрана строки и символы, используя
такие методы, как g e t l i n e ().

Использование s t d : : c i n для ввода
простых старых типов данных
С помощью потока c i n можно читать целые числа, числа с плавающей точкой
или символы непосредственно из стандартного устройства ввода. Листинг 27.4 де­
монстрирует применение потока c i n для чтения простых типов данных, введенных
пользователем.

ЛИСТИНГ 27.4. Использование потока c in для чтения простых типов данных__________
0 : #include
1 : using namespace std;
2:
3: int main()
4: {
5:
cout « "Введите целое число: ";
6:
int inputNum = 0;
7:
cin »
inputNum;

8:
9:
10:
11:

cout «
double
cin »

"Введите число Pi: ";
Pi = 0.0;
Pi;

12 :
13:
14:
15:
16:

cout « "Введите три символа, разделенных пробелами: "«endl;
char charl = '\0', char2 = '\0', char3 = 1 \01;
cin » charl » char2 » char3;

Использование std::cin для ввода
17:
18:
19:
20:

cout
cout
cout
cout

«
«
«
«

"Введены следующие переменные: " « endl;
"inputNum: " « inputNum « endl;
"Pi: " « Pi « endl;
"Три символа: " « charl « char2 « char3 «

649

endl;

21 :
22:
23: }

return 0 ;

Результат
Введите целое число: 1 2 3 4
Введите число Pi: 0 .3 1 4 1 5 9 2 6 е 1
Введите три символа, разделенных пробелами: C + +
Введены следующие переменные:
inputNum: 1234
Pi: 3.14159
Три символа: C++

Анализ
Самая интересная часть листинга 27.4 заключается в вводе значения P i с исполь­
зованием экспоненциальной формы записи и сохранении этих данных в переменной
P i типа d ou b le. Обратите внимание, как три символьные переменные заполняются в
пределах одной строки (строка 15).

И спользование м етода s t d :
для ввода в буф ер c h a r*

: c in : :g e t()

Поток c in позволяет записывать данные непосредственно в переменную типа in t;
подобное можно сделать и с массивом ch ar в стиле С:
cout « "Введите строку: " « endl;
char charBuf[10] = {0}; // Может содержать максимум 10 символов
cin » charBuf; // Опасно: пользователь может ввести больше 10 символов

При записи в символьный буфер очень важно не выйти за его границы, чтобы из­
бежать аварийного завершения программы или нарушения системы безопасности.
Поэтому лучше читать данные в символьный буфер следующим образом:
cout « "Введите строку: " « endl;
char charBuf[10] = {0};
cin.get(charBuf, 9);
// Прекращение вставки на 9-м символе

Этот более безопасный способ вставки текста в буфер стиля С использован в лис­
тинге 27.5.

650

|

З А Н Я Т И Е 2 7 . П р и м е н е н и е п о то ко в для в в о д а и в ы в о д а

Л И С Т И Н Г 2 7 .S . В с т а в к а в с и м в о л ь н ы й б уф е р б е з в ы хо д а з а е го гр а н и ц ы
0 : #include
1 : #include
2 : using namespace std;

3:
4: int main()
5: {
cout « "Введите строку: " « endl;
6:
7:
char charBuf[10] = {Obcin.get(charBuf, 91;
8:
cout « "charBuf:: " « charBuf « endl;
9:
10:
return 0 ;
11:
12: }

Результат
Введите строку:

Длинная строка, выходящая за границы
charBuf: Длинная с

Анализ
Как показывает вывод, благодаря использованию в строке 8 метода c i n : : g e t () в
буфер c h a r записаны только первые девять символов. Это самый безопасный способ
работы с буферами фиксированной длины.

СОВЕТ

По возможности не используйте массивы типа c h a r . Используйте вместо
них тип s t d : : s t r i n g .

Использование s t d : : c i n для ввода
в переменную типа s t d : : s t r i n g
Поток c i n — весьма универсальный инструмент, позволяющий поместить введен­
ную пользователем строку непосредственно в переменную типа s t d : : s t r i n g :
std::string input;
cin » input; // Прекращение вставки при первом пробеле

В листинге 27.6 показан ввод с помощ ью потока c i n

в переменную типа

s t d : : s t r in g .
Л И С Т И Н Г 2 7 .6 . В с т а в к а те к ста в стр о к у s t d : : s t r i n g с п о м о щ ь ю s t d : : c i n _____________
0 : #include
1 : #include
2 : using namespace std;

Использование std::cin для ввода

|

651

3:
4: int main()
5: {
6:
cout « "Введите ваше имя: ";
7:
string name;
8:
сin » name;
9:
cout « "Привет, " « name « endl;

10 :
11:

12 :

return 0 ;
}

Результат
Введите ваше имя: Siddhartha Rao
Привет, Siddhartha

Анализ
Вывод отобразил мое имя не полностью, поскольку так была реализована про­
грамма. Я ожидал, что переменная паше, заполняемая из потока c in в строке 8, будет
содержать введенные мной имя и фамилию, а не только одно имя. Что же произошло?
Поток c in остановил вставку, когда встретился с первым пробелом.
Чтобы позволить пользователю ввести строку полностью, включая пробелы, не­
обходимо использовать функцию g e t l i n e ():
string name;
getline(cin, name);

Применение функции g e t l i n e () с потоком c in показано в листинге 27.7.
ЛИСТИНГ 27.7. Ч те н и е в в е д е н н о й стр о к и с п о м о щ ь ю ф ун кц и и g e t l i n e ()______________
0 : #include
1 : #include
2 : using namespace std;

3:
4: int main()
5: {
6:
cout « "Введите
ваше имя: ";
7:
string name;
8:
getline(cin, name);
9:
cout « "Привет,
" « name « endl;

10:
11:

12 :

return 0 ;
}

652

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

Результат
Введите ваше имя: Siddhartha Rao
Привет, Siddhartha Rao

Анализ
Функция g e t l i n e (), показанная в строке 8, решила проблему ввода символа про­
бела. Теперь вывод содержит введенную пользователем строку полностью.

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

s t d : : f s tre a m

Класс s t d : : f s t r e a m языка C++ обеспечивает (относительно) независимый от
платформы доступ к файлу. Класс s t d : : f strea m наследует класс s t d : : o f stream для
записи в файл и класс s t d : : i f strea m для чтения из него.
Другими словами, класс s t d : : f strea m обеспечивает возможность как чтения, так
и записи.

СОВЕТ

Чтобы использовать класс

s t d : : fstrea m , в исходный текст программы

необходимо включить соответствующий заголовочный файл:

#include

Открытие и закрытие файла с помощью
методов o p e n () и c l o s e ()
Прежде чем использовать объект класса f stream , o f stream или i f stream , необхо­
димо открыть файл с помощью метода open ():
fstream myFile;
myFile.open("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
if (myFile.is_open()) // Проверка успешности открытия файла

{
// Чтение или запись
myFile.close();

}
Метод open () получает два аргумента: первый — путь и имя открываемого файла
(если не указать путь, то подразумевается текущий каталог приложения), а второй —
режим открытия файла. Выбранный выше режим открытия позволяет создать файл
заново, даже если он уже существует ( i o s _ b a s e : : tr u n c ), а также читать и записы­
вать в файл ( in | ou t).

Использование потока std::fstream для работы с файлом

653

Обратите внимание на применение метода is _ o p e n () для проверки успешности
выполнения метода open ().

Закрытие файлового потока с помощью метода

c l o s e () важно для со­

хранения его содержимого.

Альтернативный способ открытия файлового потока — с использованием кон­
структора:
fstream myFile("HelloFile.txt",
iosjoase::in|iosjoase::out|ios_base::trunc);

Если необходимо открыть файл только для записи, используйте следующий режим
открытия:
ofstream myFile("HelloFile.txt", iosjoase::out);

Если же необходимо открыть файл только для чтения, смените режим его откры­
тия, как показано далее:
ifstream myFile("HelloFile.txt", ios_base::in);

СОВЕТ

Независимо от того, что вы используете, конструктор или метод open (),
рекомендуется проверять успешность открытия файла с помощью метода

is_ o p e n (), прежде чем использовать соответствующий объект файлового
потока.

Файловый поток может быть открыт в нескольких режимах.
■ io s _ b a s e :: арр. Дозапись в конец существующего файла без его усечения.
■ io s j o a s e : : a te . Переносит файловый указатель в конец файла, но запись данных
возможна в любое место файла.
■ i o s j o a s e :: tru n c. Усекает существующий файл (принят по умолчанию).
■ i o s j o a s e :: b in a r y . Создает бинарный файл (по умолчанию создается текстовый
файл).
■ i o s j o a s e :: in . Открывает файл для чтения.
■ i o s j o a s e :: ou t. Открывает файл для записи.

Создание и запись текстового файла
с использованием метода ореп() и оператора «
После открытия файлового потока вы можете писать в него, используя оператор «
(листинг 27.8).

654

|

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

ЛИСТИНГ 27.8. Создание нового текстового файла и запись в него
0 : #include
1 : #include
2 : using namespace std;

3:
4 : int main()
5: {
6:
of stream myFile;
7:
myFile.open("HelloFile.txt", ios_base::out);
8:

9:

if(myFile.is_open())

10:

{

11:

cout «

"Файл успешно открыт" «

endl;

12 :
13:
14:
15:
16:
17:
18:
19:
20:

21:

myFile «
myFile «

"Мой первый текстовый файл!" «
"Привет, файл!";

cout « "Закрытие файла" «
myFile.close();

endl;

endl;

}
return

0;

}

Результат
Файл успешно открыт
Закрытие файла

Содержимое файла H e l l o F i l e . t x t :
Мой первый текстовый файл!
Привет, файл!

Анализ
В строке 7 файл открывается в режиме i o s b a s e : : o u t , т.е. только для записи. В
строке 9 проверяется успешность выполнения метода o p e n ( ) , а затем осуществляется
запись в файловый поток с использованием оператора вывода o p e r a t o r « , как пока­
зано в строках 13 и 14. И наконец в строке 17 файл закрывается.
В листинге 27.8 демонстрируется, что запись в файловый поток выполня­
ется точно так же, как на стандартное устройство вывода (т.е. на консоль) с
использованием объекта c o u t .
Это означает, что потоки C++ обеспечивают единообразный способ работы
с различными устройствами, будь то запись текста на экран с помощью
объекта c o u t или запись в файл с помощью объекта типа o f s tr e a m .

Использование потока std::fstream для работы с файлом

655

Чтение текстового файла с использованием
метода ореп() и оператора »
Для чтения файла можно воспользоваться объектом fstr e a m , если открыть его с
помощью флага i o s j c a s e : : in , или использовать объект i f stream . В листинге 27.9
демонстрируется чтение из файла H e l l o F i l e . t x t , созданного в листинге 27.8.
Л И С Т И Н Г 2 7 .9 . Ч те н и е т е к ста и з ф а й л а

H e l l o F i l e . t x t , с о з д а н н о г о в л и сти н ге 27.8

0 : #include
1 : #include
2 : #include

3: using namespace std;
4:
5: int main()

6:

{

7:
8:
9:
10:

ifstream myFile;
myFile.open("HelloFile.txt",

11:

{

iosjoase::in);

if (myFile.is_open())

12:
13:
14:
15:
16:
17:
18:
19:

cout « ’’Файл успешно открыт. Он содержит:" «
string fileContents;

endl;

while(myFile.good())
{
getline(myFile, fileContents);
cout « fileContents « endl;
}

20:
21:
22:
23:
24:
25:
26:
27:
28: }

cout « "Закрытие файла."
myFile.close();

« endl;

}
else
cout «

”open(): ошибкаоткрытия

return 0 ;

Результат
Файл успешно открыт. Он содержит:
Мой первый текстовый файл!
Привет, файл!
Закрытие файла.

файла" «

endl;

656

|

З А Н Я Т И Е 2 7 . П р и м е н е н и е п отоко в для в во д а и вы во д а

ПРИМЕЧАНИЕ

Чтобы код листинга 27.9 прочитал текстовый файл

H e l l o F i l e . t x t , соз­

данный в листинге 27.8, следует либо переместить его в рабочий каталог
этого проекта, либо объединить этот код с предыдущим.

Анализ
Как обычно, вызов метода is _ o p e n () в строке 8 проверяет успешность вызова
метода open (). Обратите внимание на применение оператора » при чтении содер­
жимого файла в переменную типа s t r i n g , которая затем отображается с помощью
потока c o u t в строке 18. В данном примере функция g e t l i n e () используется для
чтения из файлового потока тем же способом, что и в листинге 27.7 при чтении ввода
пользователя, по одной строке за раз.

Запись и чтение из бинарного файла
Процесс записи в бинарный файл по сути не слишком отличается от процесса,
который вам уже известен в настоящий момент. При открытии файла следует исполь­
зовать флаг i o s _ b a s e : : b in a r y . Обычно для записи и чтения используются методы
o f s tr e a m : : w r it e () и i f stream : : r e a d ( ), показанные в листинге 27.10.
Л И С Т И Н Г 2 7 . 1 0 . З а п и с ь структуры в б и н а р н ы й ф айл и е е в о с с т а н о в л е н и е оттуда________
0 : #include
1 : #include
2 : #include

3:
4:
5:
6:
7:
8:
9:

#include
using namespace std;
struct Human
{
Human() {};
Human(const char* iName, int iAge, const char*iDOB):age(iAge)

10 :

{

11:
strcpy(name, iName);
12:
strcpy(DOB, iDOB);
13:
}
14:
15:
char name[30];
16:
int age;
17:
char DOB[20];
18: };
19:
2 0 : int main()

21 :
22:
23:
24:
25
26

{

Human input("Siddhartha Rao", 101, "Май 1916м);
ofstream fsOut("MyBinary.bin",ios_base::out|ios_base::binary);
if (fsOut.is_open())

Использование потока std::fstream для работы с файлом
27
28
29
30
31
32
33
34
35

{
cout « "Запись объекта в бинарный файл" « endl;
fsOut.write((const char*)&input,sizeof(input));
fsOut.close();

}
ifstream fsln("MyBinary.bin", ios_base::in Iios_base::binary);
if(fsIn.is_open())

36
37
38
39
40
41
42
43
44
45
46
47

657

{
Human somePerson;
fsin.read((char*)&somePerson, sizeof(somePerson));
cout
cout
cout
cout

«
«
«
«

"Чтение объекта из бинарного файла: " « endl;
"Имя
= " « some Per son. name « endl;
"Возраст
="«
somePerson.age « endl;
"Родился
="«
somePerson.DOB « endl;

}
return 0 ;

Результат
Запись объекта в бинарный файл
Чтение объекта из бинарного файла:
Имя
= Siddhartha Rao
Возраст = 101
Родился = Май 1916

Анализ
В строках 22-31 создается экземпляр структуры Human, содержащей атрибуты name,
a g e и DOB. Она сохраняется на диске в бинарном файле M y B i n a r y . b i n с использова­
нием объекта класса o f s t r e a m . Затем, в строках 33 -4 4 , эта информация считывается
из файла с использованием другого потокового объекта класса i f s t r e a m . Информа­
ция для вывода атрибутов, таких как nam e и другие, считывается из бинарного файла.
Этот пример демонстрирует применение объектов i f s t r e a m и o f s t r e a m для чтения и
записи файлов с использованием методов i f s t r e a m : : r e a d () и o f s t r e a m : : w r i t e ()
соответственно. Обратите внимание на приведение типов в строках 29 и 38, застав­
ляющее трактовать указатели на структуру как указатели на c h a r . 1
1 Примененный здесь способ записи и чтения с передачей адреса объекта класса работает
только в очень редких случаях “старых простых данных” (POD), когда у класса нет виртуаль­
ных функций, а все данные содержатся в классе — например, если бы то же поле name имело
тип char* и указывало на имя в динамической памяти, при записи файла в одной программе и
чтении в другой произошла бы ошибка (так как в файле была бы сохранена не строка имени, а
ее адрес). — Примеч. ред.

658

|

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

ПРИМЕЧАНИЕ

Если бы это был не демонстрационный код, я записывал бы структуру Hu­
man со всеми ее атрибутами в файл XML. Формат XML обеспечивает гиб­
кость и масштабируемость при хранении информации.

Human, в нее необходимо до­
numChildren), то вам придется поза­
ботиться о том, чтобы метод i f s t r e a m :: read () мог корректно читать
Если после сохранения такой структуры, как

бавить новые атрибуты (например,

бинарные данные, созданные более старой версией.

Использование s t d : : s t r i n g s t r e a m
для преобразования строк
Предположим, у нас имеется строка, содержащая строковое значение "45". Как пре­
образовать это строковое значение в целое число со значением 45 и обратно? Одной из
весьма полезных утилит, предоставляемых языком C++, является класс str in g str e a m ,
обеспечивающий выполнение множества преобразований такого рода.

СОВЕТ

Чтобы использовать класс

s t d : : s tr in g s tr e a m , в исходный текст про­

граммы необходимо включить соответствующий заголовочный файл:

#include

Некоторые из основных операций класса s t r in g s t r e a m продемонстрированы в
листинге 27.11.
ЛИСТИНГ 27.11. Преобразование целочисленного значения
в строковое и обратно с помощью s t d : : s t r in g s t r e a m _______________________________
0 : #include
1 : #include
2 : #include

3: using namespace std;
4:
5: int main()

6: {
7:
8:
9:

cout « "Введите целое число:";
int input = 0 ;
cin » input;

10:
11:
12:
13:
14:
15:

stringstream converterStream;
converterStream « input;
string strlnput;
converterStream » strlnput;

И с п о л ь з о в а н и е s td ::s trin g s tre a m д л я п р е о б р а з о в а н и я с т р о к

cout «
cout «

16
17
18
19

20
21
22
23
24
25
26:
27: }

"Введенное число = " « input « endl;
"Преобразовано в строку: " « stгInput «

659

endl;

stringstream anotherStream;
anotherStream « strlnput;
int copy = 0 ;
anotherStream » copy;
cout «

"Преобразовано в целое число: " «

copy «

endl;

return 0 ;

Результат
Введите целое число: 4 5
Введенное число = 4 5
Преобразовано в строку: 45
Преобразовано в целое число: 45

Анализ
Здесь пользователя просят ввести целочисленное значение. Сначала это целое чис­
ло вносится в объект класса s t r in g s t r e a m (строка 12) с помощью оператора « . За­
тем, в строке 14, оператор » используется для преобразования этого целого числа в
строку. Далее эта строка используется в качестве исходной для получения очередного
целочисленного значения, на этот раз в переменной сору.

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Используйте класс

Не забывайте закрывать файловый поток с помо­

i f s t r e a m тогда, когда на­

мереваетесь только читать из файла.

щью метода c l o s e () после его использования.

Используйте класс

Не забывайте, что

o f s t r e a m тогда, когда на­
мереваетесь только писать в файл.

Помните о проверке успешности

открытия фай­

лового потока с помощью метода is _ o p e n () .
Используйте ее до того, как использовать дан­
ный файловый поток.

в результате чтения с помо­

щью оператора » , как в случае
c in

»

s trD a ta ;

переменная s t r D a t a содержит текст лишь до
первого пробела, встреченного во вводимой
строке.

Не забывайте,

что функция g e t l i n e ( c i n ,

s t r D a t a ) ; извлекает из входного потока всю
строку, включая пробелы.

660

|

ЗАНЯТИЕ 27. Применение потоков для ввода и вывода

Резюме
На этом занятии рассмотрены потоки C++ с практической точки зрения. Вы с са­
мого начала книги учились использовать такие потоки ввода и вывода, как co u t и c in .
Теперь вы знаете, как создавать простые текстовые файлы и выполнять их запись и
чтение. Вы узнали, как класс s t r in g s t r e a m может помочь в преобразовании простых
типов, таких как i n t , в строки и обратно.

Вопросы и ответы
■ Если я могу использовать класс f stream и для записи, и для чтения из файла,
то зачем мне классы o f stream и i f stream ?
Если ваш код или модуль должен только читать из файла, то лучше использовать
класс i f stream . Точно так же, если вам нужна только запись в файл, используйте
класс o f stream . В обоих случаях можно было бы использовать и класс f stream , но
для обеспечения целостности данных лучше иметь ограничительную политику, по­
добную использованию c o n s t (которое также не является обязательным, но крайне
рекомендуется).
■ Когда мне использовать метод c i n . g e t (), а когда метод c i n . g e t l i n e () ?
Метод c i n . g e t l i n e () гарантирует чтение всей строки, включая введенные пользо­
вателем пробелы. Метод c i n . g e t () обеспечивает чтение пользовательского ввода
по одному символу.

■ Когда следует использовать класс s tr in g s tr e a m ?
Класс s t r in g s t r e a m обеспечивает удобный способ преобразования целых чисел и
других простых типов в строки и обратно (см. листинг 27.11).

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. В программе необходима только запись в файл. Какой поток следует использо­
вать в этом случае?
2. Как использовать объект c in для получения полной строки из входного потока?
3. Необходима запись объекта s t d : : s t r i n g в файл. Следует ли выбрать режим
открытия файла i o s _ b a s e : : b in a ry ?

Коллоквиум
4.

I

661

Вы открыли поток с помощью метода open (). Почему следует воспользоваться
методом is _ o p e n () ?

Упражнения
1. Отладка. Найдите ошибку в следующем коде:
fstream myFile;
myFile.open("HelloFile.txt", ios_base::out);
myFile « "Некоторый текст";
myFile.close();

2. Отладка. Найдите ошибку в следующем коде:
ifstream myFile("SomeFile.txt");
if(myFile.is_open())

(
myFile « "Некоторый текст" «
myFile.close();

}

endl;

ЗАНЯТИЕ 28

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

Что такое исключение

■ Как обрабатываются исключения
■ Как обработка исключений помогает создавать надеж­
ные приложения C + +

664

|

ЗАНЯТИЕ 28. Обработка исключений

Что такое исключение
Ваша программа запрашивает память, читает и записывает данные, сохраняет
файлы — все работает. В вашей великолепной среде разработки все выполняется
безупречно, и вы гордитесь тем фактом, что ваше приложение не пропускает ни бай­
та, хотя и управляет гигабайтами! Вы распространяете свое приложение, и клиент
устанавливает его на тысячи рабочих станций. Некоторым из его компьютеров по де­
сять лет. Жесткие диски на некоторых из них еле крутятся. Проходит совсем немного
времени, и в вашей папке входящих писем появляются первые жалобы. В одних из
них будет упоминание о нарушении прав доступа, а в других — о сообщении “Необ­
работанное исключение”.
Вот тебе и на: “необработанное” и “исключение”. В вашей системе приложение
работало отлично, так откуда все это взялось?
Все дело в том, что мир очень разнообразен. Не существует двух одинаковых ком­
пьютеров даже при одной и той же аппаратной конфигурации. Дело в том, что на
каждом компьютере выполняется разное программное обеспечение, а состояние, в
котором находится машина, влияет на объем ресурсов, доступных в определенный
момент времени. Поэтому вполне вероятно, что отлично работавший в ваших услови­
ях диспетчер памяти отказывает в выделении блока нужного размера в другой среде.
Такие отказы редки, но все же случаются. Эти отказы и приводят к исключениям
(exception).
Исключения прерывают нормальный поток выполнения вашего приложения.
В конце концов, если доступной памяти нет, нет никакой возможности заставить
ваше приложение делать то, что оно должно делать. Тем не менее ваше приложение
способно обработать исключение и отобразить пользователю сообщение об ошибке,
выполнить по мере необходимости операции по сохранению данных и максимально
корректно завершить работу.
Обработка исключений поможет избежать таких сообщений, как “Access Violation”
или “Unhandled Exception”, а также соответствующих жалоб по электронной почте.
Давайте рассмотрим, какие инструменты предоставляет язык C++, чтобы справиться
с непредвиденным.

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

Чтобы защитить свой код от исключений, вы

обрабатываете (handle)

лая свой код безопасным в отношении исключений (exception safe).

их, де­

Реализация безопасности в отношении исключений с помощью блоков try...

|

665

Реализация безопасности
в отношении исключений
с помощью блоков t r y и c a t c h
Когда дело доходит до реализации безопасности в отношении исключений, самы­
ми важными оказываются ключевые слова C++ t r y и c a tc h . Чтобы сделать инструк­
ции безопасными в отношении исключений, следует поместить их в блок t r y и в
блоке c a tc h обработать исключения, которые будут сгенерированы в блоке try :
void SomeFuncO
{
try

{
int* numPtr = new int;
*numPtr = 999;
delete numPtr;

}
catch(...) // ... Обрабатывает все исключения
{
cout «

"Исключение в SomeFuncO, завершение работы" «

endl;

Использование блока catch ( . . . )
для обработки всех исключений
Как вы помните, на занятии 8, “Указатели и ссылки”, я упоминал о том, что стан­
дартная форма оператора new возвращает допустимый указатель на область в памяти,
если память выделена успешно; в противном случае оператор new генерирует исклю­
чение. В листинге 28.1 показано, как выделение памяти с использованием оператора
new можно сделать безопасным по отношению к исключениям и как обрабатывать
ситуацию, когда компьютер не в состоянии выделить запрошенную память.
ЛИСТИНГ 28.1. Использование блоков t r y и c a tc h для обеспечения
безопасности в отношении исключений при выделении памяти
0 : #include
1 : using namespace std;

2:
3: int main()
4: {
5:
cout « "Количество чисел, для которых нужна память: ";
6:
try
7:
{
8
int input = 0 ;
9
cin » input;

666

I

ЗАНЯТИЕ 28. Обработка исключений

10 :
11:
12:
13:
14:
15:
16:
17:
18:
19:

// Запрос области в памяти и ее последующее освобождение
int* nuinArray = new int [input];
delete[] numArray;
}
catch (...)
{
cout «
}
return 0 ;

20:

"Извините, перехвачено исключение." «

endl;

}

Результат
Количество чисел, для которых нужна память: -1
Извините, перехвачено исключение.

Анализ
Для этого примера я указал в качестве размера запрашиваемого массива -1 . Это
абсурдное значение, но пользователи иногда делают такие вещи. В отсутствие об­
работчика исключений работа программы закончилась бы некрасиво. Но благодаря
обработчику исключения мы видим более осмысленное сообщение о перехваченном
исключении.
В листинге 28.1 демонстрируется применение блоков c a tc h и tr y . Блок c a tc h ()
получает параметры, как обычная функция, а
означает, что данный блок c a tc h
принимает исключения всех типов. Но в данном случае мы могли бы захотеть при­
нимать исключения только одного типа, s t d : : b a d _ a llo c , поскольку именно они ге­
нерируются при неудачном выполнении оператора new. Обработка исключений кон­
кретного типа позволяет раздельно обрабатывать различные проблемы, в том числе с
получением дополнительной информации о проблеме.

Обработка исключения конкретного типа
Исключение в листинге 28.1 генерировалось стандартной библиотекой C++.
Типы таких исключений известны заранее, и их обработка проще, поскольку вы точ­
но знаете причину исключения, можете лучше организовать восстановление или по
крайней мере отобразить для пользователя конкретное сообщ ение, как показано в
листинге 28.2.
Л И С ТИ Н Г28.2. Обработка исключения s t d : :bad a l l o c _____________________________
0 : #include

1: #include // Для обработки исключения bad_alloc
2 : using namespace std;

3:

Реализация безопасности в отношении исключений с помощью блоков try...
4: int main()
5: {
6:
cout «
7:
try

В:

667

"Количество чисел, для которых нужна память: ";

{

9:
10:

int input = 0 ;
cin » input;

11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26: }

// Запрос памяти и ее последующее освобождение
int* numArray = new int[input];
delete[] numArray;
}
catch(std::bad_alloc& exp)
{
cout « "Перехвачено исключение: " « exp.whatO «
cout « "Программа завершается." « endl;

endl;

}

catchf...)
{

cout «
}
return 0 ;

"Извините, перехвачено исключение." «

endl;

Результат
Количество чисел, для которых нужна память: -1
Перехвачено исключение: bad array new length
Программа завершается.

Анализ
Сравните вывод листинга 2 8 .2 с выводом листинга 2 8 .1 . Как видите, теперь вы в
состоянии указать причину внезапного окончания работы приложения точнее, а имен­
но — “bad array new length” (ошибка длины запрашиваемого массива). Это связано с
тем, что теперь в программе есть дополнительный блок c a t c h (да, в ней теперь два
блока c a t c h ) , который обрабатывает исключения конкретного типа — c a t c h ( b a d _ a ll o c & ), как показано в строках 1 6 -2 0 .

СОВЕТ

В общем случае вы можете вставить в программу столько блоков c a t c h (),
сколько вам нужно, располагая их один за другим в зависимости от типа
ожидаемых исключений.
Блок c a t c h ( . . . ) , показанный в листинге 28.2, обрабатывает исключе­
ния всех типов, которые не были обработаны явно другими блоками c a t c h
(и поэтому должен располагаться последним).

668

ЗАНЯТИЕ 28. Обработка исключений

Генерация исключения с помощью оператора th ro w
Когда вы обрабатывали исключение s t d : :b a d _ a llo c в листинге 28.2, речь шла об
объекте класса s t d : :b a d _ a llo c , который сгенерировал в качестве исключения опера­
тор new. Но вы вполне можете самостоятельно сгенерировать исключение требуемого
вам типа. Для этого необходимо воспользоваться ключевым словом throw:
void DoSomething()

{
if (что_то_не_так)
throw Значение ;

Давайте рассмотрим применение оператора throw на примере пользовательского
типа исключения, генерируемого при попытке деления на нуль, как показано в лис­
тинге 28.3.
ЛИСТИНГ 28.3. Генерация специального исключения при попытке деления на нуль
0
1

#include
using namespace std;

2

3

double Divide(double dividend, double divisor)

4

5

if(divisor == 0 )
throw "Делить на 0 нельзя";

6
7
8

9

return (dividend / divisor);

}

10
11

int main()

12

{

13
14
15
16
17
18
19

cout «
double
cin »
cout «
double
cin »

20

try

"Введите делимое: ";
dividend = 0 ;
dividend;
"Введите делитель: ";
divisor = 0 ;
divisor;

21
22
23
24
25
26
27
28
29
30
31

cout «
«

"Результат деления: "
Divide(dividend, divisor);

}
catch(char* exp)

{
cout «
cout «

}
return 0 ;

"Исключение: " « exp « endl;
"Программа завершена." « endl;

Как работает обработка исключений

| 669

Результат
Введите делимое: 2 0 1 1
Введите делитель: 0
Исключение: Делить на 0 нельзя
Программа завершена.

Анализ
Этот код не только демонстрирует возможность обработки исключения типа char*,
как показано в строке 24, но и то, что вы можете перехватывать исключение, сгене­
рированное в вызываемой функции D iv id e () в строке 6. Обратите также внимание
на то, что в блок t r y {} заключена не вся функция main (), а только та ее часть, где
ожидается генерация исключения. Это хорошая практика, поскольку обработка ис­
ключений может отрицательно влиять на производительность вашего кода.

Как работает обработка исключений
В функции D iv id e () листинга 28.3 генерируется исключение типа char*, которое
затем обрабатывается обработчиком c a tc h (c h a r * ) в вызывающей функции main ().
Когда исключение генерируется с помощью оператора throw, компилятор вставля­
ет в код динамический поиск соответствующего блока c a tc h , способного обработать
это исключение. Логика обработки исключений сначала проверяет, находится ли стро­
ка, сгенерировавшая исключение, в пределах блока tr y . Если это так, то начинается
поиск блока c a tc h , который способен обработать исключение этого типа. Если же
оператор throw находится вне блока t r y или если нет блока c a tc h , соответствующего
типу сгенерированного исключения, логика обработки исключения выполняет поиск
в вызывающей функции. Так логика обработки исключений движется вверх по стеку
вызовов, рассматривая одну вызывающую функцию за другой, пока не отыщет под­
ходящий блок c a tc h , способный обработать исключение данного типа. На каждом
этапе разворачивания стека локальные переменные текущей функции уничтожаются
в порядке, обратном порядку их создания (что продемонстрировано в листинге 28.4).
Л И С Т И Н Г 2 8 .4 , П о р я д о к у н и что ж е н и я л о к а л ь н ы х о б ъ е к т о в в сл у ч а е и скл ю ч е н и я _________

0 : #include
1 : using namespace std;
2:
3: struct StructA
4: {
5:
StructAO {cout « "StructA::StructA()" « endl; }
6:
-StructAO {cout « "StructA::-StructA()" « endl; }
7: };

8:
9: struct StructB

10:
11:

{

StructBO

{cout «

"StructB::StructB()" «

endl; }

670

ЗАНЯТИЕ 28. Обработка исключений

12:
-StructBO {cout « "StructB::-StructB()" «
13: };
14:
15: void FuncBO // Генерация исключения
16: {
17:
cout « ?,B FuncB():" « endl;
18:
StructA objA;
19:
StructВ objB;
20:
cout « "Генерируем исключение!" « endl;
21:
throw "Ловите меня!";

22 :
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:

endl; }

}

void FuncA()
{
try
{
cout « "B FuncAO" «
endl;
StructA objA;
StructB objB;
FuncB();
cout « "FuncA:возврат в вызывающую функцию." « endl;
}
catch(const char* exp)
{
cout « "FuncAO : Перехвачено исключение: " « exp«endl;
cout « "FuncAO: Обработано, далее не передается"«endl;
// throw; // Снимите комментарий для передачи в main()
}
}
int main()
{
cout « "main(): Началовыполнения" « endl;
try
{
FuncAO;
}
catch(const char* exp)
{
cout «
"Исключение: " « exp « endl;
}
cout « "main(): завершениеработы." «
endl;
return 0;
}

Результат
main(): Начало выполнения
В FuncAO
StructA::StructA()

Как работает обработка исключений

|

671

StructB: :StructB()
В FuncB():
StructA::StructA()
StructB::StructB()
Генерируем исключение!
StructB::-StructB()
StructA::-StructA()
StructB::-StructB()
StructA::-StructA()
FuncA(): Перехвачено исключение: Ловите меня!
FuncA(): Обработано, далее не передается
main(): завершение работы.

Анализ
В листинге 28.4 функция m a in () вызывает функцию F u n c A (), которая вызывает
функцию F u n c B (), генерирующую исключение в строке 21. Обе вызывающие функ­
ции, F u n c A ( ) и m a in ( ), являются безопасными в отношении исключений, поскольку
у обеих реализован блок c a t c h ( c o n s t c h a r * ). У функции F u n c B (), генерирующей
исключение, нет блоков c a t c h ( ) , а следовательно, первым обработчиком сгенери­
рованного исключения будет блок c a t c h в функции F u n c A () (строки 3 4 -3 9 ), по­
скольку функция F u n c A () является вызывающей для функции F u n c B (). Обратите
внимание: функция F u n c A () решает, что это исключение не имеет серьезного ха­
рактера, полностью обработано и его не нужно передавать дальше функции m a in ( ) .
Так что функция m a in () продолжит свою работу, как будто никакой проблемы нет.
Если же снять комментарий в строке 38, исключение будет передано вызывающей
F u n c A функции, так что функция m a in () также его получит.
Вывод программы демонстрирует также порядок создания объектов (это тот же по­
рядок, в котором они расположены в коде) и их уничтожения при генерации исключе­
ния (обратный порядку создания объектов). Уничтожение объектов происходит не толь­
ко в функции F u n c B (), где было сгенерировано исключение, но и в функции F u n c A (),
которая вызвала функцию F u n c B () и обработала сгенерированное в ней исключение.

ВНИМАНИЕ!

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

Класс s t d : : e x c e p t io n
При обработке исключения s t d : : b a d _ a llo c в листинге 28.2 вы фактически по­
лучали ссылку на объект класса исключения s t d : : b a d _ a llo c , сгенерированного
оператором new. Класс s t d : :b a d _ a llo c — производный от стандартного класса C++
s t d : : e x c e p tio n , объявленного в заголовочном файле < ex c e p tio n > .

|

672

ЗАНЯТИЕ 28. Обработка исключений

Класс s t d : : e x c e p tio n является базовым для следующих классов важных исклю­
чений.

■ b a d _ a llo c. Генерируется при неудачном выделении памяти оператором new.
■ b a d _ ca st. Генерируется оператором d ynam ic_cast при попытке приведения не­
правильного типа (без отношения наследования).

■ i o s j o a s e :: fa ilu r e . Генерируется функциями и методами библиотеки iostream .
Класс s t d : : e x c e p tio n является базовым классом, предоставляющим очень по­
лезный и важный виртуальный метод, what ( ) , возвращающий описательную инфор­
мацию причины проблемы, вызвавшей исключение. Функция exp.what () в строке 18
листинга 28.2 предоставляет строку "bad array new len gth ", сообщая о том, что вы­
деление памяти потерпело неудачу. Вы можете использовать класс std : e x c e p tio n ,
являющийся базовым классом для многих типов исключений, и создать один блок
ca tch (c o n st e x c e p t io n s ) , способный обрабатывать все исключения, для которых
класс s td : -.exception является базовым:
void SomeFuncO
{
try
// Код, безопасный в отношении исключений

}
catch(const std::exceptions exp) // Обработка bad_alloc,
// bad_cast и т.д.

{
cout «

"Перехвачено исключение: " «

exp.what() «

endl;

}
}

Пользовательский класс исключения,
производный от s t d : :e x c e p t io n
Вы можете сгенерироватьисключение любого типа, какого пожелаете. Однако на­
следование от класса std : e x c e p t i o n обладает тем преимуществом, что все сущ ес­
твующие обработчики для исключений c a tc h (c o n st s td : e x c e p t i o n s ) , которые
перехватывают такие исключения, как bad a llo c , bad c a s t и другие будут работать
и автоматически обрабатывать и ваши пользовательские исключения, поскольку они
имеют один и тот же базовый класс (листинг 28.5).

ЛИСТИНГ 28.5. Класс CustomException, происходящий от класса s t d : : ex c ep tio n
0:
1:
2:
3:
4:
5:

#include
#include
#include
using namespace std;
class CustomException: public std:exception

Как работает обработка исключений
6

:

{

7:
string reason;
8: public:
9:
// Конструктор с указанием причины
10:
CustomException(const char* why):reason(why)

{}

11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:

// Перекрытие виртуальной функции what()
virtual const char* what() const throw()
{
return reason.c_str();
}
};
double Divide(double dividend, double divisor)
{

if(divisor == 0)
throw CustomException("CustomException: деление на О");
return (dividend / divisor);
}
int main()
{
cout « "Введите делимое: ";
double dividend = 0;
cin » dividend;
cout « "Введите делитель: ”;
double divisor = 0;
cin »
divisor;
try
{
cout « "Результат деления: " « Divide(dividend, divisor);
}
catch(exceptions exp) // Обработка в том числе CustomException
{
cout « exp.whatO « endl;
cout « "Программа завершена" « endl;
}
return

0;

}

Результат
Введите делимое: 2 0 1 1
Введите делитель: О
CustomException: деление на 0
Программа завершена

673

674

|

ЗАНЯТИЕ 28. Обработка исключений

Анализ
Это версия листинга 28.3, в котором при делении на нуль генерировалось прос­
тое исключение типа char*. Здесь мы создаем экземпляр класса CustomException,
определенного в строках 5 -1 7 как производного от класса s t d : : ex cep tio n . Обратите
внимание на то, что наш класс исключения реализует виртуальную функцию what () в
строках 13-16, которая возвращает описание причины генерации исключения. Логика
обработчика ca tch (e x cep tio n s) в функции main () (строки 3 9 -4 3 ) обрабатывает ис­
ключения не только класса CustomException, но и других типов исключений (напри­
мер, bad a llo c ) , для которых базовым является класс ex cep tio n .

ПРИМЕЧАНИЕ

Обратите

внимание

E xcep tion : :what

на

объявление

виртуального

метода

Custom

() в строке 13 листинга 28.5:

virtual const char* what() const throw()
Оно заканчивается спецификатором

throw

() , который означает, что дан­

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

throw,

то

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

throw ( i n t ) , то это
in t.

значит, что данная функция может генерировать ис­

ключение типа

РЕКОМЕНДУЕТСЯ

НЕ РЕКОМЕНДУЕТСЯ

Помните о перехвате исключений типа s t d : :
ex cep tio n .
Помните о возможности наследования пользо­

Не генерируйте исключения в деструкторах.
Не считайте успешность выделения памяти

вательского класса исключения (как и любого
другого) от класса

Генерируйте

s td : e x c e p t io n .

исключения осмотрительно. Они

не являются заменой возврата из функций та­
ких значений-флагов, как

tr u e

или

f a ls e .

само собой разумеющейся; следует всегда за­

tr y код, использующий опера­
new, и создавать соответствующий обработ­
чик c a t c h ( s t d : :e x c e p tio n s ).
ключать в блок

тор

Не используйте

сложную логику или выделе­

ние ресурсов в блоке catch () . Нельзя также
генерировать исключения во время обработки
других исключений.

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

Вопросы и ответы

675

узнали, что код, выделяющий память или иные ресурсы, может столкнуться с про­
блемами, а следовательно, в нем должна присутствовать обработка исключений. Вы
узнали, что язык C++ предоставляет стандартный класс исключения s t d : : e x c e p tio n ,
который имеет смысл наследовать при необходимости создать собственный пользова­
тельский класс исключения.

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

■ Почему мой класс исключения должен наследовать класс s t d : :exception?
Это, конечно ж е, необязательно, но позволит вам воспользоваться блоками
c a t c h ( ) , которые обрабатывают исключения типа s t d : e x c e p t i o n . Вы мо­
жете написать собственны й класс исключения, который не является потом­
ком других классов, но тогда вам придется добавлять собственные обработчики
c a tc h (MyNewExceptionType&) во все соответствующие места программы.

■ У меня есть функция, которая генерирует исключение. Должно ли оно обраба­
тываться в той же функции?
Нет. Следует только убедиться, что исключение данного типа обработает одна из
вызывающих функций выше в стеке вызовов.

■ Может ли генерация исключения осуществляться из конструктора?
У конструкторов фактически нет иного выбора! У них нет возвращаемых значений,
и генерация исключения — наилучший способ оповещения о проблеме.

■ Может ли генерация исключения осуществляться в деструкторе?
Технически — да. Но это очень плохая идея, поскольку деструкторы вызываются
при развертывании стека при обработке исключения. Если деструктор, вызванный
в связи с обработкой исключения, сам сгенерирует исключение, может возникнуть
весьма неприятная ситуация.

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

676

|

ЗАНЯТИЕ 28. Обработка исключений

Контрольные вопросы
1. Что такое s t d : : e x c e p tio n ?
2. Какого типа исключение генерируется при неудаче выделения памяти опера­
тором new?
3. Хороша ли идея выделить в обработчике исключения (блок ca tch ) место под, ска­
жем, миллион целых чисел для резервного копирования существующих данных?
4. Как бы вы обработали объект исключения класса M yException, производного
от класса s t d : : e x c e p tio n ?

Упражнения
1.

Отладка. Что не так в следующем коде?
class SomelntelligentStuff
{
bool StuffGoneBad;
public:
- SomelntelligentStuff()

{
if(StuffGoneBad)
throw "Проблема в данном классе";

};
2.

О тладка. Что не так в следующем коде?
int main()

{
int* pMillionlntegers = new int[1000000];
// Сделать нечто с миллионом целых чисел
delete[]pMillionlntegers;

3.

Отладка. Что не так в следующем коде?
int main()

{
try

{
int* pMillionlntegers = new int[1000000];
// Сделать нечто c миллионом целых чисел
delete[]pMillionlntegers;

}
catch(exceptions exp)

{
int* pAnotherMillionlntegers = new int[1000000];
// Создать резервную копию pMillionlntegers
/ / и сохранить ее на диске

}

ЗАНЯТИЕ 29

Что дальше
Вы изучили основы программирования на языке C + +.
Фактически мы вышли за теоретические границы понима­
ния, изучив стандартную библиотеку шаблонов (STL), шабло­
ны и то, как STL способна помочь вам писать эффективный
и компактный код. Теперь пришло время обратить внимание
на производительность и получить несколько полезных со­
ветов по программированию.
На этом занятии...
и

Как приложение C + + может лучше использовать воз­
можности процессора

■ Потоки и многопоточность
■ Полезные практические советы по программированию
на C++
■ Новые возможности, ожидаемые в С ++17
■ Как повысить свою квалификацию программиста на C+ +

678

|

ЗАНЯТИЕ 29. Что дальше

Чем различаются современные
процессоры
За последнее время процессоры компьютеров стали быстрее, их скорости обработ­
ки измеряются уже не в килогерцах (кГц) и мегагерцах (МГц), а в гигагерцах (ГГц).
Например, процессор Intel 8086 (рис. 29.1), выпущенный в 1978 году, был 16-разрядным и работал с тактовой частотой примерно 10 МГц.

РИС- 29-1. Микропроцессор Intel 8086
Процессоры в наши дни стали значительно быстрее, и то же самое можно сказать
о приложениях C++. Проще всего было бы положиться на постоянно улучшающиеся
аппаратные средства и рост производительности программного обеспечения за счет
повышения их быстродействия. Но хотя современные процессоры все еще становятся
быстрее, истинное новаторство кроется в количестве ядер, которыми они обладают.
На момент написания этой книги даже массовые смартфоны уже оснащены 64-разрядными микропроцессорами с четырьмя ядрами и большими вычислительными воз­
можностями, чем настольные компьютеры десятилетие назад.
Многоядерный процессор можно рассматривать как одну микросхему с нескольки­
ми процессорами, работающими параллельно. Каждый процессор имеет собственный
кеш L1 и способен работать независимо от других.
Чем быстрее процессор, тем выше скорость выполнения вашего приложения, что
вполне логично. Но чем поможет наличие нескольких ядер процессора? Вполне оче­
видно, что каждое ядро способно выполнять приложения параллельно, но это необя­
зательно делает ваше приложение быстрее, если вы не программируете его так, что­
бы оно могло воспользоваться новыми возможностями. Однопоточные приложения
C++, которые мы рассматривали до сих пор, вероятно, не извлекут никакой выгоды из
работы в многоядерной системе. Такие приложения выполняются в одном потоке, а
следовательно, используют только одно ядро, как показано на рис. 29.2.
Если ваше приложение выполняет все действия последовательно, то операционная
система, скорее всего, предоставит ему столько же времени, сколько и другим при­
ложениям, и оно будет использовать только одно ядро процессора. Другими словами,
ваше приложение будет выполняться на многоядерном процессоре точно так же, как
и многие годы назад (разве что благодаря наличию большого количества ядер оно не
будет постоянно прерываться, уступая свой процессор другим приложениям и опера­
ционной системе).

Как лучше использовать несколько ядер

Однопоточное приложение
(использует одно ядро)

679

Мощное приложение обработки изображений
(использует несколько ядер)

РИС- 29-2. Однопоточное приложение на многоядерном процессоре

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

Что такое поток
Код приложения всегда работает в потоке выполнения. Поток (thread) — это син­
хронно выполняемый объект, операторы которого выполняются один за другим. Код
функции main () рассматривается как выполняемый основным потоком приложения.
В этом основном потоке можно создавать новые потоки, способны е выполняться
параллельно. Такие приложения, состоящие из одного или нескольких потоков, вы­
полняющихся параллельно основному, называются многопоточными приложениями
(multithreaded application).
Как именно должны создаваться потоки, определяется операционной системой. Вы
можете создавать их непосредственно, с помощью вызова соответствующих функций
API, предоставляемых операционной системой.

680

ЗАНЯТИЕ 29. Что дальше

СОВЕТ

Язык C++ начиная со стандарта C++И определяет функции для создания
многопоточных приложений, которые сами заботятся о вызове функций API
операционной системы. Это делает ваше многопоточное приложение на­
много более переносимым.
Если вы планируете разработку приложения только для одной операцион­
ной системы, можете ограничиться использованием API это операционной
системы.

ПРИМЕЧАНИЕ

Фактические действия по созданию потока специфичны для конкретной
операционной системы. Язык C++И попытается снабдить вас независи­

s t d : : thread,
. Однако если вы пишете

мой от конкретной платформы абстракцией в виде класса
описанного в заголовочном файле

программу для одной конкретной платформы, то можете использовать по­
токовые функции, специфичные для данной операционной системы.
При написании переносимого многопоточного приложения C++ можно по­
думать о применении Boost Thread Libraries (см.

w w w .b o o st.o rg ).

Зач ем со зд а в а т ь многопоточные приложения
Многопоточность используется в приложениях, которые должны осуществлять мно­
жество действий параллельно. Предположим, что вы — один из 10000 пользователей,
пытающихся осуществить покупку HaAmazon.com. Конечно, веб-сервер Amazon.com не
может заставить 9 999 пользователей ожидать, пока один пользователь завершит свою
покупку. Веб-сервер создает множество потоков, обслуживая всех пользователей одно­
временно. Если веб-сервер работает на многоядерном процессоре (готов спорить, что
это так и есть), то такие потоки в состоянии извлечь преимущество из наличия доступ­
ных ядер и обеспечить оптимальную производительность для каждого пользователя.
Еще одним общеизвестным примером многопоточности является, например, при­
ложение с графическим интерфейсом пользователя, которое взаимодействует с поль­
зователем и в то же время выполняет некую полезную работу. Такие приложения
обычно имеют поток пользовательского интерфейса (User Interface Thread), который
отображает пользовательский интерфейс, изменяет его вид по мере надобности и при­
нимает пользовательский ввод, а также рабочий поток (Worker Thread), который в фо­
новом режиме выполняет основную работу. К таким приложениям относятся, напри­
мер, инструменты дефрагментации диска. После запуска такого приложения создается
рабочий поток, начинающий просмотр и дефрагментацию диска. Одновременно по­
ток пользовательского интерфейса отображает прогресс процесса дефрагментации,
предоставляя при этом пользователю возможность отменить дефрагментацию. Чтобы
поток пользовательского интерфейса мог отображать на экране прогресс процесса
дефрагментации, рабочий поток, занимающийся ею, должен регулярно передавать
сообщения потоку пользовательского интерфейса. Точно так же, чтобы рабочий по­
ток узнал о необходимости прекратить работу, поток пользовательского интерфейса
должен сообщить ему об этом.

Как лучше использовать несколько ядер

ПРИМЕЧАНИЕ

681

Чтобы приложение могло функционировать как единое целое, а не коллек­
ция бесконтрольных потоков, выполняющих свою работу независимо один
от другого, многопоточные приложения нуждаются в средстве “общения”
потоков между собой.
Последовательность также важна. Вы ведь не хотите, чтобы поток пользова­
тельского интерфейса закончил работу раньше, чем рабочий поток закон­
чит дефрагментацию? Нередки ситуации, когда один поток должен ожидать
другой. Например, поток чтения из базы данных должен ожидать, пока за ­
вершит текущую операцию поток записи в базу данных.
Действия по организации ожидания потоками один другого называются
синхронизацией потоков (thread synchronization).

Как потоки осущ ествляю т транзакцию данны х
Потоки способны совместно использовать переменные. У потока может быть до­
ступ к глобальным данным. Потоки могут быть созданы с указателем на совместно
используемый объект (структуры или класса) с данными, как показано на рис. 29.3.
Поток пользовательского
интерфейса

Рабочий поток
1
f

!
I

9
1

Совместно
используемый
объект

1
1
1
1
1

запись w

>

float Progress


1
1
1
1

4T6HH6W !

1

Деф рагментация
диска
|

i
! ^ чтение
1
1

Ч

1
1
1
1
1
1
|

ч

1

bool Cancel

^ за п и с ь

i
!

Отображение
f прогресса

i
i
i
i
i
i
i
i

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

682

|

ЗАНЯТИЕ 29. Что дальше

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

И спользование мью тексов и сем а ф о р о в
для синхронизации потоков
Потоки — сущности уровня операционной системы, и объекты, используемые
для их синхронизации, также предоставляются операционной системой. Большин­
ство операционных систем предоставляет для синхронизации потоков семафоры
(semaphore) и мьютексы (mutex).
Мьютекс (объект синхронизации путем взаимного исключения (mutual exclusion))
гарантирует доступ к части кода только одного потока. Другими словами, мьютекс ис­
пользуется для организации раздела кода, перед выполнением которого поток должен
подождать, пока другой поток закончит его выполнение и освободит мьютекс. Когда
следующий поток захватывает мьютекс, он может выполнять этот фрагмент кода; все
прочие потоки будут вынуждены перейти в состояние ожидания, пока он освободит
мьютекс. Начиная с C++11 стандартная библиотека предоставляет мьютексы в виде
класса s t d : : mutex в заголовочном файле .
Используя семафоры, можно контролировать количество потоков, которые выпол­
няют некоторый раздел кода. Семафор, разрешающий доступ только одному потоку,
называется бинарным семафором (binary semaphore).

Проблемы, вы зы ваем ы е многопоточностью
Многопоточность с ее необходимостью в хорошей синхронизации потоков способ­
на стать причиной множества бессонных ночей, когда эффективность синхронизации
оказывается неэффективной (читай: с ошибками). Вот две наиболее распространен­
ные проблемы, с которыми сталкиваются многопоточные приложения.
■ Состояние гонки (race condition). Два или более потоков пытаются записывать одни
и те же данные. Кто победит? Каким окажется состояние этого объекта?
■ Взаимоблокировка (deadlock). Два потока ожидают завершения один другого, и оба
находятся в состоянии ожидания. В результате приложение “зависает”.
При хорошей синхронизации состояния гонки можно избежать. Обычно, когда по­
токам позволено писать в совместно используемый объект, необходимо дополнитель­
но позаботиться о следующем:
■ одновременно записывать данные может только один поток;
■ никакому потоку не позволено читать, пока не завершится текущая запись объекта.
Избежать взаимоблокировки можно, устраняя ситуации, когда два потока вынуж­
дены ожидать один другого. У вас, например, может быть главный поток, синхрони­
зирующий рабочие потоки. Поток А может ожидать поток В, но поток В никогда не
должен при этом ожидать поток А.

Как писать отличный код C++

|

683

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

Как писать отличный код C++
Язык C++ значительно развился со времени первого выпуска благодаря усилиям
по его развитию и стандартизации, предпринятые главными изготовителями компи­
ляторов. В C++ к услугам программиста — множество утилит и функций, которые
помогают писать компактный и понятный код на C++. Писать понятные и надежные
приложения на C++ в действительности просто.
Ниже приведены полезные советы, которые помогут вам в создании хороших при­
ложений C++.
■ Присваивайте переменным осмысленные имена (понятные не только вам).
■ Всегда инициализируйте такие переменные, как i n t , f l o a t и подобные им.
■ Всегда инициализируйте указатели либо значением n u l l p t r , либо допустимым
адресом (например, возвращенным оператором new).
■ При использовании массивов никогда не пересекайте их границы. Это вызывает
переполнение буфера и может использоваться как брешь в системе безопасности.
■ Не используйте строковые буфера char* в стиле С и такие функции, как s t r l e n ()
и s t r c p y (). Тип s t d : : s t r i n g более безопасен и предоставляет много полезных
вспомогательных методов, включая позволяющие находить длину, выполнять ко­
пирование и конкатенацию.
■ Используйте статические массивы только тогда, когда абсолютно уверены в коли­
честве элементов, которые они будут содержать. Если вы не уверены в этом значе­
нии, используйте динамический массив, такой как s t d : : v e c to r .
■ При объявлении и определении функций, получающих параметры типов, отличных
от POD (простых старых данных), старайтесь избегать ненужного этапа копирова­
ния, передавая их при вызове функции по ссылке.
■ Если ваш класс содержит член (или члены) в виде простого указателя, обдумайте
владение ресурсом и управление им в случае копирования и присваивания. Рассмот­
рите возможность создания копирующего конструктора и копирующего оператора
присваивания.
■ При написании вспомогательного класса, управляющего динамическим массивом
или чем-то подобным, для повышения производительности не забудьте добавить
перемещающий конструктор и перемещающий оператор присваивания.
■ Не забывайте об использовании констант. В идеале функция g e t () не должна из­
менять члены класса, а следовательно, должна быть объявлена как константная.
Точно так же параметры функций должны быть константными ссылками, если вы
не планируете изменять значения, которые они содержат.

684

ЗАНЯТИЕ 29. Что дальше

■ Избегайте использования простых указателей. Используйте, где только можно, под­
ходящие интеллектуальные указатели.
■ При создании вспомогательного класса не жалейте усилий для поддержки всех
операторов, которые сделают использование вашего класса более простым.
■ По возможности отдавайте предпочтение шаблонам, а не макросам. Шаблоны без­
опасны с точки зрения типов.
■ При разработке класса, объекты которого будут храниться в контейнере, таком как
вектор или список, или использоваться как ключ в отображении, не забывайте пре­
доставлять оператор o p e r a t o r s определяющий заданный по умолчанию критерий
сортировки.
■ Если ваша лямбда-функция становится слишком большой, возможно, имеет смысл
создать функциональный объект с оператором o p e r a to r (), поскольку такой фун­
ктор обеспечивает повторное применение, а также единую точку поддержки.
■ Никогда не считайте безоговорочно, что оператор new завершается успешно. Код
выделения ресурса всегда может сгенерировать исключение, поэтому заключайте
его в блок t r y с соответствующим блоком c a tc h ().
■ Никогда не используйте оператор throw в деструкторе класса.
Это отнюдь не исчерпывающий список, но он охватывает ряд наиболее важных
вопросов и, несомненно, поможет вам в написании качественных и легко сопровож­
даемых программ на C++.

С++17: что новенького
Одним из больших преимуществ C++ является то, что комитет по стандартизации
активно и постоянно совершенствует язык. Ожидается, что, как и его предшествен­
ник С++11, стандарт С++17 внесет в язык множество новых возможностей. Давайте
рассмотрим некоторые особенности, которые, скорее всего, войдут в стандарт C++17
после его официальной ратификации.

ПРИМЕЧАНИЕ

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

Инициализация в i f и s w

it c h

Это небольшое, но важное усовершенствование синтаксиса i f и s w itc h , которое
можно выразить следующим образом:

C++17: что новенького

685

if (И н и ц и а л и за т ор ; у с л о в и е )

{
// Инструкции, выполняемые при истинности у с л о в и я

)
Или
swi t ch (И н и ц и а л и за т ор ; у с л о в и е )

{
// Блоки case

}
Переменная, объявленная в инструкции Инициализатора, уничтожается при вы­
ходе из инструкции i f . Рассмотрим следующий код из листинга 20.3:
auto pairFound = mapIntToStr.find(key);
if (pairFound != mapIntToStr.end())

{
cout «

"Ключ " «

cout «

pairFound->second «

pairFound->first «

" указывает на: ";

endl;

}
Теперь этот фрагмент может быть записан как
if (auto pairFound = mapIntToStr.find(key);
pairFound != mapIntToStr.end())

{
cout «

"Ключ " «

cout «

pairFound->second «

pairFound->first «

" указывает на: ";

endl;

}
Это не просто уменьшение кода (которого в данном фрагменте, по сути, нет). Это
изменение гарантирует, что переменная pairF ound, которая необходима только в бло­
ке i f , недоступна вне его. Ее область видимости ограничена до необходимого мини­
мума. Кроме того, при копировании и вставке этого улучшенного блока вы перенесете
всю необходимую логику в полном объеме.

Гарантия устранения копирования
При инициализации переменной возвращаемым значением функции может слу­
читься так, что компилятор создаст временную копию целого числа, возвращаемого
функцией R e tu r n ln t (), перед тем как инициализировать им переменную num:
int num = Returnlnt();

C++17 требует от компилятора полностью обойтись без создания такой временной
копии, т.е. устранить копирование.

686

| ЗАНЯТИЕ 29. Что дальше

Устранение накладных расходов выделения
памяти С ПОМОЩЬЮ s t d : : s t r in g _ v ie w
Рассмотрим функцию, принимающую в качестве параметра s t d : : s t r in g :
void Displaystring(const std::strings strln)

{
cout «

strln «

endl;

}
При ее вызове с передачей строкового литерала последний сначала преобразует­
ся во временный объект s t d : : s t r i n g , который и используется функцией D is p la y
S tr in g !):
Displaystring("Hello World!");

Этого преобразования можно избежать, используя вместо строки s t d : : s t r i n g _
view:
void Displaystring(std::string_view& strln)

{
cout «

strln «

endl;

}
Передача функции строкового литерала не повлечет за собой никаких накладных
расходов, связанных с распределением памяти, если функция принимает в качестве
аргумента s t d : : s tr in g _ v ie w .

s t d : :v a r i a n t

как безопасная с точки зрения
типов альтернатива объединению
Объединения рассматривались на занятии 9, “Классы и объекты”. Одна из про­
блем, связанных с объединениями, заключается в том, что оно позволяет рассматри­
вать содержимое одного типа как имеющ ее другой тип данных, поддерживаемый
объединением, например
union SimpleUnion
{
int num;
double preciseNum;

};
Вы можете инстанцировать это объединение значением d o u b le, а затем использо­
вать его как in t :
SimpleUnion ul;
ul.preciseNum =3.14; // Сохранение значения типа double
int num2 = ul.num;
// Работает; но ul содержит double!

C++17 предоставляет программисту тип s t d : : v a r ia n t — безопасную с точки зре­
ния типов альтернативу для union:

C++17: что новенького

687

variantcint, double> varSafe;
varSafe =3.14;
// Сохраняем double
double pi = get(varSafe); // 3.14
double pi2 = get(varsafe);
// 3.14
get(varSafe); // Ошибка компиляции: типа char нет
get(varSafe);
// Ошибка компиляции: есть только два типа, а не три
try
get(varSafe);

// Генерация исключения при сохранении double

}
catch(bad_variant_access&)

{ /* Обработчик исключения */ }

Условная компиляция
с использованием i f c o n s t e x p r
Эта конструкция аналогична конструкции i f - e l s e с тем отличием, что условие
вычисляется во время компиляции и код в блоке i f (или e l s e ) компилируется только
тогда, когда условие выполняется во время компиляции.
tinclude
#include
#include
using namespace std;
template ctypename T>
void DisplayData(const T& data)

{
if constexpr(is_integral::value)
cout « "Целочисленные данные: " « data « endl;
else if constexpr(is_floating_point::value)
cout « setprecision(15) « "Данные с плавающей точкой: "
« data « endl;
else
cout « "Неопределенные данные: " « data « endl;

}
В случае вызова D isp lay D a ta (15) компилятор, совместимый с С++17, будет ком­
пилировать только следующ ую строку:
cout «

"Целочисленные данные: " «

data «

endl;

В случае вызова D isp lay D a ta ("H e llo W orld! " ), поскольку тип c o n s t char* ве­
дет к выполнению блока e l s e , будет скомпилирована следующая строка:
cout «

"Неопределенные данные: " «

data «

endl;

В сочетании с автоматическим выводом возвращаемого типа, описанного на за­
нятии 7, “Организация кода с помощью функций”, эта мощная возможность потенци­
ально позволяет функции возвращать значения разных типов в зависимости от пути
выполнения.

688

|

ЗАНЯТИЕ 29. Что дальше

Усовершенствованные лямбда-выражения
Ожидаются следующие усовершенствования лямбда-функций.
■ Они должны поддерживаться внутри c o n s te x p r -функций.
■ Они должны иметь возможность захвата * t h i s с помощью синтаксиса [ * t h i s ].

Автоматический вывод типа для конструкторов
В С++14 для сочетания целочисленного значения и значения с плавающей точкой
необходимо явно объявить тип пары:
std::pair pairlntToDb(3, 3.14159265359);

C++17 позволяет упростить эту строку до
std::pair pairlntToDb(3, 3.14159265359);

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

te m p la te < a u to >
Это расширение менее используемой возможности, заключающейся в том, что
аргумент шаблона может содержать значение, которое используется во время ком­
пиляции. Например, s t d : : a r r a y — это контейнер, который моделирует массивы с
фиксированными размерами, известными во время компиляции. Для моделирования
массива из 10 целых чисел требуется следующий код:
std::array myTenNums;

Объявление шаблона класса наподобие s t d : : a r r a y имеет следующий вид:
template struct array;

C++17 позволяет упростить тип параметра шаблона до a u to , так что следующее
объявление будет вполне корректным:
template cclass Т, auto N> struct array;

Изучение C++ на этом не заканчивается
Поздравляю, вы достигли больших успехов в изучении языка C++! Наилучший
способ постоянно повышать квалификацию — это программировать и еще раз про­
граммировать! Язык C++ довольно сложен. И чем больше вы программируете, тем
выше ваш уровень понимания того, как он внутренне работает.

Документация в вебе
Если необходимо узнать больше о сигнатурах контейнеров STL, их методах или
алгоритмах, а также иные подробности, обратитесь к вебу. Одним из наиболее попу­
лярных ресурсов является сайт h t t p : //w w w .c p p r e fe r e n c e . с о т /.

Резюме

|

689

Сетевые сообщества и помощь
У языка C++ богатые и яркие сетевые сообщества. Зарегистрируйтесь на таких
сайтах, как StackOverflow (www. S t a c k O v e r f l o w . com), CodeGuru (www. C o d e G u r u . com)
и CodeProject (w w w . C o d e P r o je c t . c o m ), чтобы задавать технические вопросы и полу­
чать ответы от сообщества. (Если у вас проблемы с английским языком, вам будут
рады на сайте StackOverflow на русском языке, h t t p s : / / r u . s t a c k o v e r f l o w . c o m / . —

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

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

Вопросы и ответы
■ Я вполне доволен производительностью моего приложения. Должен ли я все
же реализовать многопоточность?
Нет. Не все приложения должны быть многопоточными. Многопоточность нужна
только при необходимости выполнения нескольких задач одновременно или для
обслуживания большого количества пользователей.

■ Почему бы мне не использовать старый стиль программирования и не озада­
чиваться всеми этими С++11 и С++14?
Эти стандарты существенно упрощают программирование. Такие ключевые слова,
как au to, избавляют от долгих и утомительных объявлений итераторов, а лямбдафункции делают конструкции fo r each () компактнее и без объектов функций. По­
этому преимущества использования современных стандартов в программировании
на C++ позволяют создавать более короткие, более простые и более эффективные
программы, чем старые версии стандарта.

690

|

ЗАНЯТИЕ 29. Что дальше

Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и закрепления получен­
ных знаний, а также упражнения, которые помогут применить на практике получен­
ные навыки. Попытайтесь самостоятельно ответить на эти вопросы и выполнить за­
дания, а потом сверьте полученные результаты с ответами в приложении Д, “Ответы”.
Если остались неясными хотя бы некоторые из предложенных ниже вопросов, не при­
ступайте к изучению материала следующего занятия.

Контрольные вопросы
1. М ое приложение обработки изображений перестает отвечать в процессе ис­
правления контраста. Что мне делать?
2. М ое многопоточное приложение обеспечивает чрезвычайно быстрый доступ
к базе данных. Но иногда я замечаю, что выбранные данные неверны. Что я
делаю неправильно?

Часть VI

Приложения
В ЭТОЙ ЧАСТИ...
П Р И Л О Ж ЕН И Е А. Двоичные и шестнадцатеричные числа
П Р И Л О Ж Е Н И Е Б. Ключевые слова языка C + +
П Р И Л О Ж ЕН И Е В. Приоритет операторов
П Р И Л О Ж ЕН И Е Г. Коды ASCII
П Р И Л О Ж ЕН И Е Д . Ответы

ПРИЛОЖЕНИЕ A

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

694

ПРИЛОЖЕНИЕ А. Двоичные и шестнадцатеричные числа

Десятичная система счисления
Цифры, которые мы используем ежедневно, находятся в диапазоне 0 -9 . Этот набор
цифр называется десятичной системой счисления. Поскольку система состоит из 10
отдельных цифр, она называется также системой с основанием 10.
Следовательно, поскольку основанием является 10, отсчитываемая от нуля пози­
ция каждой цифры означает степень числа 10, умноженную на цифру.
957 = 9 х Ю2 + 5 х ЮЧ-7Х 10°= 9х 1 0 0 + 5 x 1 0 + 7
В числе 957 отсчитываемая от нуля позиция цифры 7 — 0, цифры 5 — 1, а цифры
9 — 2. Эта позиция индексирует степени основания 10, как показано в примере. Пом­
ните, что лю бое число в степени 0 дает 1 (таким образом, 10° — то же самое, что и
1000°, поскольку оба значения равны 1).

ПРИМЕЧАНИЕ

В десятичной системе счисления самыми важными являются степени чис­
ла 10. Цифры в числе умножаются на 10, 100, 1000 и тан далее, чтобы
определить размерность числа.

Двоичная система счисления
Система с основанием 2 называется двоичной системой счисления. Поскольку си­
стема допускает только два состояния, она представляется числами 0 и 1. В C++ эти
числа обычно рассматриваются как f a l s e и tr u e (tr u e — не нуль).
Подобно тому как числа в десятичной системе счисления являются степенями
основания 10, в двоичной системе они являются степенями основания 2:
101 (двоичное) = 1 х 22+ 0 х 2 ] + 1 х 2 ° = 4 + 0 + 1 = 5 (десятичное).
Так, десятичным эквивалентом двоичного числа 101 будет 5.

ПРИМЕЧАНИЕ

Цифры в двоичном числе являются степенями числа 2, такими как 4 , 8 , 1 6 ,
32 и так далее, в зависимости от их разряда. Степень является отсчитывае­
мым от нуля местом, которое занимает рассматриваемая цифра.

Чтобы понять систему двоичных цифр, рассмотрим табл. АЛ, в которой приведе*
ны степени числа два.

ТАБЛИЦА АЛ. Степени числа 2
Степень

Значение

Двоичное представление

0
1

2° = 1

1
10

2' =2

Двоичная система счисления

|

695

Окончание табл. А. 1
Степень

Значение

Д воичное представление

2

22 = 4

100

3

23 = 8

1000

4

24= 16

10000

5

25 = 32

100000

6

26 = 64

1000000

7

27= 128

10000000

Почему компьютеры используют двоичные числа
Широкое распространение двоичная система счисления получила относительно
недавно (по сравнению со временем использования систем счисления вообще), после
появления электроники и компьютеров. Развитие электроники и электронных компо­
нентов привело к появлению систем, которые различали состояния компонентов как
включенное (при наличии разницы потенциалов или напряжения) или как выключен­
ное (при отсутствии разницы потенциалов или напряжения).
Эти состояния ВКЛ и ВЫКЛ очень удобно интерпретировать как 1 и 0, а также
полностью представлять ими набор двоичных чисел и выполнять арифметические
вычисления. Такие логические операции, как НЕ, И, ИЛИ и ИСКЛЮЧАЮЩЕЕ
ИЛИ, рассматривавшиеся на занятии 5, “Выражения, инструкции и операторы”,
(см. табл. 5 .2 -5 .5 ), легко реализуются электронными средствами, в результате чего
двоичная система счисления стала простой и популярной в электронике.

Что такое биты и байты
Бит — основная единица в вычислительной системе, которая содержит двоичное
состояние. Таким образом, о бите говорят, что он “установлен”, если он содержит со­
стояние 1, или “сброшен”, если содержит состояние 0. Коллекция битов — это байт.
Количество битов в байте теоретически не определено и зависит от используемых
аппаратных средств.
Однако большинство вычислительных систем предполагает, что в байте находится
8 битов, по той простой причине, что 8 является степенью 2. Кроме того, восемь би­
тов в байте позволяют передать до 28 (256) различных значений. Этих 256 отдельных
значений более чем достаточно для представления всех символов в наборе символов
ASCII.

Сколько байтов в килобайте
1 килобайт — это 1024 байта (2 10 байтов). Точно так же 1024 килобайта составляют
1 мегабайт, а 1024 мегабайта— 1 гигабайт. 1024 гигабайта составляют 1 терабайт.

696

|

ПРИЛОЖ ЕНИЕ А. Двоичные и шестнадцатеричные числа

Шестнадцатеричная система счисления
Шестнадцатеричная система счисления имеет основание 16. Цифра в шестнадца­
теричной системе может находиться в диапазоне 0 -9 и A -F. Так, десятичное 10 — это
шестнадцатеричное А, а десятичное 15 — шестнадцатеричное F.

Десятичное
число

Шестнадцатеричное Десятичное число
(продолжение)

Шестнадцатеричное
(продолжение)

0

0

8

8

1

1

9

9

2

2

10

А

3

3

11

В

4

4

12

С

5

5

13

D

6

6

14

Е

7

7

15

F

Подобно тому как числа в десятичной системе счисления являются степеням осно­
вания 10, в двоичной системе — степенями основания 2, в шестнадцатеричной они
являются степенями основания 16:
0 x 3 1F = 3 х 162+ 1 х 161+F х 16°=3 х 2 5 6 + 1 6 + 1 5 (десятичное) = 799.

ПРИМЕЧАНИЕ

По соглашению в C++ шестнадцатеричные числа представляют с префик­
сом “Ох”.

Зачем нужна шестнадцатеричная система
Компьютеры работают с двоичными числами. Состояние каждого блока памяти
в компьютере — 0 или 1. Однако, поскольку мы, люди, должны взаимодействовать с
компьютером и специфической для программ информацией в виде нулей и единиц,
мы нуждаемся в более компактном представлении небольших частей информации.
Так, вместо того чтобы писать 1111 в двоичном виде, нам намного проще написать F
в шестнадцатеричном.
Так, шестнадцатеричное представление может очень эффективно отобразить со­
стояние 4 битов, а используя максимум две шестнадцатеричные цифры, можно пред­
ставить состояние байта.

ПРИМЕЧАНИЕ

Менее популярна восьмеричная система счисления. Это система с основа­
нием 8, включающая цифры от 0 до 7.

Преобразование в различные системы счисления

697

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

Обобщенный процесс преобразования
При преобразовании числа из одной системы в другую вы последовательно делите
преобразуемое число на основание целевой системы счисления.Полученный остаток
заполняет разряды в целевой системе счисления, начиная с самого младшего разряда.
Следующее деление использует частное предыдущей операции.
Так продолжается до тех пор, пока остаток в пределах целевой системы счисления
и частное не достигнут 0.
Этот процесс также называется методом разбиения (breakdown method).

Преобразование десятичного числа в двоичное
Чтобы преобразовать десятичные 33 в двоичное, вычитайте из него самую высокую
из возможных степеней числа 2 (32)
Зна ком е сто 1:
Зна ком е сто 2:
Знаком есто 3:
Зна ком е сто 4:
Зна ком е сто 5:
Зн а ком е сто 6:

35/2
17/2

= ча стн ое 17, о стато к 1
= частн ое 8, о стато к 1

8 /2

= ча стн ое 4, остато к 0

4 /2

= ча стн ое 2, остато к 0

2 /2
1 /2

= ча стн ое 1, остато к О
= ча стн ое 0, остато к 1

Д во и ч н ы й экв и ва л е н т числа 33 (по знаком естам): 100011

Двоичный эквивалент числа 156
Зна ком е сто 1:

156/2

= частн ое 78, остато к 0

Знаком есто 2:

78/2
39/2

= ча стн ое 39, о стато к 0

19/2

= ча стн ое 19, остато к 1
= ча стн ое 9, о стато к 1

Зна ком е сто 5:

9 /2

= ча стн ое 4, остато к 1

Зна ком е сто 6:

4 /2

Зна ком е сто 7:

2 /2

= ча стн ое 2, остато к 0
= частн ое 1, остаток 0

Знаком есто 9:

1 /0

= частноеО, остато к 1

Знаком есто 3:
Зна ком е сто 4:

Д во и ч н ы й экв и ва л е н т числа 156:1 00 1 1 1 0 0

698

ПРИЛОЖ ЕНИЕ А. Двоичные и шестнадцатеричные числа

Преобразование десятичного числа
в шестнадцатеричное
Процесс тот же, что и при преобразовании в двоичное число, но деление осущест­
вляется на основание 16, а не 2.
Преобразование десятичного числа 5211 в шестнадцатеричное
З н а к о м е сто 1:

5211/16

З н а к о м е сто 2:

325 / 1 6 = ча стн ое 20, остато к 5

З н а к о м е сто 3:
Зн а к о м е сто 4:

20/16
1/16

= ча стн ое 325, о стато к В 16 (1110 — это В16)

= ча стн ое 1, остаток 4
= ч а стн ое 0, о стато к 1

5 2 0 5 10= 145В1б

СОВЕТ

Чтобы лучше разобраться в работе различных систем счисления, напишите
простую программу, подобную листингу 27.1 из занятия 27, “Применение
потоков для ввода и вывода”. Она использует объект s t d : : c o u t с мани­
пуляторами для отображения целых чисел в шестнадцатеричной, десятич­
ной и восьмеричной формах записи.
Чтобы отобразить целое число в двоичном формате, используйте класс
s t d : : b i t s e t , который был описан на занятии 25, “Работа с битовыми
флагами при использовании библиотеки STL”. Черпайте вдохновение из
листинга 25.1.

ПРИЛОЖЕНИЕ Б

Ключевые слова
языка C++
Ключевые слова зарезервированы компилятором для ис­
пользования языком C + +. Вы не можете определять классы,
переменные или функции с ключевыми словами в качестве
имен.

700

|

ПРИЛОЖ ЕНИЕ Б. Ключевые слова языка C++

alignas
alignof
and
and_eq
asm
auto
bitand
bitor
bool
break
case
catch
char
charl6_t
char32_t
class
compl
const
constexpr
const_cast
continue
decltype
default
delete
do
double
dynamic_cast
else

ПРИМЕЧАНИЕ

enum
explicit
export
extern
false
float
for
friend
goto
if
inline
int
long
mutable
namespace
new
noexcept
not
not_eq
nullptr
operator
or
or_eq
private
protected
public
register
reinterpr<

cast

return
short
signed
sizeof
static
static_assert
static_cast
struct
switch
template
this
thread_local
throw
true
try
typedef
typeid
typename
union
unsigned
using
virtual
void
volatile
wchar_t
while
xor
xor_eq

На занятии 10, “Реализация наследования", представлены два интересных
ключевых слова - final и override. Они не являются зарезервирован­
ными ключевыми словами C++, т.е. вы можете использовать их для име­
нования объектов и функций. Однако они имеют специальное значение в
некоторых конструкциях, о чем рассказано на упомянутом занятии.

ПРИЛОЖЕНИЕ В

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

702

ПРИЛОЖ ЕНИЕ В. Приоритет операторов

Приоритет операторов
Ранг

Название

Оператор

1

Р а зр е ш е н и е об ласти в и д и м о сти

•:

2

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

->
П
0

3

П ре ф и ксны й и н к р е м е н т и декрем ент, д о п о л н е н и е и о тр и ­

++



++



цание, ун а р н ы е м и н ус и плюс, п о л у ч е н и е а дреса и ссы лки,

л

!

а такж е о п е р а то р ы new, new [ ], d e l e t e , d e l e t e [ ],

-

+

&

*

s i z e o f () и п ри в е д е н и я ти п о в

size o f
new
new[]
delete
d e l e t e []
/\
\)
*
__>*

О б р а щ е н и е к эл е м е н ту по указателю

5

У м н ож ен ие, делени е, д е л е н и е по м од улю

*

/

6

С лож ен ие, вы чи тан ие

+

-

7

С д в и г влево, сд в и г в п р а в о

«

8

&
A

11

П о б и то в о е И СКЛ Ю Ч АЮ Щ ЕЕ ИЛИ

12

П о б и то в о е ИЛИ

I

13

Л о ги ч е ск о е И

&&

14

Л о ги ч е ск о е ИЛИ

15

Т е рн арны й у сл о в н ы й о п е р а то р , ге н е рац и я искл ю чен ия,

II
?:

п р и св а и в ан и е , со ста в н о е п р и св а и в а н и е

и



П о б и то в о е И

A

Равно, не р а вн о

10

»
A

9

%

II
V

М е н ьш е , м е н ь ш е или равн о, больш е, б о л ь ш е и ли р а вн о

V

4

!=

th ro w
= *= /= %=
+= -= « =
& =

16

Запятая

t

l=

л=

»=

ПРИЛОЖЕНИЕ Г

Коды ASCII
Работая с битами и байтами, компьютеры, по существу,
работают с числами. Чтобы представить символьные данные
в такой числовой системе, в свое время был принят стандарт
ASCII (American Standard Code for Information Interchange).
Стандарт ASCII назначает 7-битовые числовые коды латин­
ским символам A -Z , a -z , цифрам 0 - 9 , некоторым специаль­
ным клавишам (например, ) и специальным символам
(таким, как возврат на один символ).
7 битов обеспечивают 128 уникальных комбинаций, из
которых первые 32 ( 0 - 3 1 ) зарезервированы, поскольку
управляющие символы обычно используются для взаимо­
действия с периферийными устройствами, такими как прин­
теры.

704

П Р И Л О Ж Е Н И Е Г.

Коды ASCII

Таблица ASCII отображаемых символов
Коды ASCII 3 2 -1 2 7 используются для отображаемых символов, таких как 0 -9 ,
A -Z и a -z и некоторых других, таких как пробел. В приведенной ниже таблице пере­
числены десятичные и шестнадцатеричные значения, зарезервированные для этих
символов.
Символ

I

DEC

HEX

Описание

32

20

Пробел

33

21

В о скли ц а те льн ы й знак
Д в о й н ы е кавы чки

и

34

22

#

35

23

Ном ер

$

36

24

Д ол л а р

%

37

25

Знак п роц ента

&

38

26

А м п е р са н д

/

39

27

О д и н а рн а я кавы чка

(

40

28

О ткр ы ва ю щ а я скоб ка

)
*

41

29

За кры ваю щ ая скоб ка

42



Звезд оч ка

+

43



Плюс

/

44



Запятая

-

45

2D

Д еф ис

46



Точка

/

47

2F

Косая черта (или деление)

0

48

30

Н уль

1

49

31

О д ин

2

50

32

Два

3

51

33

Три

4

52

34

Ч еты ре

5

53

35

Пять

6

54

36

Ш есть

7

55

37

Семь

8

56

38

В о се м ь

9

57

39

Д евять

58

ЗА

Д во е то ч и е

;

59

ЗВ

Точка с запятой

<

60

ЗС

М е н ь ш е (или о ткры ваю щ ая угловая скобка)

=

61

3D

Равно

>
?

62

ЗЕ

Б о льш е (или закры ваю щ ая угловая скобка)

63

3F

В о п р о си те л ьн ы й знак

@

64

40

Символ @

А

65

41

П р о п и сн а я буква А

В

66

42

П р о п и сн а я буква В

Таблица ASCII отображаемых символов

705

П родолж ен и е т аблицы
Символ

DEC

HEX

О писание
П р о п и сн а я буква С

с

67

43

D

68

44

П р о п и сн а я буква D

Е

69

45

П р о п и сн а я буква Е

F

70

46

П р о п и сн а я буква F

G

71

47

П р о п и сн а я буква G

Н

72

48

П р о п и сн а я буква Н

1

73

49

П р о п и сн а я буква I

J

74

4A

П р о п и сн а я буква J
П р о п и сн а я буква К

К

75

4B

L

76

4C

П р о п и сн а я буква L

М

77

4D

П р о п и сн а я буква М

N

78

4E

П р о п и сн а я буква N

0

79

4F

П р о п и сн а я буква О

Р

80

50

П р о п и сн а я буква Р

Q

81

51

П р о п и сн а я буква Q

R

82

52

П р о п и сн а я буква R

S

83

53

П р о п и сн а я буква S

Т

84

54

П р о п и сн а я буква Т

и

85

55

П р о п и сн а я буква U

V

86

56

П р о п и сн а я буква V

W

87

57

П р о п и сн а я буква W

X

88

58

П р о п и сн а я буква X

Y

89

59

П р о п и сн а я буква Y

Z

90

5A

П р о п и сн а я буква Z

[

91

5B

О ткры ва ю щ а я квадратная скоб ка

\

92

5C

Косая черта влево

]
Л

93

5D

За кры ваю щ ая квадратная скоб ка

94

5E

С и м в о л Л (циркум ф лекс)

_

95

5F

Символ подчеркивания

'

96

60

С и м в о л ' (гравис)

а

97

61

С троч ная буква а

b

98

62

С троч н а я буква b

с

99

63

С троч ная буква с

d

100

64

С троч ная буква d

е

101

65

С троч н а я буква е

f

102

66

С троч ная буква f

9
h

103

67

С троч ная буква g

104

68

С троч ная буква h

i

105

69

С троч ная буква i

j

106

6A

С троч ная буква j

к

107

6B

Строч ная буква к

706

|

П Р И Л О Ж Е Н И Е Г.

Коды ASCII
О кончание т аблицы

Символ

DEC

HEX

О писание

1

108



С троч ная буква I

m

109

6D

С троч н а я буква m

п

110



С троч ная буква п

0

111

6F

С троч ная буква о

Р

112

70

С троч ная буква р

q

113

71

С троч н а я буква q

г

114

72

С троч ная буква г

S

115

73

С троч н а я буква s

t

116

74

С троч н а я буква t

U

117

75

С троч ная буква и

V

118

76

С троч ная буква v

W

119

77

С троч ная буква w

X

120

78

С троч ная буква х

У

121

79

С троч н а я буква у

Z

122



С троч н а я буква z

{

123



О ткр ы ва ю щ а я ф игурн ая скоб ка

1

124



В е рти кал ьная ли н и я

}

125

7D

З а кры ваю щ ая ф игурн ая скоб ка

~

126



С и м в о л ~ (тильда)

127

7F



ПРИЛОЖЕНИЕ Д

Ответы
Ответы к занятию 1
Контрольные вопросы
1. Интерпретатор — это инструмент, который интерпрети­
рует исходный код (или промежуточный байт-код) и вы­
полняет определенные действия. Компилятор получает
на вход исходный текст программы и создает объектный
файл. В языке C++ после компиляции и компоновки по­
лучается выполнимый файл, который может выполняться
процессором непосредственно, без необходимости в даль­
нейшей интерпретации.
2. Компилятор получает на входе файл исходного кода C++
и создает объектный файл на машинном языке. Зачастую
у вашего кода есть зависимости от библиотек и функций
в других файлах кода. Создание этих связей и получение
выполнимого файла, который интегрирует все явные и не­
явные зависимости, является задачей компоновщика.
3. Кодирование. Компиляция для создания объектного фай­
ла. Компоновка для создания выполнимого файла. Вы­
полнение для тестирования. Отладка. Устранение ошибок
в исходном тексте и повторение предыдущ их этапов.
В большинстве случаев компиляция и компоновка пред­
ставляют собой один этап.

Упражнения
1. Отображает результат вычитания у из х, а также их умно­
жения и сложения.
2. Результат: 2 48 14
3. Инструкция препроцессора io s t r e a m , находящаяся в
строке 1, должна начаться с #.
4. Отображает строку H e llo Buggy World

708

|

ПРИ ЛОЖ ЕНИЕ Д.

Ответы

Ответы к занятию 2
Контрольные вопросы
1. Код языка C++ чувствителен к регистру, i n t не является для компилятора ука­
занием целочисленного типа i n t .
2. Да.
/* Комментарий, использующий синтаксис в стиле С,
может располагаться в нескольких строках */

Упражнения
1. Причина неудачи в чувствительности к регистру компилятора C++. Ему не­
известно, что такое s t d : :C out и почему строка после этого не начинается с
кавычки. Кроме того, функция m ain () всегда должна объявляться как возвра­
щающая тип i n t .
2. Вот исправленная версия:
#include
int main ()

{
std::cout «
return 0;

"Is there a bug here?"; // Теперь без ошибок

}
3. Эта программа основана на листинге 2.4 и демонстрирует вычитание и умно­
жение:
#include
#using namespace std;
// Объявление функции
int DemoConsoleOutput();
int main()

{
// Вызов функции
DemoConsoleOutput();
return 0;

}
// Определение функции
int DemoConsoleOutput()

{
cout «
cout «

"Вычитание 1 0 - 5 =
"Умножение 1 0 * 5 =

return 0;

}

" « 10-5 «
" « 1 0 * 5 «

endl;
endl;

Ответы к занятию 3

|

709

Результат
Вычитание 10 - 5 = 5
Умножение 10 * 5 = 50

Ответы к занятию 3
Контрольные вопросы
1. В знаковом целом числе самый старший разряд означает знак числа (плюс или
минус). Беззнаковое же целое число используется только для положительных
значений.
2. Директива препроцессора t d e f i n e инструктирует компилятор осуществить
глобальную текстовую замену указанного значения. Однако эта замена не учи­
тывает безопасности типов и является примитивным способом определения
констант. Поэтому ее следует избегать.
3. Для гарантии, что она содержит определенное, а не случайное значение.
4. 2.
5. Имя не несет смысловой нагрузки и повторяет название типа. Хотя такой код
компилируется нормально, людям его трудно читать и поддерживать. Такого
желательно избегать. Для переменных лучше использовать описательные име­
на, которые отражают их цель, например
int Age = 0;

Упражнения
1. Это можно сделать несколькими способами:
enum YourCards {Асе = 43, Jack, Queen, King};
// Асе == 43, Jack == 44, Queen == 45, King == 46
// Альтернативный способ:
enum YourCards {Ace, Jack, Queen = 45, King};
// Ace == 0, Jack == 1, Queen == 45, King == 46

2. Просмотрите код листинга 3.5 и адаптируйте его для получения ответа на этот
вопрос.
3. Вот программа, которая запрашивает радиус круга, а затем вычисляет его пло­
щадь и периметр:
#include
using namespace std;
int main()

{
const double Pi = 3.1416;

710

|

ПРИ ЛОЖ ЕН ИЕ Д.

Ответы

cout « "Введите радиус: ";
double radius = 0;
cin » radius;
cout «
cout «

"Площадь = " «
"Длина
= " «

Pi * radius * radius « endl;
2 * Pi * radius « endl;

return 0;

}
Результат
Введите радиус: 4
Площадь = 50.2656
Длина
= 25.1328

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

Результат
Введите радиус: 4
Площадь = 5 0
Длина
=25

5. Ключевое слово a u to требует от компилятора автоматически выбрать тип пере­
менной в зависимости от инициализирующего ее значения. В приведенном коде
нет инициализации, и оператор приведет к ошибке при компиляции.

Ответы к занятию 4
Контрольные вопросы
1. 0 и 4 — это отсчитываемые от нуля индексы первого и последнего элементов
массива с пятью элементами.
2. Нет, так как известна их небезопасность, особенно при обработке пользователь­
ского ввода, поскольку они позволяют ввести строку длиннее массива.
3. Один нулевой завершающий символ.
4. В се зависит от того, как она используется. Если она используется в операторе
c o u t, например, то механизм отображения будет читать последовательность
символов, пока не найдет завершающий нулевой символ. При его отсутствии
он пересечет границы массива и, возможно, приведет к краху приложения.
5. Достаточно заменить в объявлении вектора часть i n t частью char.
vector dynArrChars(3);

Ответы к занятию 5

|

711

Упражнения
1. Вот что получилось. Приложение инициализируется значением Rook (ладья),
но оно достаточно простое, чтобы вы поняли все сами.
int m a i n ()

{
enum Square
{
Empty = О,
Pawn,
Rook,
Knight,
Bishop,
King,
Queen

};
Square chessBoard[8][8];
// Инициализация клеток с ладьями
chessBoard[0][0] = chessBoard[0][7] = Rook;
chessBoard[7][0] = chessBoard[7][7] = Rook;
return 0;

}
2. Чтобы присвоить значение пятому элементу массива, необходим доступ к эле­
менту myNumbers [4 ], поскольку индекс отсчитывается от нуля.
3. Обращение к четвертому элементу массива осуществляется до его инициали­
зации или присваивания значения. Вывод будет непредсказуемым. Всегда ини­
циализируйте переменные и массивы; в противном случае они будут содержать
последнее значение, хранившееся в выделенной для них области памяти.

Ответы к занятию 5
Контрольные вопросы
1. Целочисленные типы не могут содержать десятичных значений, которые впол­
не возможны при делении двух чисел. Используйте тип f l o a t .
2. Поскольку компилятор интерпретирует числа как целые, результат равен 4.
3. Поскольку числитель указан как 3 2 .0 , а не 32, компилятор интерпретирует его
как число с плавающей запятой, создав результат типа f l o a t , который составит
4,571.
4. Нет, s i z e o f — это оператор, который не может быть перегружен.
5. Это работает не так, как предполагалось, поскольку приоритет оператора суммы
превосходит таковой для оператора сдвига, что приводит к сдвигу на 1 + 5 = 6
битов, а не на 1 бит.
6. Результатом операции ИСКЛЮ ЧАЮ Щ ЕЕ ИЛИ будет f a l s e
табл. 5.5.

согласно

712

|

ПРИЛОЖЕНИЕ Д. Ответы

Упражнения
1. Вот правильное решение:
int Result = ((number «

1) + 5) «

1; // Теперь очевидно

2. Результат содержит значение переменной number, сдвинутое на 7 битов влево,
поскольку приоритет оператора + выше, чем оператора « .
3. Ниже приведена программа, которая получает два логических значения, вве­
денных пользователем, и демонстрирует результат использования побитовых
операторов на них.
#include
using namespace std;
int main()

{
cout «

"Введите значение true(l) или false(0): ";

bool valuel = false;
cin »
cout «

valuel;
"Введите второе значение true(l) или false(0): ";

bool value2 = false;
cin »

value2;

cout «

"Результаты логических операций:

cout «
cout «
cout «

"И: " «
(valuel & value2) « endl;
"ИЛИ: " «
(valuel | value2) « endl;
"ИСКЛЮЧАЮЩЕЕ ИЛИ: " «
(valuel A value2) «

" «

endl;

endl;

return 0;

Результат
Введите значение true(l) или false(0): 1
Введите второе значение true(l) или false(0): О
Результаты логических операций:
И: 0
ИЛИ: 1
ИСКЛЮЧАЮЩЕЕ ИЛИ: 1

Ответы к занятию 6
Контрольные вопросы
1.

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

Ответы к занятию 6

|

713

2. Его следует избегать, чтобы ваш код не стал запутанным и дорогим в обслужи­
вании.
3. См. код в ответе к упражнению 1, где используется оператор декремента.
4. Поскольку условие продолжения цикла f o r не удовлетворяется, цикл заверша­
ется, не выполнившись ни разу, поэтому оператор c o u t также ни разу не будет
выполнен.

Упражнения
1. Необходимо помнить, что индексы массива отсчитываются от нуля, а индекс
последнего элемента на единицу меньше его длины:
#include
using namespace std;
int main()

{
const int ARRAY_LEN = 5;
int myNumbers[ARRAY_LEN]= {-55, 45, 9889, 0, 45};
for(int nlndex = ARRAY_LEN - 1; nlndex >= 0; — nlndex)
cout «"myNumbers [" « nlndex
« " ] = "«myNumbers [nlndex] « e n d l ;
return 0;

Результат
myNumbers[4] = 4 5
myNumbers[3] = 0
myNumbers[2] = 9889
myNumbers[1] = 4 5
myNumbers[0] = -55

2. Вложенный цикл, эквивалентный использованному в листинге 6.14, но добав­
ляющий элементы в два массива в обратном порядке, выглядит так:
#include
using namespace std;
int m a i n ()

{
const int ARRAY1_LEN = 3;
const int ARRAY2_LEN = 2;
int myNumsl[ARRAY1_LEN] = {35, -3, 0};
int Mylnts2[ARRAY2_LEN] = {20, -1};
cout «
«

"Суммирование всех элементов myNumsl "
"со всеми элементами Mylnts2:" « endl;

714

|

ПРИЛОЖЕНИЕ Д. Ответы
for(int indexl = ARRAY1_LEN - 1; indexl >= 0; — indexl)
for(int index2 = ARRAY2_LEN - 1; index2 >= 0; — index2)
cout « myNumsl[indexl] « " + " « Mylnts2[index2]

«

" = " «

«

endl;

myNumsl[indexl] + Mylnts2[index2]

return 0;

}

Результат
Суммирование всех элементов myNumsl со всеми элементами Mylnts2:

0

+

-1

=

-1

0

+

20

=

20

-3 + -1 = -4
-3 + 20 = 17
35 + -1 = 34
35 + 20 = 55

3. Необходимо заменить фиксированное число 5 кодом, который запрашивает у
пользователя следующее:
cout «

"Сколько чисел Фибоначчи нужно вычислить:

";

int numsToCalculate = 0;
cin » numsToCalculate;

4. Конструкция s w it c h - c a s e с использованием перечисляемой константы, указы­
вающая, принадлежит ли цвет радуге, выглядит так:
#include
using namespace std;
int m a i n () {
enum Colors {
Violet = 0,
Indigo,
Blue,
Green,

Yellow,
Orange,
Red,
Crimson,
Beige,
Brown,

Peach,
Pink,
White,

};
cout «

"Доступные цвета:

cout «

"Violet:

" «

Violet «

endl;

cout «

"Indigo:

" «

Indigo «

endl;

cout «
cout «

"Blue: " « Blue « endl;
"Green: " « Green « endl;

cout «

"Yellow:

" «

" «

Yellow «

endl;

endl;

Ответы к занятию 6
cout « "Orange: " « Orange « endl;
cout « "Red: " « Red « endl;
cout « "Crimson: " « Crimson « endl;
cout «
"Beige: " « Beige «
endl;
cout « "Brown: " « Brown «
endl;
cout « "Peach: " « Peach «
endl;
cout « "Pink: " « Pink « endl;
cout « "White: " « White « endl;
cout « "Введите выбранный код: ";
int YourChoice = Blue;
cin » YourChoice;
switch(YourChoice) {
case Violet:
case Indigo:
case Blue:
case Green:
case Yellow:
case Orange:
case Red:
cout « "Выбранный цвет есть в радуге!" «
break;
default:
cout «
break;

"Этого цвета в радуге нет." «

}
return 0;

Результат
Доступные цвета:
Violet: 0
Indigo: 1
Blue: 2
Green: 3
Yellow: 4
Orange: 5
RED: 6
Crimson: 7
Beige: 8
Brown: 9
Peach: 10
Pink: 11
White: 12
Введите выбранный код: 4
Выбранный цвет есть в радуге!

endl;

endl;

715

716

ПРИЛОЖ ЕНИЕ Д. Ответы

5. В выражении условия выхода из цикла f o r программист по невнимательности
осуществил не сравнение, а присваивание счетчику значения 10.
6. Оператор w h i l e сопровождается пустым оператором 1; 1 в той же строке. Поэто­
му следующий за ним код увеличения значения переменной L o o p C o u n t e r никог­
да не будет достигнут, а следовательно, условие выхода никогда не будет выпол­
нено, цикл никогда не закончится и операторы после него никогда не выполнятся.
7. Отсутствует оператор b r e a k (т.е. часть d e f a u l t будет выполняться всегда, вне
зависимости от сработавшей ранее части c a s e , что явно неправильно).

Ответы к занятию 7
Контрольные вопросы
1. Область видимости этих переменных — реализация функции.
2. so m e N u m b e r — это ссылка на переменную в вызывающей функции. Она не со­
держит копию значения.
3. Рекурсивная функция.
4. Перегруженные функции.
5. На вершину! Стек похож на стопку тарелок; ту, которая находится сверху, мож­
но взять, и именно на нее указывает указатель вершины стека.

Упражнения
1. Прототипы функций выглядели бы следующим образом:
double Area(double Radius);
// Сфера
double Area(double Radius, double Height); // Цилиндр

Реализации (определения) функций используют соответствующие формулы,
предоставленные в вопросе, и возвращают вызывающей стороне объем как
значение.
2. Аналог — в листинге 7.8. Прототип функции был бы следующим:
void ProcessArray(double numbers[], int length);

3. Чтобы это сработало, параметр R e s u l t функции A r e a должен быть ссылкой:
void Area(double radius, double &result)

4. Либо параметр со значением по умолчанию должен располагаться в конце, либо
значения по умолчанию нужно определить для всех параметров.
5. Функция должна возвратить данные вызывающей стороне по ссылке:
void Area(double radius, double &area, double ^circumference)

{
area = 3.14 * radius * radius;
circumference = 2 * 3.14 * radius;

Ответы к занятию 8

|

717

Ответы к занятию 8
Контрольные вопросы
1. Если бы компилятор позволял такое, то это был бы очень простой способ на­
рушить то, для чего предназначены константные ссылки: защита данных от из­
менения.
2. Это операторы.
3. Адрес области памяти.
4. Оператор *.

Упражнения
1. 40.
2. В первом варианте аргументы копируются в вызываемую функцию. Во втором
они не копируются, поскольку это ссылки на переменные вызывающей сторо­
ны, и функция может их изменять. Третий вариант использует указатели, ко­
торые в отличие от ссылок могут быть пусты или недопустимы. В этом случае
следует обеспечить их допустимость.
3. Используйте ключевое слово c o n st:
1: const int* pNuml = &number;

4. Вы присваиваете целое число непосредственно указателю (т.е. перезаписываете
содержавшийся в нем адрес целочисленного значения в памяти):
*pNumber = 9 ;

// было: pNumber = 9;

5. Двойное освобождение одного и того же адреса области памяти, возвращенного
оператором new указателю pNuiriber и скопированного в указатель pNumberCopy.
Удалите один из операторов d e l e t e .
6. 30.

Ответы к занятию 9
Контрольные вопросы
1. В динамической памяти. Это то же самое, что и выделение памяти для типа i n t
с использованием оператора new.
2. Оператор s i z e o f O
вычисляет размер класса на осн ове заявленных
переменных-членов. Поскольку размер указателя является постоянным и не за­
висит от размера данных, на которые он указывает, размер класса, содержащего
один такой указатель-член, также остается постоянным.
3. Никто, кроме методов этого же класса.

718

|

ПРИЛОЖ ЕНИЕ Д. Ответы

4. Да, может.
5. Конструктор обычно используется для инициализации переменных-членов и
ресурсов.
6. Деструкторы обычно используются для освобождения памяти и ресурсов.

Упражнения
1. Язык C++ чувствителен к регистру. Объявление класса должно начаться со
слова c l a s s , а не C la s s . Оно должно закончиться точкой с запятой (;), как по­
казано ниже.
class Human
{
int age;
string name;
public:
Human () {}

};
2. Поскольку переменная-член Human: :a g e закрытая (вспомните, что, в отличие
от структуры, члены класса являются по умолчанию закрытыми) и нет никакой
открытой функции доступа, то нет и никакого способа, которым пользователь
этого класса может обратиться к переменной аде.
3. Вот версия класса Human со списком инициализации в конструкторе:
class Human
{
int age;
string name;
public:
Human(string inputName, int inputAge)
: name(inputName), age(inputAge) {}

};
4. Обратите внимание: число n является невидимым извне класса, как и требова­
лось:
#include
using namespace std;
class Circle {
const double Pi;
double radius;
public:
Circle(double InputRadius) : radius(InputRadius), P i (3.1416) {}
double GetCircumference() {
return 2 * Pi * radius;

}

Ответы к занятию 10

|

719

double GetArea() {
return Pi * radius * radius;

}
};
int main()
cout «
double
cin »
Circle
cout «
cout «
return

{
"Введите радиус: ";
radius = 0;
radius;
MyCircle(radius);
"Окружность = " « MyCircle.GetCircumference() «
"Площадь = " « MyCircle.GetArea() « endl;
0;

endl;

Ответы к занятию 10
Контрольные вопросы
1.

Используйте модификатор доступа p r o t e c t e d . Он обеспечит видимость члена
базового класса для производного класса, но не таковому вне его.

2. Часть объекта, соответствующая производному классу, срезается, а по значению
передается только часть, соответствующая базовому классу. Результат может
быть непредсказуемым.
3. Композиция делает проект гибче.
4. Позволяет раскрыть методы базового класса.
5. Нет, поскольку у первого класса, который специализирует класс B a s e , т.е. клас­
са D e r iv e d , есть отношения закрытого наследования с классом B a s e . Таким
образом, открытые члены класса B a s e являются закрытыми для класса S u b D e r iv e d , а следовательно, они недоступны.

Упражнения
1. Конструкторы вызываются в порядке объявления:
M a in m a l- B ir d - R e p t ile - P la t y p u s .

Удаление осуществляется в обратном порядке.
2. Например, так:
class Shape
{
// ... Члены класса Shape

class Polygon: public Shape
{
// ... Члены класса Polygon

720

|

ПРИ ЛОЖ ЕНИЕ Д.

Ответы

class Triangle: public Polygon
{
// ... Члены класса Triangle

}
3. Отношения наследования между классами D1 и Base должны быть закрытыми.
4. По умолчанию классы наследуются закрыто. Если бы D erived был структурой,
то наследование было бы открытым.
5. Функция S o m e F u n c () ожидает передачи параметра типа B a s e по значению. Это
означает, что вызов с указанным производным типом приведет к срезке, резуль­
тат которой непредсказуем:
Derived objectDerived;
SomeFunc(objectDerived); // Срезка

Ответы к занятию 11
Контрольные вопросы
1.

Объявите абстрактный класс S h a p e с чисто виртуальными функциями A r e a () и
P r i n t ( ) , и это заставит классы C i r c l e и T r i a n g l e их реализовать.

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

Упражнения
1.

Ниже показана иерархия наследования для абстрактного класса S h a p e и произ­
водных от него классов C i r c l e и T r i a n g l e .
#include
using namespace std;
class Shape
{
public:
virtual double Area() = 0;
virtual void Print() = 0;

};
class Circle
{
double Radius;
public:

Ответы к занятию 11
Circle(double inputRadius)

: Radius(inputRadius)

721

{}

double Area()

{
return 3.1415 * Radius * Radius;

}
void Print()

{
cout «

"Circle::Print()" «

endl;

}

class Triangle

{
double Base, Height;
public:
Triangle(double inputBase, double inputHeight)
: Base(inputBase), Height(inputHeight) {}
double Area()

{
return 0.5 * Base * Height;

}
void Print()

{
cout «

"Triangle::Print()" «

endl;

}

int main()

{
Circle myRing(5);
Triangle myWarningTriangle(6.6, 2);
cout «
cout «
«

"Площадь круга: " « myRing.Area () « endl;
"Площадь треугольника: " « myWarningTriangle.Area()
endl;

myRing.Print();
myWarningTriangle.Print();
return 0;

)
2. Отсутствует виртуальный деструктор!
3. Без виртуального деструктора последовательность выполнения конструкторов
была бы такой: V e h ic le ( ), затем — Саг (), а невиртуальный деструктор будет
вызван только о д и н ------V e h ic le ().

722

|

ПРИЛОЖ ЕНИЕ Д. Ответы

Ответы к занятию 12
Контрольные вопросы
1. Нет, язык C++ не позволяет двум функциям с одним и тем же именем иметь
разные возвращаемые значения. Вы можете создать две реализации операто­
ра [] с разными типами возвращаемого значения, но при этом один оператор
может быть определен как константная функция, а другой нет. В таком случае
компилятор C++ выбирает неконстантную версию для действий, связанных с
присваиванием, и константную версию в противном случае:
const Types operator!](int Index) const;
Type& operator!](int Index);

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

Упражнения
1. Оператор преобразования i n t ().
class Date
{
int day, month, year;
public:
operator int()

{
return ((year * 10000) + (month * 100) + day);

}
// Конструктор и т.д.

};
2. Конструктор перемещения и оператор присваивания при перемещении приве­
дены ниже.
class Dynlntegers
{
private:
int* arrayNums;

Ответы к занятию 13

723

public:
// Перемещающий конструктор
Dynlntegers(Dynlntegers&& moveSrc)

{
arrayNums = moveSrc.arrayNums; // Получение владения
moveSrc.arrayNums = nullptr;
// Освобождение от владения
// исходного объекта

}
// Перемещающий оператор присваивания
Dynlntegers& operator=(Dynlntegers&& moveSrc)

{
if(this != &moveSrc)

{
delete[] arrayNums;
//Освобождение своего ресурса
arrayNums = moveSrc.arrayNums;
moveSrc.arrayNums = nullptr;

}
return *this;

}
^Dynlntegers() {delete!] arrayNums;} // Деструктор
// Реализация конструктора по умолчанию, копирующего конструктора,
// оператора присваивания

Ответы к занятию 13
Контрольные вопросы
1. Оператор d y n am ic_ ca st.
2. Исправьте функцию, конечно. Оператор c o n s t _ c a s t и операторы приведения
вообще должны быть последним средством.
3. Правда.
4. Да, правда.

Упражнения
1.

Результат динамической операции приведения всегда должен проверяться на
корректность:
void DoSomething(Base* objBase)

{
Derived* objDer = dynamic_cast (objBase);
if(objDer) // Проверка корректности
objDer->DerivedClassMethod();

}

ПРИЛОЖЕНИЕ Д. Ответы

724
2.

Используйте оператор s t a t i c c a s t , поскольку известно, что указываемый
объект имеет тип T u n a . Взяв за основу листинг 13.1, можно получить такую
функцию m a in ():
int main()

{
Fish* pFish = new Tuna;
Tuna* pTuna = static_cast(pFish);
// Tuna::BecomeDinner сработает только при
// использовании корректного Tuna*
pTuna->BecomeDinner();
// Виртуальный деструктор в Fish гарантирует вызов ~Tuna()
delete pFish;
return 0;

Ответы к занятию 14
Контрольные вопросы
1. Конструкция препроцессора, препятствующая множественному или рекурсив­
ному включению файлов заголовка.
2. 4.
3. 1 0 + 1 0 / 5 = 10 + 2 = 1 2 .
4.

Использовать скобки: # d e f in e S P L I T (х )

{ (х) / 5 )

Упражнения
1.

# d e f in e M U L T I P L Y ( а , b)

( (а)* (Ь))

2. Вот шаблон, аналогичный макросу из контрольного вопроса 4:
template Т Split(const Т& input)

{
return (input / 5);

}
3.

Шаблонная функция s w a p () будет такой:
template
void Swap(T& x, T& у)

{
T temp = x;
x = у;
у = temp;

}
4. # d e f in e Q U A R T E R (x )

( (x)/

4)

Ответы к занятию 15

725

5. Определение шаблона класса выглядело бы так:
template ctypename ArraylType, typename Array2Type>
class TwoArrays
{
private:
ArraylType Arrayl[10];
Array2Type Array2[10];
public:
ArraylType& GetArraylElement(int Index){return Arrayl[Index];}
Array2Type& GetArray2Element(int Index){return Array2[Index];}

);
6. Вот как может выглядеть шаблонная функция D is p la y ():
#include
using namespace std;
void Display() {

}
template ctypename First, typename ...Last>
void Display(First a, Last... U) {
cout « a « endl;
Display(U ...);

}
int main() {
Display('a');
Display(3.14);
Display('a', 3.14);
Display('z', 3.14567, "Вариадический шаблон");
return 0;

Результат
a
3.14
a
3.14
z
3.14567
Вариадический шаблон

Ответы к занятию 15
Контрольные вопросы
1.

Контейнер deque. Только он обеспечивает вставку в начало и в конец контейне­
ра за константное время.

726

|

ПРИЛОЖ ЕНИЕ Д . Ответы

2. Контейнер s t d : : s e t или s t d : :map, если у вас пары “ключ-значение”. Если
элементы могут дублироваться, выберите контейнер s t d : :m u l t i s e t или
s t d : :m ultim ap.
3. Да. Создавая экземпляр шаблона s t d : : s e t , можете также задать второй па­
раметр шаблона, являющийся двоичным предикатом, который класс s e t ис­
пользует как критерий сортировки. Задайте в этом предикате критерии соот­
ветственно вашим требованиям.
4. М ост между алгоритмами и контейнерами образуют итераторы, чтобы первые
(являющиеся обобщ ением) могли взаимодействовать с последними без необхо­
димости знать конкретный тип контейнера.
5. Контейнер h ash s e t не является стандартным для C++. Вы не должны исполь­
зовать его в переносимом приложении; применяйте в таких случаях контейнер
std ::m a p .

Ответы к занятию 16
Контрольные вопросы
1. Шаблон s t d : : b a s i c _ s t r i n g .
2. Скопируйте эти две строки в два строковых объекта. Преобразуйте каждую
скопированную строку в нижний или в верхний регистр. Получите результат
сравнения преобразованных копий строк.
3. Нет, они не подобны. Строки в стиле С — это фактически простые указатели,
родственные символьному массиву, тогда как строка библиотеки STL — это
класс s t r i n g , реализующий различные операторы и функции-члены для об­
работки строк, что делает их применение простым настолько, насколько это
возможно.

Упражнения
1. Программа должна использовать функцию s t d : : r e v e r s e ():
#include
#include
#include
int main()

{
using namespace std;
cout « "Введите слово для проверки:" «
string strlnput;
cin » strlnput;
string strCopy(strlnput);

endl;

Ответы к занятию 16
reverse(strCopy.begin(), strCopy.end());
if (strCopy == strlnput)
cout « strlnput « " - это палиндром!" « endl;
else
cout « strlnput « " - это не палиндром." « endl;
return 0;

}
2.

Используйте функцию s t d : : f in d ():
#include
iinclude
using namespace std;
// Количество символов 'chToFind' в строке "strlnput"
int GetNumCharacters(strings strlnput, char chToFind)

{
int nNumCharactersFound = 0;
size_t nCharOffset = strlnput.find(chToFind);
while(nCharOffset != string::npos)

{
++nNumCharactersFound;
nCharOffset = strlnput.find(chToFind, nCharOffset + 1);

}
return nNumCharactersFound;

int main()

{
cout « "Введите строку:" «
string strlnput;
getline(cin, strlnput);

endl «

"> ";

int nNumVowels = GetNumCharacters(strlnput, 'a');
nNumVowels += GetNumCharacters(strlnput, 'ef);
nNumVowels += GetNumCharacters(strlnput, 'i ');
nNumVowels += GetNumCharacters(strlnput, 'o');
nNumVowels += GetNumCharacters(strlnput, 'u');
// Обработка прописных букв
cout «

"Количество гласных в предложении = " «

return 0;

nNumVowels;

727

728

|

ПРИЛОЖ ЕНИЕ Д. Ответы

3. Используйте функцию to u p p er ():
#include
#include
#include
int main()

{
using namespace std;
cout «
cout «

"Введите строку:" «
"> ";

endl;

string strlnput;
getline(cin, strlnput);
cout « endl;
for(size_t nCharlndex = 0;
nCharIndex < strlnput.length();
nCharlndex += 2)
strlnput[nCharlndex] = toupper(strlnput[nCharlndex]);
cout «
cout «

"Преобразованная строка: " «
strlnput « endl « endl;

endl;

return 0;

}
4. Это может быть очень просто реализовано так:
#include
#include
int main()

{
using namespace std;
const
const
const
const

string
string
string
string

strl
str2
str3
str4

=
=
=
=

"I";
"Love";
"STL";
"String.";

string strResult = strl+" "+str2+" "tstr3+" "+str4;
cout «
cout «

"Предложение:" «
strResult;

endl;

return 0;

}
5. Воспользуйтесь s t d : : s t r i n g : : f in d ():

Ответы к занятию 17

|

729

#include
#include
int main() {
using namespace std;
string sampleStr("Good day String! Today is beautiful!");
cout « "Строка: " « sampleStr « endl;
cout « "Позиции буквы 'a'" « endl;
auto charPos = sampleStr.find('a', 0);
while(charPos != string::npos) {
cout « "'" « ?a' « "' found";
cout « " найдена в позиции: " « charPos « endl;
// 'find' продолжает поиск со следующего символа
onwards
size_t charSearchPos = charPos + 1;
charPos = sampleStr.find('a', charSearchPos);

return 0;

Результат
Строка: Good day String! Today is beautiful!
Позиции буквы 'a'
'а'
найдена в позиции: 6
'а'
найдена в позиции: 20
'а ’ найдена в позиции: 28

Ответы к занятию 17
Контрольные вопросы
1. Нет, не могут. За константное время элементы могут быть только добавлены в
конец вектора.
2. Еще 10. При 11-й вставке произойдет повторное выделение.
3. Извлекает последний элемент, т.е. удаляет элемент с конца.
4. Типа Mammal.
5. С помощью оператора индексации ([ ]) или функции a t ().
6. Итератор прямого доступа.

Упражнения
1.

Одно из решений таково:
#include
#include

730

|

ПРИЛОЖ ЕНИЕ Д. Ответы

using namespace std;
char DisplayOptions()

{
cout
cout
cout
cout
cout

«
«
«
«
«

"Выберите действие:" « endl;
"1: Ввести целое число" « endl;
"2: Запрос значения по индексу" « endl;
"3: Вывод вектора" « endl « "> ";
"4: Выход!" « endl « "> ";

char ch;
cin » ch;
return ch;

}
int main()

{
vector vecData;
char chUserChoice = '\0';
while((chUserChoice = DisplayOptions()) != ’4')

{
if (chUserChoice == '1')

{
cout « "Введите вставляемое целое число: ";
int nDatalnput = 0;
cin » nDatalnput;
vecData.push_back(nDatalnput);

}
else if (chUserChoice == '2')

{
cout « "Введите индекс от 0 до ";
cout « (vecData.size() - 1) « ": ";
int nlndex = 0;
cin » nlndex;
if (nlndex < (vecData.size()))

{
cout «"Element ["«nlndex«"] = "«vecData [nlndex]
cout « endl;

}
}
else if (chUserChoice == '3')

{
cout « "Содержимое вектора: ";
for(size_t nlndex = 0;
nlndex < vecData.size(); ++nlndex)
cout « vecData[nlndex] « f ';
cout « endl;

Ответы к занятию 17

|

731

}
}
return 0;

2. Используйте алгоритм s t d : : f in d ():
vector ::iterator elementFound = std::find(vecData.begin(),
vecData.end(), value);

3. Вот возможное решение. Обратите внимание, что класс D im ensions реализует
оператор c o n s t ch ar*, позволяющий работать с ним потоку co u t.
#include
#include
#include
#include
using namespace std;
char DisplayOptions() {
cout « "Выберите действие:" « endl;
cout « "1: Ввод длины и ширины " « endl;
cout « "2: Запрос значения по индексу" « endl;
cout « "3: Вьюод размеров всех упаковок" « endl;
cout « "4: Выход!" « endl « "> ";
char ch;
cin » ch;
return ch;

}
class Dimensions {
int length, breadth;
string strOut;
public:
Dimensions(int inL, int inB) : length(inL), breadth(inB)
operator const char * () {
stringstream os;
os « "Длина "s « length « ", ширина: "s
« breadth « endl;
strOut = os.str();
return strOut.c_str();

}
};
int main() {
vector vecData;
char chUserChoice = '\0';
while((chUserChoice = DisplayOptions()) != '4') {
if (chUserChoice == 11') {
cout « "Введите длину и ширину: " « endl;
int length = 0, breadth = 0;
cin » length;
cin » breadth;
vecData.push_back(Dimensions(length, breadth));

{}

732

|

ПРИЛОЖ ЕНИЕ Д. Ответы
} else if (chUserChoice == '2') {
cout « "Введите индекс от 0 до ";
cout « (vecData.size() - 1) « ": ";
size_t index = 0;
cin » index;
if (index <
cout «
«
cout «

(vecData.size())) {
"Element[" « index «
vecData[index];
endl;

"] = "

}
} else if (chUserChoice == '3') {
cout « "Содержимое вектора:
for(size_t index = 0; index < vecData.size(); ++index)
cout « vecData[index] « '
cout «

endl;

return 0;

}
4.

Инициализация списком делает код компактнее:
#include
#include
#include
using namespace std;
template
void DisplayDeque(deque inDQ) {
for(auto element = inDQ.cbegin();
element != inDQ.cend();
++element)
cout « * element « endl;

}
int main() {
deque strDq { "Hello"s, "Containers are cool"s,
"C++ is evolving!"s

);
DisplayDeque(strDq);
return 0;

}

Ответы к занятию 18
Контрольные вопросы
1.

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

Ответы к занятию 18

|

733

2. Особенность списка в том, что такие операции не влияют на допустимость су­
ществующих итераторов.
3. m L is t .c le a r ( ) ; или m L is t .e r a s e ( m L is t .b e g in () ,m L is t .e n d ( ) ) ;
4. Да, перегруженная версия функции i n s e r t () позволяет вставить диапазон эле­
ментов из исходной коллекции.

Упражнения
1. Решение, как в упражнении 1 занятия 17, “Классы динамических массивов биб­
лиотеки STL”, для вектора. Единственное отличие — в использовании функции
вставки для списка: m L i s t . i n s e r t ( m L i s t . b e g i n () , n D a t a I n p u t ) ;
2. Сохраните итераторы для двух элементов в списке. Вставьте элемент между
ними, используя функцию вставки. Используйте итераторы для демонстрации
того, что они все еще в состоянии обратиться к значениям, на которые указы­
вали прежде.
3. Вот возможное решение:
#include
#include
#include
using namespace std;
int main() {
vector vecData(4);
vecData[0] = 0;
vecData[l] = 10;
vecData[2] = 20;
vecData[3] = 30;
list listlntegers;
// Вставка содержимого вектора в начало списка
listlntegers.insert(listlntegers.begin(),
vecData.begin(), vecData.end());
cout « "Содержимое списка: ";
list ::const_iterator iElement;
for(iElement = listlntegers.begin();
iElement != listlntegers.end();
++iElement)
cout « * iElement « " ";
return 0;

};
4. Возможное решениеприведено ниже.
tinclude
#include
tinclude

734

|

ПРИЛОЖ ЕНИЕ Д. Ответы

using namespace std;
int main() {
list listNames;
listNames.push_back("Jack") ;
listNames.push_back("John");
listNames.push_back("Anna");
listNames.push_back("Skate");
cout « "Содержимое списка: ";
list ::const_iterator iElement;
for(iElement=listNames.begin();iElement!=listNames.end();
++iElement)
cout « * iElement « " ";
cout « endl;
cout « "Содержимое после реверса: ";
listNames.reverse();
for(iElement=listNames.begin();iElement!=listNames.end();
++iElement)
cout « * iElement « " ";
cout « endl;
cout « "Содержимое после сортировки: ";
listNames.sort();
for(iElement=listNames.begin();iElement!=listNames.end();
++iElement)
cout « * iElement « " ";
cout « endl;
return 0;

Ответы к занятию 19
Контрольные вопросы
1. Критерий сортировки по умолчанию определяется как s t d : : l e s s o , что фак­
тически задействует оператор o p e r a to r < для сравнения двух целых чисел, и
возвращает значение tr u e , если первое число меньше второго.
2. Рядом, один за другим.
3. Для всех контейнеров библиотеки STL это функция s i z e ().

Упражнения
1. Одно из возможных решений:

Ответы к занятию 19

735

#include
#include
linclude
using namespace std;
template
void DisplayContents(const T & container) {
for(auto iElement = container.cbegin();
iElement != container.cend();
++iElement)
cout « * iElement « endl;
cout «

endl;

}
struct ContactItem {
string name;
string phoneNum;
string displayAs;
ContactItem(const string & namelnit, const string & phone)
name = namelnit;
phoneNum = phone;
displayAs = (name + ": " + phoneNum);

}
// Используется в set::find()
bool operator == (const ContactItem & itemToCompare) const

{
return (itemToCompare.phoneNum == this->phoneNum);

}
// Используется для сортировки
bool operator < (const ContactItem & itemToCompare) const

{
return (this->phoneNum < itemToCompare.phoneNum);

}
// Используется для вывода DisplayContents в cout
operator const char * () const
{
return displayAs.c_str();

}
};
int main() {
set setContacts;
setContacts.insert(
ContactItem("Jack Welsch",
setContacts.insert(
ContactItem("Bill Gates",
setContacts.insert(
ContactItem("Angi Merkel",
setContacts.insert(
ContactItem("Vlad Putin",
setContacts.insert(

"+1 7889 879 879"));
"+1 97 7897 8799 8"));
"+49 23456 5466"));
"+7 6645 4564 797"));

{

736

|

ПРИ ЛОЖ ЕНИЕ Д.

Ответы

ContactItem("John Travolta", "+1 234 4564 789"));
setContacts.insert(
ContactItem("Ben Affleck", "+1 745 641 314"));
DisplayContents(setContacts);
cout « "Введите искомый номер: ";
string input;
getline(cin, input);
auto contactFound = setContacts.find(ContactItem("", input));
if (contactFound != setContacts.end()) {
cout « "Номер принадлежит "
« ( * contactFound).name « endl;
DisplayContents(setContacts);
} else
cout « "Контакт не найден" « endl;
return 0;

)
2. Структура и определение мультимножества могут быть такими:
#include
#include
#include
using namespace std;
struct PAIR_W0RD {
string word;
string meaning;
PAIR_WORD(const string&sWord, const string&sMeaning)
: word(sWord), meaning(sMeaning) {}
bool operatorword);

}
};
int main() {
multiset msetDictionary;
PAIR_W0RD wordl("C++", "A programming language");
PAIR_W0RD word2("Programmer", "A geek!");
msetDictionary.insert(wordl);
msetDictionary.insert(word2);
cout « "Введите слово, которое вы хотите найти" « endl;
string input;
getline(cin, input);
auto element = msetDictionary.find(PAIR_W0RD(input,""));

Ответы к занятию 20
if (element != msetDictionary.end())
cout « "Значение: " « (*element).meaning «

endl;

return 0;

}
3.

Одно из решений приведено ниже.
#include
#include
using namespace std;
template
void DisplayContent(const T & cont) {
T::const_iterator element;

for(element = cont.begin();
element != cont.end(); ++element)
cout « * element « " ";

}
int main() {
multiset msetIntegers;
msetIntegers.insert(5);
msetIntegers.insert(5);
msetIntegers.insert(5);
set setlntegers;
setlntegers.insert(5);
setIntegers.insert(5);
setIntegers.insert(5);
cout « "Вывод содержимого multiset: ";
DisplayContent(msetlntegers);
cout « endl;
cout « "Вьгоод содержимого set: ";
DisplayContent(setlntegers);
cout « endl;
return 0;

}

Ответы к занятию 20
Контрольные вопросы
1. Критерий сортировки по умолчанию определяется как s t d : : l e s s o .
2. Рядом, один за другим.
3. Функция s i z e ().
4. В отображении нет двойных элементов!

737

738

|

ПРИ ЛОЖ ЕН ИЕ Д.

Ответы

Упражнения
1. Ассоциативный контейнер, который допускает двойные записи, например
s t d : :m u l t i m a p :

std::multimap multimapPeopleNamesToNumbers;

2. Определение предиката таково:
struct fPredicate
{
bool operator
struct Double {
int m_nUsageCount;
// Конструктор
Double() : m_nUsageCount(0) {};
void operator()(const elementType element) const

{
++m_nUsageCount;
cout « element * 2 «

' ';

}
};
3. Бинарный предикат имеет следующий вид:
template
class SortAscending {
public:
bool operator()(const elementType & numl,
const elementType & num2) const {
return (numl < num2);

Вот как может быть использован этот предикат:
#include
#include
#include
int main() {
std::vector veclntegers;
// Вставка чисел: 100, 90... 20, 10
for(int nSample = 10; nSample > 0; — nSample)
veclntegers.push_back(nSample * 10);

740

|

ПРИ ЛОЖ ЕНИЕ Д.

Ответы

std::sort(veclntegers.begin(), veclntegers.end(),
SortAscending());
for(size_t nElementlndex = 0;
nElementlndex < veclntegers.size();
++nElementIndex)
cout « veclntegers[nElementlndex] «



return 0;

}

Ответы к занятию 22
Контрольные вопросы
1. Лямбда всегда начинается с квадратных скобок ([ ]).
2. С помощью списка захвата:
[Varl, Var2,

...](Туре& param)

{ ...; }

3. Следующим образом:
[Varl, Var2,

...](Туре& param) -> ReturnType { ...; }

Упражнения
1. Вот одно из возможных решений:
sort(vecNumbers.begin(), vecNumbers.end(),
[](int numl, int num2) {return (numl > num2); } );

2. Вот как может выглядеть это лямбда-выражение:
cout « "Число, добавляемое ко всем элементам:
int numlnput = 0;
cin » numlnput;
for_each(vecNumbers.begin(), vecNumbers.end(),
[ = ] (int & element) { element += numlnput;});

Пример, демонстрирующий решения упражнений 1 и 2:
#include
#include
#include
using namespace std;
template
void DisplayContents(const T & container) {
for(auto element = container.cbegin();
element != container.cendO;
++element)
cout « * element « ' ';

Ответы к занятию 23
cout «

741

endl;

}
int main() {
vector vecNumbers { 25, -5, 122, 2011, -10001 };
DisplayContents(vecNumbers);
sort(vecNumbers.begin(), vecNumbers.end());
DisplayContents(vecNumbers);
sort(vecNumbers.begin(), vecNumbers.end(),
[](int Numl, int Num2) {
return (Numl > Num2);

});
DisplayContents(vecNumbers);
cout « "Число, добавляемое ко всем элементам: ";
int numcontainer = 0;
cin » numcontainer;
for_each(vecNumbers.begin(), vecNumbers.end(),
[ = ] (int & element) {
element += numcontainer;

});
DisplayContents(vecNumbers);
return 0;

Результат
25 -5 122 2011 -10001
-10001 -5 25 122 2011
2011 122 25 -5 -10001
Число, добавляемое ко всем элементам: 5
2016 127 30 0 -9996

Ответы к занятию 23
Контрольные вопросы
1.

Используйте функцию s t d : : l i s t : : r e m o v e _ i f ( ) , поскольку она гарантирует,
что существующие итераторы на элементы в списке (которые не были удалены)
останутся действительными.

2. Функция l i s t :: s o r t () (и даже s t d : : s o r t ( ) ) в отсутствие явно заданного
предиката прибегает к сортировке с использованием предиката s t d : : le s s < > ,
который, в свою очередь, использует для сортировки объектов коллекции опе­
ратор o p e r a t o r s
3. По одному разу для каждого элемента в диапазоне.
4. Функция fo r _ e a c h () работает с унарным предикатом и возвращает функцио­
нальный объект, который может содержать информацию состояния. Функция
tr a n sfo r m () может работать с унарным или бинарным предикатом и предо­
ставляет перегруженную версию, которая в состоянии работать с двумя диа­
пазонами.

742

|

ПРИ ЛОЖ ЕН ИЕ Д.

Ответы

Упражнения
1. Одно из возможных решений приведено ниже.
struct CaselnsensitiveCompare {
bool operator()(const string & strl, const string & str2) const

{
string strlCopy(strl), str2Copy(str2);
transform(strlCopy.begin(),
strlCopy.end(), strlCopy.begin(), tolower);
transform(str2Copy.begin(),
str2Copy.end(), str2Copy.begin(), tolower);
return (strlCopy < str2Copy);

};
2. Вот пример такой демонстрации. Обратите внимание, что функция s t d :: сору ()
работает, не зная характера коллекций. Она использует только классы итератора:
#include
#include
#include
#include
#include







using namespace std;
int main() {
list listNames;
listNames.push_back("Jack");
listNames.push_back("John");
listNames.push_back("Anna");
listNames.push_back("Skate");
vector vecNames(4);
copy(listNames.begin(),listNames.end(),vecNames.begin());
vector ::const_iterator iNames;
for(iNames = vecNames.beginO;
iNames != vecNames.end(); ++iNames)
cout « * iNames « ' ';
return 0;

}
3. Различие между функциями s t d : : s o r t () и s t d : : s t a b l e _ s o r t () в том, что
последняя при сортировке сохраняет относительные положения одинаковых
объектов. Поскольку приложение должно хранить данные в порядке событий,
следует выбрать функцию s t a b l e _ s o r t (), чтобы сохранить этот порядок.

Ответы к занятию 24

|

743

Ответы к занятию 24
Контрольные вопросы
1. Да, предоставив соответствующий бинарный предикат.
2. Класс Coin должен реализовать оператор o p e r a t o r s
3. Нет, воздействовать можно только на вершину стека. Поэтому вы не можете
обратиться к элементу внизу стека.

Упражнения
1. Бинарный предикат может быть оператором o p e r a t o r c
class Person {
public:
int age;
bool isFemale;
bool operator anotherPerson.age)
bRet = true;
else if (isFemale && anotherPerson.isFemale)
bRet = true;
return bRet;

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

Ответы к занятию 25
Контрольные вопросы
1. Нет. Количество битов, которые может хранить множество битов, фиксируется
во время компиляции.
2. Поскольку он им не является. Класс b i t s e t не может менять свой размер дина­
мически, как другие контейнеры; он не поддерживает итераторы, как контейне­
ры, и не нуждается в них.
3. Нет. Для этого лучше подходит класс s t d : : b i t s e t .

744

|

ПРИЛОЖЕНИЕ Д. Ответы

Упражнения
1. Вот пример кода, в котором объект класса s t d :: b i t s e t создается, инициализи­
руется, отображается и добавляется:
#include
iinclude
int main() {
// Инициализация значением 1001
std::bitset fourBits(9);
std::cout « "fourBits: " « fourBits « std::endl;
// Инициализация другого множества значением 0010
std::bitset fourMoreBits (2);
std::cout « "fourMoreBits: " « fourMoreBits « std::endl;
std::bitset addResult(fourBits.to_ulong() +
fourMoreBits.to_ulong());
std::cout « "Результат сложения равен " « addResult;
return 0;

}
2. Вызовите функцию f l i p () для любого из объектов класса b i t s e t в приведен­
ном выше примере:
addResult.flip();
std::cout « "Результат применения flip(): "
« addResult « std::endl;

Ответы к занятию 26
Контрольные вопросы
1. Лично я искал бы на www. b o o s t . org. Надеюсь, вы тоже!
2. Нет. Как правило, хорошо разработанный (и правильно выбранный) интеллек­
туальный указатель не замедляет приложение.
3. При внедрении его содержат принадлежащие объекты; в противном случае эту
информацию может содержать совместно используемый объект в динамиче­
ской памяти.
4. Список следует перебирать в обоих направлениях, поэтому он должен быть
двунаправленным.

Упражнения
1. Ошибка в строке o b je c t-> D o S o m e th in g ( ) ; , так как указатель потерял владе­
ние объектом во время предыдущего копирования. Эта строка приведет к сбою
(или к какой-то иной неприятности).

Ответы к занятию 27

745

2. Код может выглядеть следующим образом:
#include
tinclude
using namespace std;
class Fish {
public:
FishO {
cout «

"Конструктор Fish" «

endl;

}
-FishO {
cout «

"Деструктор Fish" «

endl;

}
void Swim() const {cout «

"Fish плавает в воде" «

endl;}

};
class Carp: public Fish {
};
void MakeFishSwim(const unique_ptr & inFish)
inFish->Swim();

{

}
int main() {
uniquej?tr myCarp(new Carp);
MakeFishSwim(myCarp);
return 0;

}
Поскольку здесь нет никакого копирования, при условии, что функция Маке
FishSwimO получает аргумент как ссылку, никакой срезки не происходит.
Кроме того, обратите внимание на синтаксис создания экземпляра переменной
шуСагр.
3.

Класс u n iq u e _ p tr не допускает копирования и присваивания, поскольку и ко­
пирующий конструктор, и копирующий оператор присваивания являются за­
крытыми.

Ответы к занятию 27
Контрольные вопросы
1. Для только записи в файл используйте класс o f stream .
2. Используйте метод c i n . g e t l i n e (), как в листинге 27.7.
3. Нет, поскольку класс s t d : : s t r i n g содержит текстовую информацию, вы може­
те остаться в режиме по умолчанию, которым является текстовый режим (пере­
ход в бинарный режим не нужен).
4. Чтобы проверить успех выполнения метода open ().

746

ПРИЛОЖ ЕНИЕ Д. Ответы

Упражнения
1. Вы открыли файл, но не проверили успех этой операции с помощью метода
is _ o p e n (), прежде чем использовать поток или закрыть его.
2. Вы не можете писать в поток i f stream , который предназначен только для чте­
ния, но не для записи, и следовательно, не поддерживает оператор вывода в
поток « .

Ответы к занятию 28
Контрольные вопросы
1. Класс такой же, как и любой другой, но созданный специально как базовый для
других классов исключений, таких как b a d _ a llo c .
2. Исключение s t d : :b a d _ a llo c .
3. Нет, это плохая идея, так как возможна повторная генерация исключения из-за
нехватки памяти.
4. С помощью того же обработчика c a tc h ( s t d : : ex cep tio n & ex p ), что и для клас­
са b a d _ a llo c .

Упражнения
1. Никогда не генерируйте исключения в деструкторах.
2. Вы забыли обработать исключения (пропустили блок t r y . . . c a tch ).
3. Не выделяйте память в блоке c a tc h ! Тем более такое большое ее количество.
Если учесть, что вам уже не удалось выделить память в блоке tr y , продолжать
попытки выделения не имеет никакого смысла.

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

Предметный указатель
А

R

A SC II 65

R -зн ачени е 107

fill n 569, 577

assert 403

RAM 56

find 432, 568, 571

return 168, 175

f i n d e n d 569

auto 38, 72

fill 569, 577

find first o f 569

RTTI 327, 386

В

f i n d j f 432, 568, 571

bitset 120

С

C++
п р еи м ущ еств а 32
C + + И 2 5 ,3 8 , 71, 74, 151, 186,

275, 450, 639, 680
C + + 1 4 33, 38, 69, 75, 418, 453,
556, 617
C + + 1 7 39, 142, 275, 684
cla ss 230
con st 75, 368
con stexp r 74, 76, 2 4 6 ,2 7 8

S

for each 540, 569, 581

set 496

gen erate 569, 579

s iz e o f 69

gen erate_n 569, 579

siz e o f... 418

lexicograp h ical com p are 569

static 260
static assert 420

lo w er bound 571, 594
m ism atch 569

STL 421, 427, 439

partial_sort 570

string 441

partial_sort_cop y 570

struct 269

partition 5 7 0 ,5 9 2

T

rem ove 570, 5 86

tem p late 406

r e m o v e c o p y 570

r a n d o m s h u ffle 589

this 2 6 6

rem ove co p y i f 570

thread 680

r e m o v e i f 432, 570, 5 86

D

throw 668

d y n a m ic c a s t 327

rep lace 570, 588

ty p e d e f 73

r e p la c e _ if 570, 588

typ en am e 406

reverse 432, 450

E

search 568, 575

ex cep tio n 671

U

ex p licit 256, 264, 350

union 272

F

unique_ptr 637

V

friend 270

vector 95

L -зн ачени е 107

M
m ultiset 496
m utable 560
m utex 682

N
nothrow 217

О
operator 344
override 335

stable_partition 570, 593
stab le_sort 549, 570, 592

final 311, 336

L

search n 5 6 8 ,5 7 5
sort 549, 570, 590

transform 432, 451, 549, 569,

583

virtual 318, 334

unique 549, 570, 590

A

un iqu e co p y 570

А бстракция 237, 283

upper bound 571, 594

А грегац и я 308

и зм ен я ю щ и й 569
н е и зм ен я ю щ и й 568

А д ап тер 599
priority q u eu e 431
q u eu e 431
stack 431
контей н ера 428, 431
А лгори тм
adjacent find 569
binary search 571, 590
co p y 569, 585
cop y_b ack w ard 5 6 9 ,5 8 6

P

co p y i f 585

P O D 69

cou n t 568, 573

private 234, 236

c o u n t j f 568, 573

public 234

equal 569

А р гум ен т 43

Б
Базовы й класс 284
Байт 695
Б езоп асн ость тип ов 383
Б инарны й
оп ер атор 353
предикат 487
Б итовое м н ож ест в о 120
Бит 695
Блок 107

748

|

Предметный указатель

В

И н кр ем ент 109

В ектор 4 2 8 ,4 5 8

И нструкция 106

unordered set 508
vector 458

вставка 460, 461

d o ...w h ile 146

разм ер 468

д о с т у п 464

for 148

ем кость 468

g o to 143

вставка 460, 461
уд ал ен и е 466

и нициализация списком 461

i f 130
влож енная 134

очистка 472

sw itch 138

разм ер 468
со зд а н и е эк зем п ляра 458

w h ile 145
итеративная 146

у д а л ен и е 466
В енгер ск ая нотация 62

составная 107, 133

В заи м облок и ров к а 682

И нтегрированная ср ед а
р азработки 34

Виртуальная ф ункция 318
В и р туаль н ое н асл ед о в а н и е 332

И нтеллектуальны й
указатель 628

В ы полним ы й файл 33

И ск лю чен и е 664

Г

И тератор 150, 431
ввода 431

Г л убокое к опирование 631

вы вода 431

д

двунаправленны й 432

Д вухстор онн я я оч ер едь 470

однон ап равлен ны й 431

Д ек р ем ен т 109
Д ек 470

п р оизвол ьного д о ст у п а 432
И терация 146

очистка 472
Д естр у к т о р 2 46
б а зо в о го класса 323

К
К ласс 230

виртуальны й 320

b asic_strin g 453

п орядок вы зовов 300

b itset 616

Д и рек ти ва п р еп р о ц ессо р а 42,

deq u e 470

3 96
# d efin e 8 0 ,3 9 6
# e n d if 399
# if n d e f 399
# in clu d e 42, 399
Д р у г 270

ex cep tio n 671

E

forward list 475
fstream 652
in itialize list 461

инициализация списком 461
очистка 472
со зд а н и е экзем пляра 458
v ecto r< b o o l> 621
w strin g 440, 453
абстрактны й 328
базовы й 284
д р у г 270
некопируем ы й 258, 366
н есоздав аем ы й в стеке 262
производн ы й 284
член 231
ш аблонны й 409
спец иализаци я 413
К лю чевое сл ово 80
auto 72, 435, 450
cla ss 230
con st 75, 368
con stexp r 7 6 ,2 4 6
enum 78
ex p licit 350
final 311, 336
friend 270
in line 183, 184
m utable 560
operator 344

istream 643

override 335
private 234, 303

list 475

protected 288, 305

m ap 514

public 234
static 260
struct 269

m ultim ap 514

Е м кость 468

д о с т у п 464
ем кость 468

m ultiset 496
m u tex 682

3

tem p late 406

ostream 643

Закры тое н асл едов ан и е 303

priority queu e 608

З а щ и щ ен н о е н асл едов ан и е 305

this 2 66
throw 668

queu e 605

ty p e d e f 73

Знак 65

set 496

typ en am e 406
union 272

stack 601

И
И ден ти ф ик ац ия ти п а врем ени
вы полнения 327, 386
И ниц и али зац ия 57
агрегатная 275
сп иском 71
цикла for 151
И нкапсуляция 231, 283

string 439, 440
stringstream 658
thread 680
tup le 418
unique_ptr 637
unordered_m ap 529
u n o rd ered m u ltim a p 530
u n o r d e r e d m u ltise t 508

virtual 318, 334
К ом м ентарий 46
м н огострочн ы й 52
одн остр оч н ы й 52
К омпилятор 34
К омпиляция 34
К ом позиция 308
К ом поновщ ик 34

Предметный указатель
К онстанта 5 5 , 74
литеральная 75, 376
К онструктор 237
копирую щ ий 252
п ер ем ещ аю щ ий 2 5 8 ,3 7 1
порядок вы зовов 300
по ум ол ч ан и ю 240, 244
п р еобр азую щ и й 264
К онтейнер 428
deque 428
fo r w a r d jis t 428
list 428
m ap 429
m ultim ap 429
m ultiset 429
set 429
unordered_m ap 429
u n o rd ered m u ltim a p 429
unordered_m ultiset 429
unordered_set 429
v ector 428
адаптер 4 2 8 ,4 3 1
адаптивны й 599
ассоциативны й 429
вы бор 435
п оследовательны й 428
К опирование
глубокое 2 5 2 , 2 5 5 , 363, 631
д естр ук ти вн ое 634
п о в ер х н о ст н о е 250
при зап и си 633
К ортеж 418

сокры тие в п р ои зв одн ом
классе 298
М н огоп оточ н ость 679
М н о ж еств ен н о е
н асл едов ан и е 286, 309
М н ож еств о 496
би тов 616
М одиф икатор д о с т у п а 288, 305
private 303
p rotected 289, 305
М ул ьти м нож ество 496
М ул ьти отобр аж ен и е 514
М ью текс 682

Н
Н асл ед ован и е 283
виртуальное 332
закры тое 303
за щ и щ ен н ое 305
м н о ж ест в ен н о е 286, 309
откры тое 284

О
О бласть в и д и м ости 59
О бъ еди н ен и е 2 7 2
О бъектны й файл 34
О бъект 231
функциональный 37 0 ,5 2 8 ,5 3 7
О бъ явлени е 4 8 ,1 6 7
u sin g 45
ф ункции 48

Л

О перативная память 56

Л ям бда-вы раж ен и е 3 8 ,1 8 6 ,

О ператор 107, 344

553, 554
m utable 560
синтаксис 560

% 108

сп исок захвата 558

О

& 1 1 9 ,1 9 3
& & 114

369

* 1 0 8 ,1 9 7 ,3 5 1

М

+ 1 0 8 ,3 5 4

М акрос 396

+ + 109

assert 403

+ = 357

м ассивов 94

1 0 8 ,3 5 4
— 109
- = 357
. 232
! 114
!= 1 11,359

многом ерны й 93

<

символьны й 97

«

4 3 ,1 2 1 , 642

=

749

112

5 0 ,1 2 1 , 642
> 1 1 2 ,2 3 2 ,3 5 1
? 141
| 119

»

I U5

/ 108
\ 238, 295, 376
П 366
л 115,119
~ 119
break 1 3 8 ,1 5 3
co n st_ ca st 389
con tinu e 153
d elete 202
d yn am ic_cast 385
n ew 201, 215
и скл ю чен ие 215
reinterpret_cast 388
return 1 6 8 ,1 7 5
s iz e o f 6 9 ,1 2 4 ,1 9 9 , 267
siz e o f... 418
static_cast 384
throw 668
бинарны й 353
бол ьш е или равн о 112
бол ьш е 112
вы бора поля 232
вы бора чл ена 351
вы вода в поток 43, 642
вычитания
с п рисваиванием 357
вычитания 108
д ек р ем ен т а 109
дел ен и я п о м од ул ю 108
дел ен и я 108
извлечения и з потока 642
и ндек сац ии 3 66
и нк р ем ен та 109
к опи рую щ его
присваивания 363
к освен н ого обр ащ ен и я 198
м еньш е или р авно 112
м еньш е 112
н еп ерегр уж аем ы й 378
неравен ства 111, 359
п олучен и я а д р еса 194
постф и к сн ы й 109
п р еобр азован и я 348
преф иксны й 109
п риведен ия 381
п р иор и тет 126, 702

750

|

Предметный указатель

присваивания 107
п ер ем ещ аю щ и й 371

П ользовательский литерал 3 76

со ст а в н о й 122
присваивания

256

в и д и м о ст и 2 3 8 ,2 9 5
р азы м енования 197, 351

cou t 644

в сти ле С 97
конкатенация 445
Строковы й литерал 44, 74

м анипулятор 643

С труктура 269

ф айловы й 652

С четчик ссы лок 633

р еж им откры тия 653

сд в и га 121

П редикат 369, 538
бинарны й 487, 538, 563

п рисваиванием 357

Т
Т аблица виртуальны х
ф ункций 325

ун арны й 543, 557

сравнения 111

П р еп р о ц есс о р 42, 3 96

Тернарны й усл овн ы й
оп ератор 141

д ир ек ти ва 42, 3 96

тернарны й усл овн ы й 141
точки 232

w strin g 453

си н хр он и зац и я 681

разреш ен и я обл асти

сум м и рован ия 108

string 441

cin 648
вы полнения 679

равенства 1 1 1 ,359

сл ож ен и я с

С трока 440

П оток 43, 642

П р и в ед ен и е

Тип

указателя 232

в осход я щ ее 384

b ool 64

у м н ож ен и я 108

н и сход я щ ее 384

char 64

ун арны й 345

П р и ор и тет оп ер атор ов 126

d ou b le 69

ф ун кц и и 369

П р обл ем а р о м б а 334

float 69

П роизводн ы й класс 284

int 66

П р ост р ан ст в о и м ен 43, 44

lo n g 66

О п р ед ел ен и е 167
ф ун кц и и 48

lon g lo n g 66

std 45

О п тим изация 184
О ткры тое н асл ед о в а н и е 284

П р осты е стар ы е д ан н ы е 69

short 66

О тладка 34

П роцедурное

string 100

является 2 8 4 ,3 0 3

u n sign ed int 66

п р огр ам м ир ов ани е 229

О т н о ш ен и е
со д ер ж и т 303

u n sign ed lon g 66

Р

u n sign ed lon g lon g 66

О т о б р аж ен и е 514

Р азм ер 69, 468

u n sign ed short 66

О ч ер едь 600

Р екурсия 173

v o id 168

с п р иор и тетам и 608
О ш и бка врем ен и
вы полнения 39

б е зо п а сн о ст ь 383

С

п р еобр азован и е 264

С бор к а м у с о р а 212

п р и в ед ен и е 381
ф ун дам ен тальн ы й 63

С вязанны й сп и сок 429, 476
С ем аф ор 682

Л

С им вол заверш аю щ и й

П амять
ди н ам и ч еск ая 201, 204

нулевой 97

У
У зел 476
Указатель 192

оперативная 5 6

С инглтон 2 5 9 ,2 6 2

утечк а 202, 212

С л ож н ость 430

this 2 66

С о ст оя н и е гонки 682

ариф м етика 204

глобальная 61, 62

С п ец иал изаци я 288

висячий 214

ини ц иали заци я 57

С п и сок 429, 476

и м ассив 209

П ер ем ен н ая 55

локальная 60

двухсвязн ы й 475

обл асть в и д и м о сти 59

захвата 558

состоя н и я 558

и ни ц иал и заци и 244

П ер ем ещ а ю щ и й
конструктор 371
П ер еп о л н ен и е б у ф ер а 101
П ер еп о л н ен и е стек а 173
П ер еч и сл ен и е 78
П ол и м ор ф и зм 283, 316
п одти п ов 3 16
реализац и я 324, 326

одн освя зн ы й 475
С р езк а 309, 631

интеллектуальны й 351, 628
с о списком ссы лок 634
п ередача в ф ун кц и ю 208
У нарный оп ер атор 345
У течка памяти 2 0 2 ,2 1 2

С сы лка 218
константная 221
С тандартная би бли отек а
ш абл онов 421, 427, 439
Стек 182, 600

Ф
Ф айл
вы полним ы й 33
заголовочны й 399
объектны й 34

Предметный указатель
Ф унктор 370 , 5 28 , 5 3 7
адаптивны й 538
Ф ункция 47 , / 6 5

прототи п / 6 7

Ш

реал изац и я 168

Ш аблон 405

р екурсивная 173

вари ади ческ ий 417

m ain() 4 3

унарная 538

и нстан ц и р ов ан и е 410

аргум ент ¥3 , / 6 8

ч и сто виртуальная 328
ш аблонная 407

парам етр ти п а 406

бинарная 538

п о ум ол ч ан и ю 411

виртуальная 318
таблица 325
возвращ аем ое зн ач ен ие 44
встраиваемая 183

с п ер ем ен н ы м чи слом

X

коллизия 529

лям бда. См. Л ям бда-

о п р едел ен и е / 6 7 ,1 6 8
параметр 168
м ассив 178
входн ой 222
необязательны й 172

ц
Ц иклическая зав и си м ость 634
Цикл 142
d o ...w h ile 146
for 148
для д и ап азон а 151
w h ile 145

передача по ссылке 180,221

беск он ечны й 154

п о ум ол ч ан и ю 171

влож енны й 157

п ерегрузка / 7 7
передача указателя 208

сп ец и ал и зац и я 413

Х еш и р ов ан и е 5 0 7 ,5 2 8

вызов / 6 7 ,1 6 8
вы раж ение
объявление / 6 7

парам етров 417

Х еш -т абл и ц а 528

Я
Язы к п рограм м ирования
С 32
C# 212
C + + . См. C + +
Java 212
и нтер п р етир уем ы й 39

751

ЯЗЫК ПРОГРАММИРОВАНИЯ C++
ЛЕКЦИИ И УПРАЖНЕНИЯ
6-Е ИЗДАНИЕ
Стивен Прэта

w w w .w illia m s p u b lis h in g .c o m

ISBN 978-5-8459-2048-5

Книга представляет собой
тщательно проверенный,
качественно составленный
полноценный учебник
по одной из ключевых
тем для программистов
и разработчиков. Эта
классическая работа по
вычислительной технике
обучает принципам
программирования, среди
которых структурированный
код и нисходящее
проектирование,
а также использованию
классов, наследования,
шаблонов, исключений,
лямбда-выражений,
интеллектуальных указателей
и семантики переноса.
Автор и преподаватель Стивен
Прата создал поучительное,
ясное и строгое введение в C++.
Фундаментальные концепции
программирования излагаются
вместе с подробными
сведениями о языке C++.
М ножество коротких
практических примеров
иллюстрируют одну или две
концепции за раз, стимулируя
читателей осваивать новые
темы за счет непосредственной
их проверки на практике.
в продаж е