/
Author: Крупник А.Б.
Tags: компьютерные технологии программирование самоучитель программное обеспечение языки программирования издательство питер
ISBN: 5-469-00468-6
Year: 2005
Text
СЕРИЯ
САМОУЧИТЕЛЬ
ПИТЕР
ББК32.973-018.1я7
УДК 681.3.06@75)
К84
Крупник А. Б.
К84 Самоучитель C++. —- СПб.: Питер, 2005. — 252 с: ил.
ISBN 5-469-00468-6
Написанная простым, понятным языком, эта книга поможет вам сделать первые шаги
в программировании на C++. Изложение строится на коротких примерах, помогающих читателю
освоить основные конструкции C++ и главные принципы объектно-ориентированного
программирования. Эта книга не ставит своей целью сказать «все» о языке C++, она сосредоточена на
«самом главном». Прочитав ее, вы сможете легко ориентироваться в мире C++, а также изучить
другие объектно-ориентированные языки, такие как Java и С#. Книга может служить введением
в программирование и рассчитана на всех интересующихся этой темой.
ББК 32.973-018.1я7
УДК 681.3.06@75)
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было
форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги.
ISBN 5-469-00468-6
© ЗАО Издательский дом «Питер», 2005
Краткое содержание
Введение и
Глава 1. Объекты 13
Глава 2. Операторы и циклы зз
Глава 3. Строки и контейнеры 43
Глава 4. Функции, указатели и ссылки 63
Глава 5. Разработка программы 92
Глава 6. Примитивные объекты юо
Глава 7. «Я» и мир объектов 112
Глава 8. Рождение и смерть объектов 127
Глава 9. Операторные функции 145
Глава 10. Наследование 172
Глава 11. Макросы и шаблоны 192
Глава 12. Ввод-вывод 200
Глава 13. Работа над ошибками 213
Заключение 222
Приложение А. Приоритеты и порядок выполнения операторов 225
Приложение Б. Устройство целочисленных переменных 227
Приложение В. Стандартная библиотека 232
Литература 248
Алфавитный указатель 249
Содержание
Введение и
От издательства 12
Глава 1. Объекты 13
«Я» и мир объектов 13
Элементарные частицы 14
Действия 15
Операторы 16
Метаморфозы 17
COUT 18
Порядок выполнения и приоритет 19
Компилятор 20
GCC 23
Sizeof, или «Размер имеет значение» 25
Знак 27
Unsigned и signed char 29
Строки 30
Ссылки 31
Глава 2. Операторы и циклы зз
Условности 33
Логические операторы 35
Умные операторы 36
Унарные и бинарные операторы 38
Содержание 7
Глава 3. Строки и контейнеры 43
Приговор 43
Массивы 46
Контейнеры 48
Итераторы и алгоритмы 49
Питание программ 52
Файлы 53
Анаграммы 55
Стиль 59
Глава 4. Функции, указатели и ссылки 63
Сделай функцию сам 63
Функции-тезки 65
Параллельные миры 66
Рекурсия, или «Раз, два, три» 68
Указатели 73
Указатели и массивы 75
Массив указателей 79
Указатель на указатель 83
Указатель на массив 84
Указатель на функцию 89
Глава 5. Разработка программы 92
Алгоритм 92
Первая версия 93
Промежуточные варианты 94
Добавление функций 96
Глава 6. Примитивные объекты юо
Enum 100
Записи 102
Typedef 105
Сложные объявления 108
Глава 7. «Я» и мир объектов ш
Часы 112
Часы с «кнопочками» 116
8 Содержание
Карты 119
Константы 124
Глава 8. Рождение и смерть объектов 127
Богатые и бедные классы 127
Анаграммы-2 129
Видимость 132
Пространства имен 134
Static 136
Деструкторы 137
Конструктор копирования 141
Глава 9. Операторные функции 145
Доступ к массиву 145
Равенство 148
Указатель This 149
Родственники и друзья 152
Ввод-вывод 157
Функции-объекты 159
Оператор ! 161
Увеличение 162
Две скобки [][] 165
Постоянные операторы и функции 166
Тайные преобразования 169
Глава 10. Наследование 172
Составные объекты 172
Право наследования 175
Изменчивость и отбор 177
Идеи и вещи 182
Виртуальный деструктор 186
Пример: снова часы 186
Глава 11. Макросы и шаблоны 192
Макросы 192
Inline 193
Шаблоны классов 196
Содержание 9
Глава 12. Ввод-вывод 200
Потоки ввода-вывода 200
Функции ввода-вывода 202
Манипуляторы 203
Файлы 205
Непоследовательные файлы 209
Глава 13. Работа над ошибками 213
Assert и Throw 213
Путь ошибки 215
Провалы памяти 218
Заключение 222
Приложение А. Приоритеты и порядок выполнения
операторов 225
Приложение Б. Устройство целочисленных переменных 227
Приложение В. Стандартная библиотека 232
Стандартные контейнеры 232
Последовательные контейнеры 232
Ассоциативные контейнеры 233
Адаптеры 233
Очередь 233
Стек 234
Типы объектов 234
Конструкторы и деструкторы 234
Собственные функции 235
Собственные функции, общие для всех контейнеров 235
Другие собственные функции 236
Собственные функции последовательных контейнеров 237
Собственные функции ассоциативных контейнеров 237
Алгоритмы 238
Алгоритмы, не меняющие контейнеры 238
Модифицирующие алгоритмы 240
10 Содержание
Удаляющие алгоритмы 242
Меняющие алгоритмы 242
Сортировка 243
Алгоритмы для отсортированных контейнеров 244
Численные алгоритмы 246
Литература 248
Алфавитный указатель 249
Введение
Итак, вы держите в руках девятисотую, а может быть, девятитысячную книгу
по C++. Когда же все это кончится? Наверное, никогда. Ведь C++ постоянно
развивается, и книги, написанные в конце 80-х — начале 90-х годов прошлого века,
в эпоху «раннего C++», сейчас безнадежно устарели. Изменился це только сам
язык, но и взгляд на то, как его следует изучать. Ранние книжки повторяли
мучительный путь обучения их авторов: сначала в них рассказывалось о языке С,
затем читатель знакомился с описанием объектов — классами и пытался переломить
себя, перейдя от функций и данных, разбросанных по исходному тексту
программы, к объектам, состоящим из тех же функций и данных, но уже объединенных
в единое неразрушимое целое.
Постепенно знатоки C++ пришли к выводу, что это вовсе не «улучшенный С»,
а совсем другой язык, который требует, прежде всего, иного взгляда на мир, иной
манеры мыслить. Вот почему эта книга начинается с описания привычных для нас
объектов (таких как телевизор) и способов взаимодействия с ними.
А затем мы сразу же перейдем к стандартным объектам языка C++. Мы
познакомимся с контейнерами, способными хранить и обрабатывать однотипные объекты
C++, такие как строки и целые числа. А дальше мы создадим собственные объекты
и научим их взаимодействовать друг с другом.
Эта книга построена по принципу движения от простого к сложному. Уже на
первых ее страницах вы встретите примеры работающих программ, поведение
которых сможете самостоятельно менять. Постепенно мы перейдем от «азов» к
довольно сложным примерам, которые позволят заглянуть в мир «настоящего» C++,
куда вряд ли мог попасть даже создатель C++ Бьярн Строуструп. Ведь понимание
того, что же такое C++, добыто нелегким трудом сотен людей, создавших
шаблоны множества полезных объектов и стандартную библиотеку C++.
Из всего сказанного становится ясно, что C++ — очень сложный язык. И это
действительно так. В древности, когда время текло медленно, его изучали бы всю
жизнь в удаленных монастырях. Вам же предлагается другой путь: понять самое
главное, основные идеи, что позволит не только создать довольно сложные
программы, но и свободно ориентироваться в огромном мире C++, куда входят и
другие объектно-ориентированные языки, такие как Java и С#.
Александр Крупник, krupnik@sandy.ru
Н. Новгород, 1 июля 2004 г.
12
От издательства
От издательства
Ваши замечания, предложения и вопросы отправляйте по адресу электронной
почты comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Все исходные тексты, приведенные в книге, вы можете найти по адресу http://www.
piter.com/download.
Подробную информацию о наших книгах вы найдете на веб-сайте издательства:
http://www.piter.com.
Глава 1
Объекты
...Все наше достоинство заключается в
мысли. Вот чем должны мы возвышаться, а не
пространством и продолжительностью,
которых нам не наполнить. Будем же
стараться хорошо мыслить: вот начало
нравственности.
БлезПаскаль
«Я» и мир объектов
Чтобы хорошо программировать на C++, недостаточно знания самого языка,
нужен еще правильный взгляд на мир, состоящий из различных объектов,
взаимодействующих с нами и друг с другом. Впрочем, взгляд на мир вполне можно
заменить взглядом на обычный телевизор.
Телевизор — это отдельный объект, который мы выделяем в окружающем мире,
никогда не путая со столом или табуреткой. Телевизор управляется с помощью
специального пульта. Нажимая кнопки и смотря на экран, обычный человек не
интересуется тем, что у телевизора внутри.
Иными словами, объект под названием «телевизор» обладает интерфейсом, то
есть набором команд, которые им управляют. Нажимая на пульте кнопку под
номером 1, мы командуем телевизору переключиться на первый канал, нажимая
другую кнопку — велим увеличить громкость. Третья кнопка вызовет меню и т. д.
Интерфейс необходим любому сложному объекту, потому что просто-таки
нелепо для переключения на другой канал открывать корпус телевизора и что-то там
подкручивать. Это, конечно, не значит, что корпус совсем не должен
открываться. Иногда в телевизоре нужно установить новое устройство (например, декодер)
или заменить испорченную деталь. Все это делает человек, понимающий в
телевизорах, все же остальные только нажимают кнопки пульта и не лезут телевизору
«в душу».
14
Глава 1. Объекты
Заметим, что пульт — совершенно отдельный объект со своим собственным
интерфейсом (кнопками). Нажимая кнопку, мы велим пульту отправить телевизору
нужную команду, а как пульт это делает, нам знать не обязательно. Пульт нельзя
считать частью телевизора, это отдельный объект, знающий интерфейс телевизора
и потому могущий им управлять. Вместо пульта можно использовать наладонный
компьютер со специальной программой, которая, как и пульт, знает интерфейс
телевизора данной марки.
Как и телевизор, многие окружающие нас объекты имеют собственный, иногда
довольно сложный интерфейс. Компьютер, цифровая камера, видеомагнитофон,
CD-плейер, СВЧ-печь и даже обыкновенный электрический чайник
управляются кнопками или своими пультами. Причем сложные предметы часто состоят из
более простых, но имеющих свои интерфейсы. Так, например, цифровая камера
обязательно содержит процессор, который управляется комбинациями
напряжений на его контактах. Интерфейс процессора используется в программировании
на языке ассемблера.
Подобно реальному миру, язык C++ создает свой собственный мир из различных
объектов, обладающих внутренним устройством и интерфейсом. Внутренности
объекта не должны волновать простого программиста, который пользуется только
интерфейсом. Сам объект может быть в миллион раз сложнее телевизора, но если
интерфейс его прост, управлять объектом сможет каждый. При этом объект
может и не существовать в тот момент, когда пишется программа. Достаточно знать
его интерфейс. То есть в программировании на C++ возможно разделение труда:
квалифицированные программисты-архитекторы решат, из каких объектов будет
состоять программа, и определят их интерфейс, другие программисты начнут
писать программу, пользуясь этим интерфейсом, третьи будут в это время создавать
сами объекты. Так, разделяя и властвуя, можно построить огромные программы,
которые окажутся гораздо сложнее любых созданных человеком материальных
объектов: самолетов, атомных реакторов и даже самих компьютеров, на которых
эти программы будут отлаживаться и выполняться.
Элементарные частицы
Но не все объекты в «жизни» и в C++ обладают собственным интерфейсом. Какой
интерфейс может быть у табуретки? Следовательно, есть элементарные объекты,
из которых строятся все остальные. В C++ таких объектов несколько. Конечно,
в компьютерной программе не могут жить стулья и чайники. Чтобы построить
модели реальных объектов, C++ использует числа, хранящиеся в переменных.
Создавать переменные в C++ гораздо проще, чем чайники и табуретки в
реальной жизни. Для этого нужно придумать имя переменной и задать ее тип. Записав
в программе на C++
int hight.
мы создаем переменную по имени hight типа int. Здесь int — одно из слов языка
C++, говорящее о том, что переменная hight способна хранить только целые числа,
Действия
15
а точка с запятой обозначает конец инструкции — элементарного действия, из
которого и состоит программа на C++.
Написав в программе инструкции:
int hight:
hight-5;
мы создаем целочисленную переменную hight и затем посылаем в нее число 5.
Можно, конечно, попытаться послать в переменную hight и нецелое число (часто
называемое числом с плавающей точкой):
int hight:
hight=150.8;
но поскольку hight — целочисленная переменная, в ней окажется только «целая»
часть числа 150, а дробная часть 0.8 будет отброшена.
Чтобы число с плавающей точкой было в целости и сохранности, его следует
хранить в специальной переменной типа double
double dhight:
dhight=150.8:
Как и int, слово double принадлежит языку C++ и, помещенное слева от имени
переменной, показывает, что переменная относится к типу double, то есть способна
хранить дробные числа.
Действия
Лошадь сказала, взглянув на верблюда:
«Какая гигантская лошадь-ублюдок».
Верблюд же вскричал: «Да лошадь разве ты?!
Ты просто-напросто — верблюд недоразвитый».
И знал лишь бог седобородый,
что эти животные — разной породы.
В. Маяковский. Стихи о разнице вкусов
Переменные dhight типа double и hight типа int в наших примерах коренным
образом отличаются, это переменные «разной породы». Тем не менее и те и
другие переменные могут участвовать в арифметических (и не только) действиях.
Две целочисленные переменные можно складывать (вычитать, умножать,
делить):
int al = 2:
int а2 = 3:
int аЗ:
a3=al + а2:
16
Глава 1. Объекты
И то же самое допускают переменные с плавающей точкой:
double dl = 2 3
double d2 - 2 5.
double d3.
d3 = dl + d2
После описанных действий в переменной аЗ окажется число 5, в переменной d3 —
число 4.8, а переменные al, a2, dl, d2 останутся прежними, участие в
арифметических действиях им не вредит.
Из наших последних примеров видно, что C++ позволяет объявить переменные
и одновременно присвоить им начальные значения. Объявление double dl = 2.3;
не только создает новую переменную с плавающей точкой, но и присваивает ей
значение 2.3.
Операторы
Различные действия с переменными становятся возможны в C++ благодаря
операторам. Оператор сложения +, помещенный между двумя переменными al и а2,
приведет к тому, что будет вычислена их сумма, а оператор присваивания =
перешлет результат сложения в переменную аЗ:
аЗ = al + a2.
Если оператор сложения заменить оператором вычитания -, то будет
вычислена разность al - а2. Оператор умножения * найдет произведение al * a2, а
оператор деления / — частное от деления al на а2. Последний случай любопытнее всех
остальных, поэтому попробуем рассмотреть его чуть подробнее:
int al = 2.
int a2 - 3
int аЗ.
аЗ - al / а2.
Мы привыкли к тому, что частное от деления 2 на 3 — это бесконечная дробь
0.66666..., но в C++ результат зависит от типа переменных. Результат деления
одной целочисленной переменной на другую тоже должен быть целым числом,
значит, вместо 0.6666 должен получиться 0. Так и происходит, независимо от того,
какая переменная стоит слева от оператора присваивания.
double d3.
int al - 2.
int a2 = 3.
d3 = al / a2.
В этом примере переменная d3 окажется равной 0.00000..., потому что результат
целочисленного деления будет нулевым, но переменные типа double и int — это
«животные разной породы», и целочисленный нуль отличается от нуля с
плавающей точкой. Поэтому «целый» 0 преобразуется к типу double и затем только
происходит присваивание. А в результате переменная d3 станет равной 0.00000...
Метаморфозы
17
Заметим, что уже такие простые переменные, как int и double, обладают
свойствами «настоящих» объектов. Не зная, как они устроены, мы все же можем
применять к ним одни и те же операторы и получать осмысленные результаты.
Метаморфозы
Можно, например, складывать переменные int и double:
int i = 3;
double d = 2.5;
d - d + i;
В последнем примере складываются «лошади» и «верблюды», то есть совсем
по-разному устроенные переменные i и d. В таких случаях C++ старается
привести переменные к одному типу так, чтобы действие было максимально безопасным.
В нашем случае переменная int более «узка», у нее меньше значений, чем у
переменной типа double, поэтому целочисленная переменная приводится к типу double,
и результат сложения присваивается переменной d, которая станет равной сумме
2.5 и 3, то есть 5.5.
Запись d = d + i; нелепа с точки зрения алгебры, но ее нужно понимать не как
равенство, а как команду присвоить результат сложения d + i переменной d. Чтобы
яснее показать, что переменная d увеличивается на величину i, в C++ часто пишут
вместо d = d + i; просто d += i; Такая же сокращенная запись годится и для других
операторов. Например, вместо d = d - i; лучше написать d -= i;, а запись d *= i;
короче и понятней, чем d = d * i;
Приведение переменных к «нужному» типу происходит автоматически, в
соответствии с внутренними правилами языка C++. Но иногда приходится явно
указать, какое произвести преобразование, чтобы в результате получилось то, что
задумано.
Мы уже знаем, что целочисленное деление 2 на 3
double d3;
int al = 2;
int a2 = 3:
d3 - al / a2;
приводит к тому, что переменная d3 окажется равной нулю, хоть она и типа double.
Чтобы добиться более естественного результата, нужно явно преобразовать одну
из целочисленных переменных к типу double, тогда вторая переменная будет уже
«подтянута» к типу doubl e автоматически и в результате получится число 0,66666...,
а не ноль. Для преобразований переменных в C++ есть специальный оператор
static_cast<™n>(). Применив его для преобразования делимого к типу double
int al - 2;
int a2 « 3;
d3 = static cast<double>(al) / a2;
получим ожидаемый результат d3 = 0.66666...
18
Глава 1. Объекты
cout
Объекты типа i nt и doubl e относятся к числу элементарных и служат строительным
материалом для «настоящих» объектов, таких как cout — объект, позволяющий
вывести на экран значения переменных. Делается это с помощью оператора «:
int i - 2. j - 3:
i +- j:
cout « j;
Инструкция cout« j; выведет на экран сумму 2 + 3, то есть целое число 5. Создатели
C++ нарочно выбрали для вывода на экран оператор «, как бы давая понять, что
переменная направляется объекту cout, который принимает ее, обрабатывает и
затем показывает на экране. Но « — не просто стрелка, это оператор, он не
направляет объект (в нашем случае — переменную j), а указывает, какое действие
произвести между cout и j. Результат этого действия — выведенное на экран значение j.
Так что понятная на первый взгляд инструкция cout« j; выглядит, если подумать,
довольно странно, все равно что сумма двух чисел, отправленная в никуда:
int 11 - 2;
int i2 - 3;
il + 12:
Но в C++ такая запись верна, часто встречается, и нам стоит постепенно к ней
привыкать.
Посмотрим, например, как выводятся на экран числа с плавающей точкой.
Казалось бы, обращаться с ними нужно совсем по-другому, но операторы в C++
необычайно умны, и вывод на экран переменной с плавающей точкой d
double d;
cout « d;
будет выглядеть точно так же, как и для целочисленной переменной. При этом,
конечно, объект cout обойдется с переменной d совсем не так, как с целочисленной
переменной i, но все это произойдет внутри объекта, а для нас, простых
пользователей, ничего не изменится, мы просто увидим нужное число на экране.
Как и всякий «настоящий» объект, cout имеет собственный интерфейс,
воспользоваться которым можно с помощью собственных функций, «пристегиваемых»
точкой к имени объекта. Чтобы, например, установить точность вывода чисел с
плавающей точкой, нужно воспользоваться собственной функцией precision. Строка
программы
cout.precisionE);
задает число знаков после запятой, так что фрагмент программы
double d - 0.1234567;
cout.precisionE);
cout « d « endl;
Порядок выполнения и приоритет
19
выведет на экран число 0.12346. Последняя цифра 6 здесь получилась из-за
округления. В этом примере нам встретился и другой способ управления объектом
cout — с помощью так называемых манипуляторов. Инструкция
cout « d « end!:
показывает, что вывод на экран можно организовать «по цепочке»: сначала
выводится переменная d, а затем объекту посылается специальный знак (манипулятор
end!), который велит объекту cout перевести строку на экране, и следующая
переменная будет показана уже с новой строки. Например, фрагмент программы:
int i - 2;
double d - 1.2345;
cout « i « endl « d:
сначала покажет на экране число 2, а затем, на следующей строке — число 1.2345:
2
1.2345
Порядок выполнения и приоритет
Одна инструкция C++ может содержать несколько операторов. Например, в
инструкции
cout « i « endl « d:
их три и все они одинаковы. Какой же оператор « выполнится первым? Правила
C++ говорят нам, что операторы « выполняются слева направо. То есть, сначала
на экране будет показано содержимое переменной i, затем программа переведет
строку и, наконец, выведет на экран содержимое переменной d1.
Кроме порядка выполнения (слева направо или справа налево) у каждого
оператора C++ есть еще приоритет, то есть одни операторы имеют большую силу, чем
другие. Например, в инструкции
cout « а + b;
оператор сложения имеет больший приоритет, чем оператор вывода. Поэтому
сначала будут суммированы переменные а и Ь, а лишь затем программа покажет их
сумму на экране.
Иногда естественный приоритет операторов нас не устраивает и приходится
менять очередность действий с помощью скобок. Например, в инструкции
cout « i + j * k;
наибольшим приоритетом обладает оператор умножения *. Второй по
значимости — оператор сложения и, наконец, самый бесправный — оператор вывода.
1 Результатом действия cout«i, очевидно должен быть объект типа cout, иначе некуда будет вывести
манипулятор endl. To есть, каждый вывод на экран порождает объект cout, который возникает слева от
очередной переменной или манипулятора, чтобы можно было вывести их на экран.
20
Глава 1. Объекты
То есть инструкция сначала перемножит j и к, затем к их произведению
прибавит i и результат покажет на экране. Если, скажем, i = 2, j = 3, а к = 5, то на экран
будет выведено число 2 + 3x5=17, но если по смыслу задачи сначала нужно
сложить i и j, а сумму умножить на к, то в предыдущую инструкцию нужно добавить
скобки, которые изменят «естественный» порядок действий
cout « (i + j) * k;
и тогда на экране появится уже число B + 3)х5 = 25.
В языке C++ много разных операторов, с которыми мы будем знакомиться
постепенно. Полную таблицу приоритетов и порядков выполнения операторов C++
можно найти в приложении А.
Компилятор
В предыдущих разделах мы, казалось бы, научились писать простейшие
программы на C++, способные выполнить элементарные арифметические вычисления
и показать их результат на экране. Но чтобы эти программы стали понятны не
только нам, но и компьютеру, их нужно соответствующим образом подготовить.
Программа, написанная на языке C++, предназначена скорее человеку, чем
компьютеру. Мы читаем ее инструкции и понимаем, что она делает. Но компьютер не умеет
читать и тем более понимать. Он может только выполнять команды процессора —
специальной микросхемы, которая управляет его работой. Процессор
взаимодействует с другой важнейшей частью компьютера — памятью, где хранятся данные.
Поэтому текст, написанный на языке C++, прежде чем он превратится в
программу, исполняемую компьютером, нужно пропустить через другую программу,
называемую компилятором. Компилятор читает текст на языке C++ и превращает
инструкции C++ в последовательность команд процессора, а также выделяет
память компьютера для хранения данных программы.
Инструкция
int i:
не станет командой процессора, увидев ее, компилятор просто выделит память
для переменной i. А вот инструкция
int a:
cout « a:
будет превращена во множество команд процессора, потому что операция вывода
даже простого объекта на экран проста только на бумаге, процессору же придется
прочитать число из памяти компьютера, преобразовать его в понятные человеку
символы (не забудьте, что числа в памяти компьютера закодированы!) и затем
направить эти символы на экран монитора, что тоже очень не просто. Преимущество
языка C++ (его, как и Паскаль, С, Java, Basic, называют языком высокого
уровня) как раз в том и состоит, что человеку, пишущему программу, совсем не нужно
знать, как устроен процессор или монитор компьютера. Записав cout «а;, про-
Компилятор
21
граммист перекладывает всю монотонную, утомительную работу на компилятор,
а сам может сосредоточиться на творческой части программы.
Но чтобы компилятор смог превратить программу в последовательность команд
процессора, одних инструкций на C++ недостаточно. Нужно еще выполнить ряд
условностей, а также дать компилятору ряд указаний, чтобы он смог правильно
перевести программу на язык компьютера. Поэтому первая наша программа,
которую действительно можно скомпилировать и затем выполнить на компьютере,
будет выглядеть чуть сложнее.
Листинг 1.1. Первое слово программы
#include <iostream>
using namespace std;
int mainO {
cout « "Мама!" « endl;
}
Самая понятная нам строчка в листинге 1.1 выводит на экран слово «Мама!». То
есть, объект cout способен правильно распорядиться не только переменными и
манипуляторами (такими как endl), но и последовательностями символом,
заключенными в двойные кавычки. Эти кавычки лишь помечают начало и конец
последовательности, так что объект cout выведет на экран слово Мама! без всяких
кавычек и затем, увидев манипулятор endl, переведет строку.
Единственная инструкция нашей программы cout «...; окружена фигурными
скобками и к ней приписаны значки int mainO {cout «...;}. Рамка int main(){}, в
которую заключаются инструкции, есть в каждой программе на C++, и мы пока будем
ее использовать просто потому, что компилятор не сможет без нее работать.
Также он откажется работать и без директивы #include <iostream>, которая
включает в исходный текст программы описание объекта cout. Если забыть про эту
директиву, компилятор не поймет, что же такое cout, и не сможет продолжить работу.
Заметим, что не все объекты C++ нуждаются в директиве #include. Переменные
типа int или double так укоренились в языке C++, что компилятор узнает их без
всяких описаний, и ему нечего будет возразить против программы, показанной
в листинге 1.2.
Листинг 1.2. «Немая» программа
int mainO {
int il = 2.i2 = 3. i3;
i3 = il + i2: // 13 = 5
}
Эта программа вычисляет сумму двух переменных i 1 и i2 и посылает ее в
переменную i3. Но результат ее работы так и пропадет где-то в недрах компьютерной
памяти, потому что в программе нет инструкции, направляющей содержимое i3 во
внешний мир, то есть на экран монитора.
В программе из листинга 1.2 есть одно новшество — две косые черты, стоящие
слева от инструкции i3 = il + i2;. Эти значки — признак начала комментария —
пометок, помогающих понять, что же делает программа. Комментарии предназначены
22
Глава 1. Объекты
людям, и компилятор просто игнорирует то, что стоит в данной строке программы
правее признака комментария //. Часто программисты ленятся писать
комментарии, но без них программа скоро забывается и становится непонятна даже своему
создателю.
Если сравнить листинги 1.1 и 1.2, то можно догадаться о назначении директивы usi ng
namespace std; в листинге 1.1. Очевидно, она как-то связана с объектом cout. Стоило
исключить его из листинга 1.2, и директива using namespace std; не понадобилась.
Действительно, директива using namespace указывает компилятору так называемое
пространство имен, используемое по умолчанию. Эти пространства введены в C++,
чтобы можно было создавать очень большие программы, разрабатываемые
многими людьми, где очень вероятны конфликты имен, когда одним именем называются
разные объекты. Чтобы этого не случилось, вводятся пространства имен, и объекты
с одним именем, но из разных пространств компилятор будет считать разными.
В таком подходе есть свои преимущества, но есть и недостатки. Так,
приходится указывать, к какому пространству имен принадлежит объект, даже если он
единственный в программе и его не с чем путать! И директива using namespace std;
просто говорит компилятору, что все имена, встреченные в программе, по
умолчанию принадлежат стандартному пространству std. Это пространство имен можно
указать перед именем каждого объекта, и тогда директива using namespace станет
не нужна (листинг 1.3).
Листинг 1.3. Явно указанное пространство имен
#include <iostream>
int mainO {
std::cout « "Мама!" « std::endl;
}
В листинге 1.3 пространство имен пришлось указать дважды — перед объектом
cout и перед манипулятором end!. От этого листинг сделался гораздо менее
понятным, поэтому в дальнейшем мы постараемся там, где это возможно, использовать
директиву usi ng namespace.
Для указания пространства имен в листинге 1.3 использован :: — оператор
области действия. Кажется странным, что этот значок называется оператором, ведь мы
привыкли к тому, что им связываются два объекта, часто называемые операндами
(например, оператор сложения il+i2). Но в C++ есть операторы, вроде оператора
области действия ::, применимые к одному объекту.
Этот раздел уместно будет завершить коротким замечанием об устройстве имен
в программах на C++. Любое имя в C++ должно начинаться с буквы и может
содержать латинские буквы1, цифры, а также символ _. Все другие символы в имени
объекта компилятор будет воспринимать как операторы, поэтому объявление
int i-j; //неверно!!
будет для него сущей бессмыслицей, потому что компилятор поймет его как
попытку вычесть из переменной i переменную j и выдаст сообщение об ошибке.
Регистр букв имеет значение, поэтому One и one — разные имена.
GCC
23
GCC
В предыдущем разделе мы говорили о компиляторе вообще. Но компилятор — это
программа, выполняемая на компьютере, и, как у всякой программы, у нее есть
имя. Компилятор, которым мы будем пользоваться, называется gcc (версия 33.1).
Эта программа распространяется бесплатно и для большего удобства я разместил
ее на сайте издательства «Питер». Найти ее можно среди файлов к этой книге1.
У компилятора gcc есть множестве/версий для самых разных компьютеров. Наша
версия компилятора работает под управлением операционной системы Windows
(Windows 98, NT, 2000 или ХР).
Но прежде чем устанавливать и запускать компилятор, необходимо установить
файловую оболочку, помогающую переходить из одной компьютерной папки
в другую, создавать, менять, копировать файлы, запускать программы и т. п. Нам
подойдет оболочка FAR, которую, как и компилятор, можно отыскать на сайте
издательства «Питер» http://www.piter.com.
Установка оболочки очень проста: запускаем программу farl65.exe и указываем
папку, где она будет храниться. При первом запуске FAR (для этого нажимается
кнопка Пуск и в программной группе Far Manager выбирается значок Far Manager)
нужно указать удобный шрифт (я предпочитаю 10x18). Для переключения между
латинскими и русскими буквами используется комбинация клавиш Alt+Shift2.
Теперь можно использовать FAR для запуска программы установки компилятора
gcc.exe. Предположим, что она находится в папке download на диске F. Запускаем
FAR, нажимаем клавиши Alt+Fl и в появившемся слева меню выбираем имя
диска F. Далее выбираем (подсвечиваем) нужную папку («подсветка»
перемещается в окне клавишами Т> 1) и входим в нее, нажав клавишу Enter. Теперь осталось
только отыскать там программу gcc.exe, выбрать ее, запустить нажатием клавиши
Enter и указать диск, где расположится компилятор. Если это F, то все его файлы
окажутся там в папке f:\gcc, и на этом установка закончится.
Наконец, все готово к тому, чтобы скомпилировать нашу первую программу, для
которой заведем специальную папку и создадим там файл Ш.срр, Для этого
необходимо войти в папку, нажать клавиши Shift+F4, указать имя файла, нажать
клавишу Enter, набрать текст, показанный в листинге 1.1, нажать клавишу F10 и в
появившемся меню выбрать пункт Сохранить.
Осталось только запустить компилятор, для чего в командной строке оболочки
FAR (рис. 1.1) набирается следующая команда и нажимается клавиша Enter:
gcc Ш.срр -о Ш.ехе
1 Проще всего сделать это так: ищете на сайте www.piter.com мою фамилию, в показанном списке книг
выбираете «Самоучитель C++» и в ее кратком описании ищете раздел «файлы к книге». Там должны
быть компилятор gcc.exe, оболочка FAR, а также исходные тексты программ.
2 Знак + означает, что для перехода от латинских букв к русским и обратно необходимо нажать
клавишу Alt и, не отпуская ее, нажать Shift.
24
Глава 1. Объекты
Несколько секунд работы компилятора — и в нашей папке появляется файл
Lll.exe.
Рис. 1.1. До компиляции — одно нажатие клавиши Enter
Это и есть компьютерная программа, которая запускается на выполнение простым
нажатием клавиши Enter1. Нажав Enter, увидим, как на мгновение приподнялись
голубые панели оболочки и вновь сомкнулись над лежащим под ними черным
пространством. Чтобы посмотреть, что же они скрыли, нажмем Ctrl+O и увидим то,
что ожидали — выведенное на экран слово «Мама!» (рис. 1.2).
Рис. 1.2. Результат работы первой программы
Этот результат немного разочаровывает, и хочется, чтобы программа управлялась
мышью и системой меню, как, например, Калькулятор или Блокнот. К сожале-
1 Перед запуском программу необходимо выбрать в файловой оболочке. Можно также использовать
для запуска программы двойной щелчок мыши.
Sizeof, или «Размер имеет значение»
25
нию, программы, которые познакомят нас с языком C++, — совсем другого типа.
Это так называемые консольные приложения, которые, оставаясь полноценными
Windows-программами, могут выводить на специальный экран, называемый
консолью, только символы вроде а, б, 3, q, f, но не картинки. Удобной консолью, часть
которой показана на рис. 1.2, снабжена оболочка FAR, и здесь нет ничего
удивительного, ведь FAR — тоже консольное приложение, хотя и гораздо более
сложное, чем наша первая программа. Как и «полноценные» программы для Windows,
FAR управляется мышью и системой меню, вызываемой клавишей F9.
В заключение скажем пару слов о командной строке компилятора, которую мы
видим в левом нижнем углу рис. 1.1. В ней дсс —имя компилятора, lll.cpp — название
файла, содержащего исходный текст программы, -о — специальные управляющие
символы, с помощью которых задается имя программы (в нашем случае — lll.exe).
Между именем программы и символами -о должен быть по крайней мере один
пробел.
Sizeof, или «Размер имеет значение»
Мы уже поняли, что язык C++ задуман так, что писать программу на нем можно,
не думая об устройстве компьютера, потому что переводом «человеческого» языка
C++ на язык данного процессора должен заниматься компилятор. И тогда
программы, написанные на C++, смогут выполняться на самых разных компьютерах:
PC (с операционными системами Windows и Unix), Macintosh, SUN и т. д.
Но самый простой пример показывает, что полностью забыть о внутреннем
устройстве компьютера нам не удастся. Действительно, вспомним еще раз задание
начального значения целочисленной переменной
int i=3;
Когда значение переменной столь мало, вопросов не возникает. Но представим
себе такое объявление переменной:
int i=1000000000:
Здесь уже появляется сомнение. Ведь интуитивно ясно, что компьютер, как бы
велик он не был, конечен. Значит, он не может хранить бесконечные числа, и
переменная i должна иметь какие-то пределы. Чтобы понять, что это за пределы, нам
придется познакомиться с принципами хранения данных в компьютере.
Начнем с того, что данные могут быть представлены в компьютере только
напряжениями на электрических контактах. Можно вообразить себе, что каждая
десятичная цифра кодируется определенным напряжением на контакте микросхемы.
Если, скажем, напряжение изменяется от 0 до 5 В, то нулю соответствует диапазон
напряжений от 0 до 0,5 В, единице — от 0,5 до 1 В, двойке — от 1 до 1,5 В и т. д.
Такое представление чисел возможно, но от него пришлось отказаться, потому
что оно оказалось слишком ненадежным и трудно реализуемым. Гораздо проще
использовать только две градации напряжения: Есть напряжение-Нет
напряжения, ДА-НЕТ, 0-1. Поэтому память компьютера — это длиннющая последователь-
26
Глава 1. Объекты
ность элементов, называемых битами, которые могут находиться только в двух
состояниях, обозначаемых нулем 0 и единицей — 1. Двух состояний не хватает
для хранения каких-то чисел или букв, поэтому процессор оперирует
последовательностями восьми идущих подряд бит, называемых байтами. Легко понять, что
байт может находиться в 28 = 256 различных состояниях. Действительно, первый
бит байта имеет два состояния, тогда последовательность двух идущих подряд бит
имеет уже 4 состояния, потому что каждому из двух состояний первого бита
соответствует одно из двух состояний второго. Точно так же последовательность трех
бит может находиться в 8 состояниях, потому что четырем состояниям двух
предыдущих бит соответствует 2 состояния третьего бита. Рассуждая в том же духе,
придем к выводу, что байт (8 бит) может находиться в 256 различных состояниях.
Значит, байт способен вместить много полезного: букву алфавита (ведь букв, даже
с учетом регистра — существует не более 100 во всех европейских языках), колоду
карт, целое число и многое другое. Вот почему в C++ существует специальный
объект типа char, занимающий один байт памяти и способный хранить небольшие
целые числа:
char а = 127;
или буквы:
char а = 'а':
Последний пример компилятор воспримет как приказ создать переменную а типа
char, содержащую число, которым кодируется строчная латинская буква а. В
системе Windows этой букве соответствует число 61, так что инструкции
char a = 61:
и
char a = 'а':
делают одно и то же, но последняя запись гораздо лучше, потому что не зависит
от конкретного компьютера, где буквам могут соответствовать другие цифры.
Инструкция же char а = ' а'; гарантирует нам, что переменная а будет хранить
число, которым на данном компьютере кодируется буква а.
Чтобы понять, каков размер того или иного объекта, в C++ есть специальный
оператор sizeof (). Программа из листинга 1.4 показывает на экране размер известных
нам в данный момент объектов.
Листинг 1.4. Размеры некоторых объектов
#include <iostream>
int mainO {
using namespace std:
double d:
cout « sizeof(char) « endl; //1 байт
cout « sizeof(int) « endl; //4 байта
cout « sizeof(d) « endl: //8 байтов
}
Знак
27
Как видите, оператор sizeofO применим и к типу {классу) объекта sizeof(int),
и к самому объекту sizeof (d).
Зная размер переменной, можно оценить ее «вместимость». Например, число
состояний, в которых могут находиться 4 идущих подряд байта, равно
256x256x256x256 = 4294967296, то есть в переменной типа int, занимающей как
раз столько памяти, можно разместить целые числа от -2147483648 до 2147483647
(если не забыть нуль, как раз и получится 4294967296 числа).
Догадаться о том, какие числа помещаются в переменной double, помогает
способ задания их начальных значений, с которым мы до сих пор не встречались.
Оказывается, значение double можно указать в виде произведения дробной части
на степень десятки, обозначаемую латинской буквой е (или Е). Например, число
36.6 задается как
double d « 3.66E1;
то есть произведение 3.66 на 10. Такой способ позволяет разбить число на две
части: мантиссу (у нас это 3. 66) и порядок, то есть степень десяти, равный в нашем
случае единице.
Степень может быть и отрицательной. Например, показанное ниже объявление
задает переменную р, начальное значение которой равно 10~2, то есть 0,01:
double р = 1е-2:
В записи числа с помощью буквы е имеется одна хитрость: перед е всегда должна
стоять цифра. Например, запись
double p = е-2; // неверно!
неправильна, потому что символы е-2 компилятор воспринимает как имя
переменной! Теперь понятно, почему имя в C++ всегда начинается с буквы: не будь
этого, компилятор стал бы путать имена и значения переменных double, чего,
конечно, допустить нельзя.
Знак
Диапазон значений целочисленных переменных (плюс-минус два с лишним
миллиарда), о котором мы узнали в предыдущем разделе, велик, но все же конечен.
Поэтому иногда возникает необходимость иначе распорядиться памятью,
занимаемой переменной, и записывать туда только положительные числа и ноль. На этот
случай в C++ предусмотрено слово unsigned (без знака), которое используется при
объявлении переменной
unsigned int i;
Слово int здесь ставить не обязательно, потому что переменные unsigned всегда
целые и могут быть объявлены просто как
unsigned i;
28
Глава 1. Объекты
Переменные unsigned занимают те же четыре байта, что и переменные со знаком.
И зная число состояний, в которых эти четыре байта могут находиться, легко
понять, что в переменной без знака умещаются числа от 0 до 4294967295.
Как и ожидалось, переменная unsigned способна хранить большие положительные
числа, чем в переменной со знаком. Но как теперь совместно использовать
переменные со знаком и без, что, например, будет, если значение одной переменной •
попытаться присвоить другой?
В программе из листинга 1.5 мы попытались совершить невозможное —
присвоить переменной без знака значение -3.
Листинг 1.5. Преобразование целочисленных переменных
#include <iostream>
using namespace std:
int mainO {
unsigned int ui;
ui = -3;
cout « "ui = "« ui « endl:
}
И нам это почти удалось. Компилятор не стал отвергать программу из
листинга 1.5, ограничившись предупреждением:
warning: assignment of negative value '-3' to 'unsigned int' (предупреждение: присваивание
отрицательного числа -3 беззнаковой переменной)
Предупреждения компилятора говорят о том, что в исходном тексте программы
встретилось что-то подозрительное, но в отличие от ошибок (error) это не мешает
получить программу, исполняемую компьютером1.
Правда, работать она может совсем не так, как нам бы хотелось. Например,
программа из листинга 1.5 вместо -3 показывает на экране
ui = 4294967293
И тут нечему удивляться, ведь ui в нашем примере — переменная без знака, и
именно такой считает ее объект cout, когда выводит на экран.
На самом деле, беззнаковая переменная, когда ей присваивают значение -3,
находится точно в таком же состоянии, что и переменная со знаком, равная -3. Просто
те же самые биты уже понимаются как код для положительного числа и
воспринимаются объектом cout совсем не так. Если же присвоить значение unsigned
переменной со знаком, то на экране возникнет число -3 (листинг 1.6).
Листинг 1.6. Переменные int и unsigned отображаются на экране по-разному
#include <iostream>
using namespace std;
int mainO {
int i;
1 Имя программы заканчивается точкой и буквами .ехе. Эти последние символы, стоящие в имени
файла после точки, называют расширением.
Unsigned и signed char
29
unsigned ui;
ui = -3:
i = ui;
cout « "ui = "« ui « endl; // ui = 4294967293
cout « "i = "« i « endl: // l = -3
}
Unsigned и signed char
Взаимодействие знаковых и беззнаковых переменных далеко не всегда так
очевидно, как в программе из листинга 1.6. Когда числа со знаком и без участвуют
в арифметических действиях, возможны самые неожиданные результаты. Здесь,
конечно же, действуют правила C++ о «подтягивании» более «узкого» типа к
более «широкому». Но чтобы получить о них представление, необходимо завершить
знакомство с «родными» объектами языка C++, которые узнаются компилятором
без всяких заголовков, включаемых директивой #i ncl ude.
Прежде всего заметим, что переменные char тоже могут быть со знаком и без:
unsigned char;
signed char;
Здесь появилось новое слово signed, говорящее о том, что переменная хранит
числа со знаком. Заметим, что переменная int не нуждается в слове signed, потому
что она signed — по умолчанию. Но переменные char могут быть как со знаком, так
и без в зависимости от используемого компилятора. Чтобы определить, каковы
переменные char для данного компилятора, достаточно запустить тестовую
программу, показанную в листинге 1.7.
Листинг 1.7. Проверка переменной char
#include <iostream>
using namespace std;
int mainO {
char ch =255;
int i;
i = ch;
cout « i « endl; // для компилятора gcc i=-l
}
Переменная char (знаковая или беззнаковая) занимает один байт с числом
различных состояний, равным 256. Значит, по аналогии с переменными int в знаковой
переменной char хранятся числа от -128 до 127, а в беззнаковой — от 0 до 255.
Если послать число 255 в знаковую переменную, то оно там станет
отрицательным, потому что 255 больше, чем 127 — максимальное положительное число,
которое способна хранить переменная char со знаком. Если же число 255 посылается
в переменную char без знака, то оно останется там самим собой. И поскольку
переменная со знаком int заведомо «шире» любой переменной char, она станет отрица-
30
Глава 1. Объекты
тельной, если переменная char — знаковая или останется положительной и равной
255 в противном случае.
Запустив программу из листинга 1.7, увидим на экране число -1. Значит,
компилятор gcc по умолчанию считает переменные char числами со знаком. Но поскольку
программа, написанная на C++, должна компилироваться многими
компиляторами, никогда не нужно надеяться, что переменная char будет со знаком по
умолчанию, и всегда следует писать в программе signed char; или unsigned char; — в
зависимости от смысла этой переменной.
Строки
Емкости объектов char, способных хранить всего одну букву, явно
недостаточно даже для междометий, не говоря уже о целых предложениях. Вот почему
в C++ для хранения и обработки последовательностей символов придуманы
и широко используются специальные объекты string, по-русски называемые
строками.
Одна строка способна занять столько памяти, что в ней легко поместится «Война
и мир», но мы будем скромнее и начнем с одного слова «Война», которое
показывает на экране программа из листинга 1.8.
Листинг 1.8. Пример строки
#include <iostream>
#include <string>
using namespace std;
int main(){
string s = "Война";
cout « s « endl:
}
Как видите, можно совместить объявление строки и задание ее начального
значения (в нашем случае это «Война»). Кавычки здесь служат для указания начала
и конца строки символов, поэтому на экране окажется просто слово Война без
всяких кавычек.
В отличие от элементарных объектов char, int и double, строка — «настоящий»
объект C++, чье описание хранится в файле string и подключается к программе
директивой #include <string>. Как и всякий такой объект, строка управляется
собственными функциями, две из которых показаны в листинге 1.9.
Листинг 1.9. Использование собственных функций строки
#include <iostream>
#include <string>
using namespace std;
int main(){
string s = "Война":
Ссылки
31
s.appendC и мир"):
cout « s « endl; // Война и мир
cout « s.sizeO « endl; // 11
}
Функция appendO присоединяет к слову «Война» символы « и мир», поэтому
объект cout покажет на экране фразу Война и мир (конечно, без всяких кавычек).
Другая собственная функция sizeO возвратит текущее число символов (с учетом
пробелов) в строке, поэтому на экране появится число 11.
Функция sizeO — очень странная. До сих пор мы знали только функции, которые
что-то делали со своими объектами в соответствии с тем, какие аргументы (числа,
строки и вообще — любые возможные объекты) указывались внутри круглых
скобок. Функция что-то получала и соответственно вела себя.
Функция sizeO ничего не получает, ведь пространство между круглыми
скобками пусто, наоборот — она возвращает текущее число символом в строке. То есть,
собственная функция может сама выступать как какой-то объект, чье содержимое
можно показать на экране. В нашем случае функция sizeO выступает э роли
целого числа без знака (говорят, что она возвращает значение unsigned int), которое
и выводит на экран объект cout.
Ссылки
Ссылки играют огромную роль в C++, несмотря на то, что это всего лишь способ
иначе назвать тот или иной объект. Ссылка объявляется с помощью оператора &,
например:
int &ra = а:
Здесь а — исходная целочисленная переменная, га — ссылка на нее. Только что
объявленная ссылка «намертво» связывается с переменной и разлучить их
впоследствии невозможно.
Листинг 1.10. Ссылки
#include <iostream>
using namespace std:
int main(){
int a - 2;
int &ra = a;
cout « "Ссылка = " « га « endl; // га = 2
га = 5:
cout « "a = " « a « endl: // a = 5
}
Программа, показанная в листинге 1.10, сначала объявляет переменную а и
присваивает ей начальное значение — двойку. Затем объявляется га — ссылка на
переменную а. Делается это с помощью оператора &, причем его положение между
int и га не имеет значения. Следующие объявления эквивалентны:
32
Глава 1. Объекты
int&ra = a;
int& га = а:
int &га = а;
Смысл этих объявлений в том, что переменная га, хоть она и по-другому
называется, занимает то же самое место в памяти, что и а, по сути, это та же переменная а.
После объявления ссылки га в нашей программе га становится равной тому же,
что а, то есть двум. Но если присвоить га иное значение, например 5, то и
переменная а также станет равной пяти.
Глава 2
Операторы и циклы
Условности
Арифметические операторы, о которых мы говорили в предыдущей главе, весьма
важны и часто встречаются. Но важнее всего в программировании, безусловно, г/с-
ловные операторы, потому что они позволяют изменить ход программы, в
частности заставить какой-то ее фрагмент выполняться многократно. Без них программа
может состоять только из идущих друг за другом инструкций, каждую из
которых придется явно описать. Если учесть, что компьютеры способны выполнять
миллиарды инструкций в секунду, то для описания одной десятой секунды жизни
процессора не хватило бы жизни человеческой.
Знакомство с условными операторами начнет программа, определяющая
минимальное число из трех (листинг 2.1).
Листинг 2.1. Минимальное число из трех
#include <iostream>
using namespace std;
int mainO {
int x = 4. у = 2, z = 3. min;
min = x; // min = 4
if(y < min) min = y: // min = 2
if(z < min) min = z; // min не меняется
cout « "min = " « min « endl; 111
}
После ее выполнения в переменной min должно оказаться самое маленькое
число, в нашем случае— 2. Первая инструкция программы засылает в min
переменную х, и min становится равной 4. Далее выполняется условная инструкция
if (у < min) min = y, смысл которой прост: если у меньше min, то в переменную min
засылается значение у. Если же условие не выполняется, переменная min остается
прежней. В нашем случае у равен 2, a mi n — 4, у меньше mi n, следовательно,
инструкция выполняется, min становится равной 2, и программа переходит к инструкции
if(z < min) min = z, смысл которой аналогичен. В нашем случае z равна 3, a min — 2.
34
Глава 2. Операторы и циклы
Значит, условие z < min не выполняется, присваивания min = z не происходит,
программа переходит к следующей инструкции и выводит на экран цифру 2.
В нашем следующем примере условный оператор помогает уместить в нескольких
строчках множество инструкций. Программа, приведенная в листинге 2.2,
выводит на экран сумму тысячи последовательных целых чисел от 1 до 1000.
Листинг 2.2. Суммирование 1000 целых чисел
#include <iostream>
using namespace std;
int main(){
int N = 1000:
int sum = 0. i = 1;
whiled <= N) {
sum = sum + i;
i - i + 1:
}
cout « "sum = " « sum « endl;
}
В начале программы задаются числа слагаемых int N = 1000, а также начальное
значение суммы и первого слагаемого:
int sum = 0. i = 1;
Сумма вычисляется с помощью цикла whi1e(){}. Выполнение цикла начинается
с проверки условия, стоящего в круглых скобках. Если оно истинно, то
выполняется тело цикла, заключенное в фигурные скобки. Не будь этих скобок, в цикле
выполнялась бы только инструкция, сразу стоящая за whileO — sum = sum + i, а это
совсем не то, что нам в данной задаче нужно.
В нашем цикле сначала проверяется условие i <= N (i меньше или равно N), и
поскольку в этот момент i равно 1, а N — тысяче, оно выполняется, а значит, к
переменной sum прибавляется единица (sum = sum + iI, затем увеличивается
переменная i (i = i + 1), и мы вновь переходим к проверке условия i <- N, но теперь уже i
равно 2, а N — по-прежнему тысяче. Значит, условие снова выполняется, и цикл
совершает свой второй оборот, прибавляя к переменной sum двойку и вновь
увеличивая переменную i (она становится равной трем).
Легко понять, что в последний раз условие i <= N выполнится, когда
переменная i будет равна 1000. На этом последнем обороте цикла к переменной sum
прибавится 1000, затем i увеличится на 1 и снова начнется проверка условия i <= N. На
этот раз условие не выполнится, потому что переменная i уже равна 1001,
программа пропустит тело цикла и перейдет к следующей инструкции, выводящей на
экран значение суммы:
cout « "sum=" « sum « endl:
1 Запись sum=sum+i поначалу многим непонятна. Но программирование — не алгебра, а оператор =
означает присваивание, то есть к переменной sum прибавляется i, и результат снова отправляется в
переменную sum.
Логические операторы
35
Задача 2.1. Не компилируя программу, попробуйте понять, чему будет равна
сумма при N=1000.
Задача 2.2. Какое число покажет на экране программа из листинга 2.2, если в
цикле while() убрать фигурные скобки?
whiled <= N)
sum = sum + i;
i « i + 1:
Заметим, что программу, показанную в листинге 2.2, довольно легко приспособить
для суммирования любого количества чисел. Нужно только поменять значение N.
Программисты не должны писать «одноразовые» программы. Ведь
программирование — тяжелый труд, и нужно стремиться использовать его результаты
многократно.
Завершим этот раздел перечислением других условных операторов. Легко
построить условие, используя операторы < (меньше), <= (меньше или равно), > (больше),
>= (больше или равно). Кроме них есть еще оператор = (равно), значение которого
истинно, если две переменные равны, и оператор != (не равно), его значение
истинно, когда переменные различны.
Логические операторы
Для задачи суммирования чисел, решенной нами в предыдущем разделе, было
достаточно проверять при каждом обороте цикла всего одно условие i <= N. Но так
бывает далеко не всегда. Случается, что какое-то действие нужно выполнить при
соблюдении двух и большего числа условий. Например, подсчет чисел, попавших
в заданный интервал, требует двойной проверки: переменные должны быть
одновременно больше чего-то одного и меньше чего-то другого.
Поэтому в языке C++ есть специальные логические операторы, помогающие
записывать сложные условия. Например, условие попадания числа х в интервал (9,10)
записывается так:
X > 9 && х < 10;
В нем сначала проводятся сравнения х > 9 и х < 10, потому что приоритет
операторов <> выше, чем оператора &&, а затем с результатами этих сравнений работает
оператор &&. Предположим, что проверяемое число относится к типу doubl e и равно
9,5. Тогда результатом обоих сравнений будет «истина», а если аргументы,
соединяемые оператором &&, истинны, то таковым будет и окончательный результат. Но
если хотя бы одно условие (х > 9 или х < 10 ) не выполняется, значение выражения
х > 9 && х < 10 будет ложным.
Оператор && называется логическим И и дает «истину», только если истинны обе
величины, участвующие в операции (их еще называют операндами). Причем
истиной считается любое число, не равное нулю. Например, результат операции
10 && -20 истинен, а 0 && 1000 — ложен.
36
Глава 2. Операторы и циклы
Другой оператор из этого семейства A1) называется логическим ИЛИ, он истинен,
когда хотя бы один из операндов истинен. То есть,
1||ы. 1Ц0-1. 0Ц1-1. о11о-о
Здесь «истина» обозначается единицей, а «ложь» — нулем.
Наконец, последний логический оператор (!) называется оператором отрицания,
он превращает истинное значение в ложное, и наоборот. Значение 11000 ложно,
а значение !0 — истинно.
Как мы уже поняли, результатов условной или логической операции может быть
только два: верно-неверно, да-нет и т. д. Логично поэтому ввести специальную
переменную, которая имеет всего два значения, а значит, способна хранить
результат логической операции. Такие переменные в языке C++ объявляются как
bool, например:
bool to_be;
У них всего два значения: true (истина) и fal se (ложь). Впрочем, без всякой ошибки
можно считать, что true равно единице, a fal se — нулю. Рассмотрим листинг 2.3.
Листинг 2.3. Вывод на экран результата логической операции
#include <iostream>
using namespace std:
int mainO {
int a = 2. b = 3;
bool 1;
1= a < b;
cout « 1 « endl;
cout « (a < b) « endl;
}
Инструкция cout « 1 « endl выведет на экран единицу. Тот же результат
получится при непосредственном выводе результата операции а < b без присвоения его
булевой переменной. Но теперь нужно заключить а < b в скобки, потому что
оператор « имеет больший приоритет, и компилятор, если не поставить скобки, поймет
последнюю инструкцию программы как вывод на экран переменной a (cout «a),
а дальше вроде бы нужно выполнять операцию cout < b, но с объектом cout это
невозможно, и компилятор, выдав сообщение об ошибке, прекратит работу.
Задача 2.3. Чему равно выражение tobe 11 !to_be при разных значениях булевой
переменной to_be?
Умные операторы
Мы привыкли к тому, что арифметические операторы (+, -,*,/) применимы к
числам, и хорошо знаем, что такое дважды два. Но оказывается, что в C++ один и тот
же оператор имеет множество смыслов, поэтому арифметические операторы
можно встретить там, где числами и не пахнет. Посмотрим, например, каков смысл
Умные операторы
37
сложения строк, с которыми мы познакомились в разделе «Строки» предыдущей
главы (листинг 2.4).
Листинг 2.4. Сложение строк
#include <iostream>
#include <string>
using namespace std;
int mainO {
string a. b. c:
a = "Война и ";
b = "мир";
с = a + b:
cout « с « endl;
if(a > b)
cout « a « endl;
else
cout « b « endl:
}
Программа, показанная в листинге, имеет дело с тремя строками: a, b и с. Значения
первых двух задаются явно (например, а="Война и ";), а в третью строку
отправляется сумма первых двух (с = а + Ь;). Но что значит «прибавить к строке а
строку Ь»? В языке C++ под этим понимается дописывание строки b после строки а. То
есть, после сложения строк а и b в строке с оказывается их сумма, равная «Война
и мир».
Возникает вопрос: как компилятор C++ узнает, что делать с объектами,
соединенными знаком + (и любыми другими операторами)? Ответ зависит от типа объекта.
Дело в том, что все объекты C++ равны, но некоторые «равнее других». Самые
равные — уже известные нам целочисленные переменные, а также переменные
типа double. Они настолько глубоко укоренились в языке, что «выдрать» их
оттуда невозможно. Встречая переменные такого типа, компилятор знает, что делать,
и ему не нужны файлы описания, включаемые директивой #include <>.
Другое дело — такие объекты, как cout и string. Перечень возможных операций,
а также их смысл содержится в описании этих объектов. Вот почему компилятор
требует подключения файлов <iostream> (там описан объект cout) и <string> (там
описаны объекты типа string).
Эти описания можно, в принципе, менять, задавая другой смысл сложения строк
или выдумывая какие-то новые операции (умножения строк, например), но лучше
такие эксперименты проводить над собственными объектами, не трогая объекты
из стандартной библиотеки, такие как string.
В следующей главе мы подробнее займемся строками, а в этой расскажем лишь
о возможных операциях сравнения. В конце листинга 2.4 сравниваются две строки
а и Ь. Здесь применена чуть другая условная конструкция if О else. Если условие
в круглых скобках (у нас — а < b ) истинно, выполняется инструкция, следующая
за if (в нашем случае cout « а « endl), если нет — инструкция после else (у нас —
cout « b « endl). В общем случае после if О и else могут выполняться несколько
инструкций, но для этого их надо заключить в фигурные скобки:
38
Глава 2. Операторы и циклы
if (){
инструкция;
инструкция;
} else {""
инструкция:
инструкция;
}
На первый взгляд, условие а > b в программе из листинга 2.4 кажется странным,
как будто сравниваются лампочка и апельсин. Нам понятно, что два меньше трех.
Но что больше — «Война и » (обратите внимание на пробел после «и») или «мир»?
Ответ зависит от метода сравнения строк. Стандартные объекты типа string
сравниваются лексикографически, то есть большей считается строка, у которой больше
первый несовпадающий символ. В нашем случае не совпадают уже первые
символы строк «В» и «м». Какой же из них больше? Очевидно больше второй,
потому что буквы, как правило, кодируются такими числами, чтобы большему числу
соответствовало низшее положение в алфавитном списке. Символ «м» ниже по
алфавиту, чем «В»1, и, следовательно, больше. Значит, в результате выполнения
условной инструкции if () на экране появится строка «мир».
Конечно, строки можно сравнивать и другими способами, например по длине, но
лексикографическое сравнение — самое разумное, потому что позволяет
расставить строки в алфавитном порядке, как в словаре.
Унарные и бинарные операторы
Эти мудреные названия означают всего лишь, что есть операторы, работающие
с одним объектом (унарные) и с двумя (бинарные). Нам встречались те и другие.
Например, оператор сложения +— бинарный, поскольку слагаемых всегда два,
а оператор приведения типа static_cast<>() — унарный (почему— догадайтесь
сами).
В этом разделе мы познакомимся с несколькими важными операторами, а поводом
для изучения станет программа суммирования чисел из листинга 2.2. В ней
встречается довольно неуклюжая инструкция sum = sum + i, смысл которой в увеличении
переменной sum на величину i. Эта инструкция станет понятней, если записать ее
с помощью нового оператора +=:
sum +- i;
Задача 2.4. Оператор += — бинарный или унарный?
Естественно, все арифметические операторы позволяют использовать такую же
запись, вместо х - х * а удобнее писать х *= а, вместо х = х / а следует писать х /= а
и т. д. Новый оператор +- можно применить и в инструкции увеличения i на еди-
1 Здесь сравниваются большая «В» и маленькая «м», но результат будет таким же, как и при сравнении
«В» и «М», потому что прописные буквы кодируются еще большими числами, чем строчные.
Унарные и бинарные операторы
39
ницу, записав ее как i += 1, но поскольку операция увеличения на единицу
встречается очень часто, для нее придумали специальный оператор автоувеличения
(++). То есть увеличение i на единицу можно записать1 как i=i+l, как i += 1 или как
i++. Последняя запись — самая короткая и понятная. Кроме автоувеличения есть,
конечно, и автоуменьшение на единицу (- -).
Знание новых операторов позволяет нам записать программу суммирования так,
как показано в листинге 2.5.
Листинг 2.5. Использование операторов += и ++ при суммировании чисел
#include <iostream>
using namespace std;
int mainO {
int N = 1000;
int sum - 0. i - 1:
whiled <= N) {
sum +¦ i;
i++;
}
cout « "sum=" « sum « endl;
}
В новом цикле whileO тоже две инструкции, но иначе записанные. Инструкция
sum += i; заменяет инструкцию sum = sum + i: (см. листинг 2.2), а инструкция i++;
действует так же, как i - i + 1;. Можно пойти дальше и записать цикл while
whiled <= N)
sum += i++;
так, что единственная выполняемая в нем инструкция sum += i++; станет похожа на
груду спичек, брошенных на пол небрежной рукой. Но она выполняет то, что
требуется, то есть прибавляет к переменной sum текущее значение i и только затем
увеличивает i на единицу. То есть, к sum сначала прибавится 1, затем 2 и так до
1000, после чего i увеличится на единицу, станет равной 1001, условие i <= N в
цикле whileO не выполнится, и программа перейдет к следующей инструкции,
которая выведет на экран сумму чисел от 1 до 1000, и ее работа на этом завершится.
Но так будет лишь в том случае, когда оператор автоувеличения стоит справа2 от
переменной. Только тогда увеличение i происходит после прибавления i к
переменной sum. Но оператор ++ может стоять и слева3 от переменной, и тогда в
инструкции sum +- ++i; i увеличится до суммирования.
То есть, при входе в цикл whileO переменная i будет равна 1, затем i увеличится
на 1, став равной 2, и уже двойка прибавится к переменной sum.
Последним прибавленным числом будет 1001, потому что в самом начале цикла i
будет равна 1000 (условие i <¦ N выполнится), затем к i сначала прибавится
единица, и только потом i будет добавлена к переменной sum. Значит, цикл
whiled <- N) sum +« ++i;
1 Оператор ++ применим только к переменным, запись (а+Ь)++ бессмысленна.
2 Такую запись называют постфиксной.
3 Такую запись называют префиксной.
40
Глава 2. Операторы и циклы
сложит все числа от 2 до 1001, то есть сделает не совсем то, что от него ожидали.
Задача 2.5. Докажите, что цикл whiled <= N) sum += ++i; при начальном значении
суммы sum=0 складывает все числа от i+l до N+1.
Задача 2.6. Попробуйте просуммировать целые числа от 1 до 200000000. Что при
этом получается, почему?
Операторы автоувеличения (++) и автоуменьшения (- -) позволяют очень
компактно записать инструкцию, но пользоваться ими надо с осторожностью, ведь
переменная, к которой применен оператор ++ (или - -), если она еще раз встретится в
инструкции, сделает результат вычислений непредсказуемым. Пусть, например,
sum=0, a i=7. Чему тогда будут равны переменные sum и i после выполнения
следующей инструкции:
sum=i++ * i++;
Что сделает компилятор: умножит 7x7, зашлет в sum результат D9) и потом
дважды увеличит i? Или же возьмет первый операнд 7, увеличит его, умножит 7x8,
зашлет результат E6) в sum, затем еще раз увеличит i? Оказывается, может
произойти все, что угодно, и не имеет смысла даже думать об этом. Просто нужно
понять, что такие опасные двусмысленные выражения использовать нельзя никогда,
ни при каких обстоятельствах.
До сих пор нам был известен только один цикл whileO, именно с его помощью
были впервые просуммированы числа от 1 до 1000. Но в C++ есть еще две
инструкции, с помощью которых можно организовать циклические вычисления. Первая
do{}while() очень похожа на цикл while(), но в ней проверка условия выхода из
цикла выполняется после, а не до вычислений, как в цикле whileO.
Программа, показанная в листинге 2.6, суммирует числа от 1 до 1000 в цикле
do{}whileO.
Листинг 2.6. Пример цикла do{}while()
linclude <iostream>
using namespace std;
int mainO {
int N = 1000;
int sum = 0. i = 1:
do {
sum += i++;
} while (i <= N);
cout « "sum - " « sum « endl;
}
Суммирование в этом цикле идет до тех пор, пока условие i <= N истинно. Поскольку
проверка whiled <= N) производится в конце вычислений, цикл do{}while()
выполняется хотя бы один раз. В нашем примере последнее прибавленное к sum число
будет равно 1000. После прибавления оператор ++ увеличит i на единицу, i станет
равна 1001, условие i <= N не выполнится, программа выйдет из цикла и приступит
к выводу результата на экран.
Унарные и бинарные операторы
41
Задача 2.7. Каким будет результат работы программы из листинга 2.6 при N = 0?
Почему?
Другая инструкция for О особо удобна, когда число оборотов цикла в
точности известно. В листинге 2.7 показано, как с ее помощью просуммировать числа
от 1 до 1000.
Листинг 2.7. Пример цикла for()
#include <iostream>
using namespace std;
int mainO {
int N « 1000;
int sum = 0;
for(int i=l; i <= N; i++)
sum += i;
cout « "sum = " « sum « endl;
}
Инструкция for() управляется тремя выражениями, стоящими в круглых скобках
и разделенными точкой с запятой. Первое выражение задает начальное значение
переменной, которая меняется в цикле. Причем такую переменную можно прямо
объявить, как это у нас и сделано (int i=l;). Второе выражение (i <= N) задает
условие выполнения цикла, наконец, третье выражение (i++) задает шаг изменения
переменной после каждого оборота цикла.
Чтобы знать досконально, как работает цикл for О, попробуем «всухую»
прокрутить его несколько раз. Итак, перед первым оборотом переменная sum равна нулю,
a i — единице. Прежде чем выполнять инструкцию sum += i, цикл проверяет
условие i <= N, а поскольку в этот момент i=l, a N=1000, условие выполняется, и к
переменной sum прибавляется текущее значение i, то есть единица. Теперь sum=l
и выполняется инструкция изменения переменной i. В нашем случае это i++, а
поскольку текущее значение переменной i все еще единица, оно становится равной
двум, и теперь все готово к новому обороту цикла, но сначала проверяется условие
i <= N. В этот момент i=2, условие выполняется и к переменной sum, равной в этот
момент единице, прибавляется текущее значение i=2 и переменная sum становится
равной трем. Дальнейшие обороты цикла ничем не отличаются от только что
описанных, стоитразве что посмотреть на последний оборот. Очевидно, в этот момент
i=1000, условие i <= N выполняется и к переменной sum прибавляется 1000, затем
переменная i увеличивается на единицу, становится равной 1001, условие i <= N
не выполняется, цикл прекращает работу, и программа, не прибавляя 1001 к sum,
переходит к следующей за циклом инструкции, то есть к выводу результатов на
экран. Заметим, что цикл for О, так же как инструкция whileO, может вообще не
выполниться ни разу, потому что условие проверяется до перехода к телу цикла.
Если условие сразу не выполняется, программа пропустит цикл for О и сразу
перейдет к следующей за ним инструкции.
Раз уж в этом разделе зашла речь об операторе автоувеличения, уместно будет
сказать пару слов о названии языка C++. Применение оператора автоувеличения
к некоему объекту С означает, что C++ — улучшенная версия языка С,
разработанного Деннисом Ритчи и Кеном Томпсоном в начале 70-х годов прошлого века.
42
Глава 2. Операторы и циклы
Язык С (мы обычно говорим Си) стал необычайно популярен, и назвав новый
язык C++, Бьярн Строуструп, его создатель, сделал гениальный маркетинговый
ход, во многом определивший немыслимый успех C++.
Поначалу язык C++ действительно был «улучшенным» С, но неумолимая
логика развития привела к полному отходу C++ от С, и сейчас это как бы дйа
существующих вместе совершенно разных языка, между которыми сохраняется связь,
то есть наработанное для С еще используется в C++. Языки C++ и С по-прежнему
совместимы «снизу вверх», то есть компилятор C++ понимает почти любую
программу, написанную на С. Но сам язык C++ уже настолько далек от С, что его
следует изучать отдельно и без оглядки на «предка».
Глава 3
Строки и контейнеры
Приговор
В разделе «Умные операторы» главы 2 мы уже встречались со специальными
объектами — строками, задаваемыми ключевым словом string. Было показано, как
меняется смысл операторов =, +, < и т. д., когда они используются с такими
объектами, как строки.
Оператор + хорош и логичен при соединении строк, оператор = — при задании
начального значения строки. Но было бы неправильно выполнять все действия
с объектами с помощью операторов. Строки гораздо сложнее чисел, и никаких
операторов не хватит, чтобы выполнить все возможные действия. Да и выглядеть
это будет непривычно. Какой, например, оператор выбрать для вставки одной
строки внутрь другой? Вот поэтому основная часть операций с объектами
проводится с помощью их собственных функций.
Рассмотрим в качестве примера классическую задачу: вставить запятую в
приговор: «Казнить нельзя помиловать». Программа, показанная в листинге 3.1,
использует для этого функцию insertO.
Листинг 3.1. Спасение
#include <iostream>
#include <string>
using namespace std;
int mainO {
string sentence»
"Казнить нельзя помиловать";
sentence.insertA4. ".");
cout « sentence « endl;
}
До сих пор нам встречались собственные функции с одним аргументом,
помещаемым внутри круглых скобок. Но у собственных функций может быть сколько
44
Глава 3. Строки и контейнеры
угодно аргументов, разделенных запятыми. Так, у функции insertO, показанной
в листинге, их два: первый — это позиция, куда вставляется строка (в нашем
случае — 14), и сама вставляемая строка (в нашем случае — просто запятая).
Представьте себе, что от правильного положения запятой зависит ваша жизнь.
Значит, нужно предельно точно определить номер символа, с которого
начнется вставка. А для этого необходимо знать, что символы в строке C++
нумеруются, начиная с нуля! Буква «К» имеет нулевой номер, буква «а» — первый,
а буква «я» — тринадцатый. Значит, запятая должна быть четырнадцатой, и
программа, показанная в листинге 3.1, выведет на экран фразу «Казнить нельзя,
помиловать».
Познакомившись с функцией insertO, имеет смысл еще раз вернуться к
операторам. Дело в том, что почти все операторы дублируются соответствующей
собственной функцией объекта string. Вспомним фрагмент программы из главы 2,
в котором соединяются две строки «Война и » и «мир»:
string a. b. с:
а = "Война и ";
b = "мир";
с - а + Ь;
Поскольку работу оператора + выполняет функция append (), этот фрагмент можно
переписать так:
string a.b.c:
а = "Война и ";
b = "мир";
с = a.append(b);
cout « с;
То же самое можно записать еще короче:
string a;
а = "Война и ":
cout « a.appendC'Mnp");
В двух последних примерах проявилась способность собственной функции не
только управлять объектом, но и возвращать полученные результаты во
внешний мир. Запись cout « a.appendC'Mnp") возможна, когда функция a.appendO не
только меняет строку а, но и возвращает измененную строку, так что ее можно
присвоить другой строке (в нашем случае строке с) или просто вывести на экран.
Собственные функции C++ в зависимости от своего устройства способны
возвращать самые разные объекты: целые числа (как функция sizeO), строки и т. д.
Бывают функции, не возвращающие ничего.
Наконец, бывают функции, не похожие на все то, что мы до сих пор видели.
Оказывается, вместо оператора присваивания можно использовать следующую
конструкцию:
string а("Здравствуй");
Такой способ присваивания начальных значений довольно непривычен. Казалось
бы, он не имеет отношения ни к оператору =, ни к собственным функциям. На са-
Приговор
45
мом деле, здесь используется особая собственная функция, которая есть в каждом
объекте. Эта функция отвечает за присваивание начальных значений и
называется конструктором. Поскольку она уникальна, создатели C++ решили вызывать ее
не так, как другие функции.
Другой важный пример дублирования оператором собственной функции —
организация доступа к отдельным символам строки. Делается это либо с помощью
оператора [], либо собственной функцией at(). Программа, показанная в
листинге 3.2, сначала выводит на печать два последовательных восклицательных знака,
потому что и a.atD), и а[4] возвращают четвертый символ строки, а если учесть,
что нумерация идет с нуля, то это как раз и есть восклицательный знак.
Листинг 3.2. Использование собственной функции at()
#include <iostream>
#include <string>
using namespace std;
int main(){
string a= "Мама!";
cout « a.atD) « endl;
cout « a[4] « endl;
for(int i - 0; i < a.sizeO: f++)
cout « a[i] « endl;
}
В цикле for(), показанном в том же листинге следом за инструкциями вывода,
наша строка выводится вертикально:
м
а
м
а
I
Чтобы определить размер строки, используется функция sizeO, возвращающая
число символов — в нашем случае это 5. Обратите внимание, функция sizeO не
меняет сам объект, а только узнает, сколько в нем хранится символов, и
возвращает полученное число во внешний мир.
Завершить этот раздел хочется примером важной функции findO, у которой нет
соответствующего оператора. По ее названию многие, наверное, догадались, что
findO ищет заданный фрагмент строки (листинг 3.3).
Листинг 3.3. Поиск иголки в стоге сена
finclude <iostream>
#include <string>
using namespace std;
int mainO {
int p;
string a = "сеносеносеноиголкасеносеносеносено":
p = а."МгкК"иголка"):
cout « p « endl:
}
46
Глава 3, Строки и контейнеры
Показанная программа ищет «иголку» в стоге «сена» и возвращает позицию
первого символа найденного фрагмента. В нашем случае «и» — первый символ в
слове «иголка» — окажется на 12-й позиции, и функция findO возвратит число 12.
Если же фрагмент обнаружить не удастся, функция выдаст -1 — отрицательное
число, которое, естественно, не может быть позицией символа в строке.
Массивы
В разделе «Приговор» мы познакомились с произвольным доступом к отдельным
символам, составляющим строку. С помощью оператора [] оказалось возможным
выделить из строки какую угодно букву. Если, например, а — строка, то а[3] — это
третий (с учетом того, что перед ним стоят нулевой, первый и второй символы)
символ строки, а[5] — пятый и т. д. Но всего чудеснее оказалась возможность
поставить вместо конкретного номера C или 5) целочисленную переменную,
например а[1]. Если переменная i равна 5, то получим пятую букву строки, если 3 —
третью. Казалось бы, никакой разницы и никакого продвижения вперед. Но теперь
можно менять целочисленную переменную в цикле, и, значит, автоматически
перебирать символы сколь угодно длинной строки, подсчитывать частоту их
появления, выделять отдельные слова и т. д.
Представим себе, что запись a[i] невозможна, а необходимо подсчитать
частоту встречающихся в строке символов. Тогда пришлось бы вручную указывать
все буквы а[0], а[1], а[2], а[3], ..., а[1000] и т. д. Ясно, что смысл
программирования, как способа в немногих строках задать множество операций, здесь
теряется. Без возможности объединить в одну удобную оболочку массу
однотипных переменных вычисления на компьютере теряют смысл. Вот почему
буквы, составляющие такой объект, как строка, можно автоматически
перебирать в цикле.
Теперь самое время вспомнить, что кроме составляющих строку букв есть еще
переменные типа char, int, double, и все они тоже должны быть упакованы
так, чтобы можно было автоматически перебирать их, изменяя в цикле одну
переменную-индекс. Для такой упаковки в C++ есть множество средств, но самое
простое и популярное — это массивы.
В массивы можно собрать переменные и объекты любого типа. Например, массив
из пяти переменных типа doubl e задается так:
double tmp[5];
Увидев такое объявление, компилятор выделит в памяти компьютера место для
пяти идущих подряд переменных double. Если учесть, что переменная double
занимает 8 байт, то всего будет выделено 8x5 = 40 байт.
Поскольку в массивах, как и в строках, нумерация начинается с нуля, первые
восемь байт памяти (с нулевого по седьмой) займет переменная tmp[0], далее
пойдет переменная tmp[l] и так до последней переменной tmp[4], которая займет
байты с 32 по 39.
Массивы
47
Из-за того, что в массив помещены переменные одного размера, идущие друг
за другом плотно, без пробелов, вычисления резко упрощаются. В программе из
листинга 3.4 показано, как легко вычисляется среднее из пяти величин типа double,
если эти величины объединены в массив.
Листинг 3.4. Вычисление среднего
linclude <iostream>
using namespace std;
int mainO {
double tmp[5] = {36.6.36.9.37.3.38.3.38.5};
double sum;
fordnt i = 0; i < 5: i++)
sum += tmp[i];
cout « "среднее = " « sum / 5 « endl;
}
Начальные значения массива, как это видно из листинга 3.4, можно задать внутри
фигурных скобок, отделив одно значение от другого запятой. Цифры в фигурных
скобках позволяют догадаться, что вычисляется средняя температура тела,
возможно, за сутки.
Любопытно, что, задавая начальные значения элементов массива, можно не
указывать его размер:
double tmpC] = {36.6.36.9,37.3.38.3.38.5};
При такой записи компилятор, подсчитав числа в фигурных скобках, сам поймет,
что задается массив из пяти чисел, и, зная размер переменных типа doubl e, выделит
необходимую память и заполнит ее указанными числами. Особенно такая запись
удобна при создании массива символов. Например, объявление:
char hello[] = "Мама!";
создает массив из шести переменных типа char: первые пять мест займут буквы,
а шестое — специальный завершающий символ, добавляемый компилятором,
чтобы обозначить конец последовательности букв. Этот символ обозначается как
«\0» и отличается от любого символа, который можно показать на экране тем, что
все биты у него — нулевыеI.
Как мы уже говорили, в массив можно поместить любые объекты C++. Программа,
показанная в листинге 3.5, пользуется массивом из двух строк, чтобы вывести на
экран фразу «Война и мир».
Листинг 3.5. Путешествие по массиву из двух строк
#include <iostream>
#include <string>
using namespace std;
int mainO {
string s[2] =.{"Война и ". "мир"}; Продолжение &
1 Символ «\0» играет важную роль в таких массивах. Не будь его, оператор вывода не имел бы
понятия, где кончается последовательность, и был бы вынужден показать на экране всю оставшуюся
память компьютера байт за байтом, начиная с буквы, стоящей в начале массива.
48
Глава 3. Строки и контейнеры
Листинг 3.5 (продолжение)
cout « s[0] « s[l] « endl;
cout « s[0][4] « endl; // Вывод 'а'
}
Массив строк состоит всего из двух элементов: s[0] и s[l]. Начальные значения
строк задаются в фигурных скобках. Заметим, что s[0] — обычное имя строки.
А раз так, к нему применим оператор [], выделяющий отдельную букву. В
последней строке программы на экран выводится четвертый символ нулевой строки
(s[0][4]), равный (с учетом начинающейся с нуля нумерации) букве «а».
Контейнеры
Массивы, о которых мы говорили в предыдущем разделе, представляют собой
простейшую «упаковку» для однотипных объектов. По своей сути это идущие подряд
ячейки памяти, в которых располагаются объекты — один за другим.
Массивы широко и с незапамятных времен применяются в C++, многие
стандартные объекты C++ работают именно с ними. Одно из основных достоинств
массива—в простоте задания начальных значений (они записываются внутри фигурных
скобок). Основной его недостаток — в отсутствии у массива свойств стандартных
объектов C++. «Правильный» объект C++ имеет собственные функции
(методы), он управляется ими и только через них мы узнаем о его состоянии. Никаких
собственных функций у массива нет, поэтому пользоваться им довольно опасно.
Можно, например, задать индекс, превышающий размер массива. Компилятор
этого не заметит, но программа после попытки записи в «неположенное» место
памяти, скорее всего, аварийно завершится.
Вот поэтому в С+*+ созданы новые «упаковки» для объектов, называемые
контейнерами. Контейнеры — тоже объекты со своим внутренним устройством и
собственными функциями. В этом разделе мы познакомимся с вектором —
контейнером, очень похожим на массив. Простейшие действия с контейнером
иллюстрирует листинг 3.6.
Листинг 3.6. Простейшие действия с вектором
#include <iostream>
#include <vector>
using namespace std;
int mainO {
vector<int> s;
fordnt i=0; i < 10; i++)
s.push_back(i);
fordnt i-0: i < s.sizeO; i++)
cout « s[i] « endl;
}
Любой контейнер нужно сначала объявить. В нашей программе инструкция
vector<int> s объявляет контейнер s типа vector, содержащий целочисленные
переменные. Естественно, в контейнере могут храниться и другие объекты: строки,
переменные типа doubl e и т. д.
Итераторы и алгоритмы
49
После объявления контейнер пуст. Чтобы заполнить его, применяется
собственная функция вектора s.push_back(). В нашем случае цикл
forCint i = 0: i < 10; i++)
s.push_back(i);
«заталкивает» в контейнер десять последовательных чисел от 0 до 9. А дальше все
эти числа по очереди «достаются» из вектора оператором [] и одно за другим
выводятся на экран. Обратите внимание, что в отличие от массива, у вектора есть
собственная функция sizeO, возвращающая число его элементов. Мы
использовали ее при выводе чисел на экран, чтобы не ошибиться и не выйти за пределы
контейнера.
Итераторы и алгоритмы
В предыдущем разделе мы «доставали» отдельный элемент из контейнера vector
с помощью оператора [], часто называемого оператором произвольного
доступа — за то, что он выдает любой элемент контейнера, независимо от уже
прочитанных. Но есть контейнеры, разрешающие только последовательный доступ
к составляющим их объектам. Чтобы, например, прочитать шестой элемент
такого контейнера, нужно прочитать предыдущие 5. В таких контейнерах
передвижение от объекта к объекту возможно только с помощью специальных объектов —
итераторов.
Смысл итераторов в том, что они указывают на объекты, хранящиеся в контейнере.
Слово «указывает» означает, что итератор «знает», где расположен объект, и в любой
момент может получить к нему доступ. Делается это с помощью оператора *.
Листинг 3.7. Итераторы
#inc1ude <iostream>
#include <vector>
using namespace std;
int mainO {
vector<int> s:
for(int i-0: i < 10; i++)
s.push_back(i):
vector<int>::iterator ini. at. end;
ini = s.beginO:
end = s.endO:
for(at - ini; at < end; at++)
cout « *at « endl;
}
Программа, показанная в листинге 3.7, заполняет вектор целыми числами от 0 до 9,
а затем выводит каждый элемент контейнера на экран. Для вывода используются
три итератора ini, at, end, объявляемые инструкцией
vector<int>::iterator ini. at. end;
50
Глава 3. Строки и контейнеры
После инструкций i ni = s. begi n(); end - s. end(); итератор i ni указывает на нулевой
элемент вектора, а итератор end указывает на элемент, стоящий непосредственно
за последним элементом контейнера.
Вывод элементов вектора на экран производится в цикле:
for(at - ini: at < end; at++)
cout « *at « endl;
Сначала итератор at указывает на нулевой элемент контейнера (at = i ni). Применив
к этому элементу оператор *, мы получаем сам этот нулевой элемент, который
затем выводится на экран с помощью оператора «. Далее наступает очень важный
момент: в цикле к итератору at прибавляется единица (at++), после чего он уже
указывает на следующий элемент контейнера. Я нарочно не сказал
«увеличивается на единицу», потому что итераторы — не числа и смысл прибавления к ним
чисел иной: после прибавления единицы1 итератор указывает на следующий объект,
который выводится на экран оператором «. Так, увеличивая at на единицу, цикл
продолжит работу, пока не выведет на экран последний элемент вектора. После
еще одного прибавления единицы at станет равным end, то есть станет указывать
на элемент уже не принадлежащий контейнеру, условие at < end не выполнится,
и цикл вовремя завершится.
Двух итераторов, указывающих на нулевой элемент и на элемент,
непосредственно следующий за последним, достаточно для того, чтобы проделать множество
сложных действий над всеми элементами контейнера. Делается это с помощью
специальных функций, называемых алгоритмами. Их описание помещено в файл
algorithm, подключаемый директивой #include<> (см. приложение В).
Листинг 3.8. Алгоритмы
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std:
int mainO {
vector<int> s:
forCint i=0: i < 10; i++)
s.push_back(i);
vector<int>::iterator at. ini. end;
ini = s.beginO:
end = s.endO:
random_shuffle(ini.end);
for(at=ini; at < end; at++)
cout « *at « endl;
cout « endl:
sortdni.end);
for(at=ini; at < end; at++)
cout « *at « endl;
cout « endl;
1 Если прибавить двойку, итератор «перескочит» через объект. Если, скажем, at указывает на нулевой
элемент, то at + 2 укажет на второй и т. д.
Итераторы и алгоритмы
51
В программе из листинга 3.8 элементы контейнера сначала случайно тасуются
алгоритмом random_shuffle(), а затем сортируются, то есть выстраиваются в порядке
возрастания алгоритмом sortO.
Первоначальный порядок чисел в контейнере после серии инструкций push_back()
был таким: 0,1,2,3,4,5,6,7,8,9. После перетасовки порядок меняется1; 8,1,9,2,0,
5,7,3, 4, 6. Увидеть этот новый порядок можно с помощью цикла for(), в котором
итератор at последовательно указывает на все элементы контейнера от
нулевого до последнего девятого. А далее функция sort О расставляет числа в порядке
возрастания и тем самым приводит контейнер в первоначальное состояние. Как
и функции random_shuf fle(), функции sort О достаточно знать начальный и
конечный2 итераторы сортируемого контейнера. Но, как и функция перетасовки, sort О
работает не со всеми контейнерами, а только с теми, где возможен произвольный
доступ к любому элементу, осуществляемый оператором []. Объект vector — как
раз такой контейнер.
Заметим, что строка — тоже контейнер, чьи элементы, как и элементы
вектора, можно сортировать, тасовать, применять к ним другие стандартные
алгоритмы. Как и у любых контейнеров, у строк есть итераторы, объявляемые так
(листинг 3.9):
string::iterator at:
Листинг 3.9. Передвижение по строке с помощью итератора
#include <iostream>
#include <string>
#include <algorithm>
using namespace std:
int mainO {
string s - "abracadabra":
string::iterator at:
sort(s.begin(). s.endO);
for(at = s.beginO: at < s.endO: at++)
cout « *at:
}
Показанная программа выводит на экран буквы «aaaaabbcdrr» — не меньшую
абракадабру, чем первоначальная строка, но зато все буквы теперь расставлены по
алфавиту.
В этом разделе мы в первый раз встретились с алгоритмами — функциями,
которые не пристегиваются точкой к объектам, а «гуляют сами по себе». В дальнейшем
мы научимся сами создавать такие функции, а пока будем пользоваться теми, чьи
описания подключаются директивой #inc1ude <a1gorithm>.
1 Разные компиляторы по-разному реализуют алгоритм random_shuffle(). Для компилятора,
отличного от gcc, порядок будет иным.
2 То есть итератор, указывающий на элемент, стоящий непосредственно за последним элементом
контейнера.
52
Глава 3. Строки и контейнеры
Питание программ
До сих пор наши программы были на «голодном пайке»: все необходимые
данные они получали из собственного текста, хотя в реальности они, конечно же,
получают «пищу» извне и в таких огромных количествах, что суперкомпьютеры,
способные выполнять триллионы операций в секунду, иногда работают годы без
«передышки».
Пока нам был известен только один канал связи с внешним миром — вывод на
экран компьютера с помощью оператора « (cout«). Логично предположить, что
существует объект, через который вводятся данные, и соответствующий оператор ».
Этот объект называется cin и через него можно получать данные с клавиатуры
компьютера. Программа, показанная в листинге 3.10, вводит с клавиатуры целые
числа, затем складывает их с помощью алгоритма accumulateO и выводит сумму
на экран.
Листинг 3.10. Ввод чисел с клавиатуры
#iinclude <iostream>
#include <vector>
#include <numeric>
using namespace std;
int mainO {
int buf:
vector<int> t;
while(cin » buf)
t.push_back(buf);
cout « endl « endl;
cout « accumulate(t.begin(). t.endO. 0) « endl;
}
Вводом чисел заведует цикл while(cin » buf). Условие выполнения цикла cin »
buf на первый взгляд кажется странным, но » — это не стрелка, показывающая
направление ввода от cin к переменной buf,» — это бинарный оператор, говорящий
о том, какое действие производится между двумя объектами cin и buf. А у всякого
действия может быть результат. В нашем случае после каждого ввода с клавиатуры
и нажатия клавиши Enter значение cin » buf оказывается равным true, и
выполняется тело цикла whileO, в котором функция push_back() посылает очередное число
в контейнер. Если же вместо очередного числа ввести символ, отличный от числа,
то выражение ci n » buf станет равным f al se, и цикл прекратится. Прекратить ввод
с клавиатуры можно и стандартным способом — просто нажав Ctrl + Z (при этом на
экране появятся символы *Z) и затем Enter1.
Окончив ввод, программа дважды переводит строку, чтобы результат вычислений
не «наезжал» на введенные числа, и затем вызывается алгоритм accumulateO,
который складывает все числа в контейнере и отправляет результат на экран. Используя
два итератора t.beginO и t.endO, accumulateO вычисляет сумму всех объектов,
начиная с того, на который указывает итератор t.beginO, и кончая объектом,
стоящим непосредственно перед тем, на который указывает итератор t.endO. Третий
1 Чтобы это сработало, клавиатуру нужно переключить на латинский регистр.
Файлы
53
аргумент алгоритма accumulateO — начальное значение суммы — равен нулю. Это
значит, что сумма целых чисел, входящих в контейнер, тоже будет целым числом,
что опасно, ведь при большом количестве слагаемых сумма превысит число,
которое способна хранить переменная int, и наступит переполнение1. Поэтому лучше,
когда для суммы предусмотрен больший диапазон значений, чем для отдельных
слагаемых. Чтобы, например, сумма оказалась типа double, нужно сделать третий
аргумент алгоритма accumul ate равным 0.0.
Только что мы суммировали целые числа, но accumulateO, как и многие другие
алгоритмы, работающие с контейнерами, может суммировать любые объекты, лишь
бы для них был определен оператор +, например строки. Программа, показанная
в листинге 3.11, складывает строки, введенные с клавиатуры. После ввода каждой
строки нажимается, как обычно, Enter, а в самом конце вводится символ Ctrl + Z.
(то есть нажимаются клавиши Ctrl+Z, а затем Enter)
Поскольку сложение строк — это их объединение, на выходе появится одна
строка, состоящая из всех ранее введенных строк. Если, скажем, с клавиатуры были
введены буквы «а», «Ь», «г», «а», «с», «a», «d», «a», «b», «г», «а», то в сумме
получится «abracadabra».
Листинг 3.11. Сложение строк алгоритмом accumulateO
finclude <iostream>
#include <vector>
#include <string>
#include <numeric>
using namespace std:
int main(){
string buf, sum(""); // sum - пустая строка
vector<string> t;
while(cin » buf)
t.push_back(buf):
cout « endl « endl;
cout « accumulate(t.begin(). t.endO. sum) « endl;
}
Эта программа мало отличается от предыдущей. Правда, в ней для задания
начального значения суммы используется конструктор строки sumC"), о котором
уже говорилось в разделе «Приговор».
Файлы
Ввод с клавиатуры — важный «источник питания» программ, но далеко не
единственный. Вспомним о файлах, в изобилии встречающихся на жестких дисках
наших компьютеров. Файлы хранят тексты программ, данные, которые должен
обрабатывать компьютер, статьи, книги и многое другое. За десятилетия
существования компьютеров человечество успело создать миллиарды файлов, которые,
конечно же, должны быть доступны программам.
Подробно о действиях с целыми числами рассказано в приложении Б.
54
Глава 3. Строки и контейнеры
С точки зрения C++, файл — это особый объект, управляемый собственными
функциями и операторами. Программа, показанная в листинге 3.12, открывает файл
rhyme, читает его строка за строкой, а затем сортирует прочитанные строки и
выводит их на экран.
Листинг 3.12. Чтение строк из файла
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int mainO {
char buff[80];
vector<string> s;
ifstream infile;
infile.open("rhyme");
while A) {
infile.getline(buff. sizeof(buff));
if(infile.eofO) break;
s.push_back(buff);
}
sort(s.begin(). s.endO):
for(int i = 0; i < S;Size(); i++)
cout « s[i] « endl;
infile.closeO;
}
Файл infile, объявляемый в программе, — это объект типа if stream, описанный во
включаемом файле <fstream>. Далее в программе инструкция infile.openCrhyme")
открывает файл под названием rhyme, который должен быть в той же папке, что
и сама программа. Файл rhyme находится среди исходных текстов программ,
размещенных на сайте www.piter.com, и содержит строки детской считал очки «царь,
царевич, король, королевич, сапожник, портной...».
После открытия файла начинается самое важное: чтение строк и заполнение ими
контейнера s. Все это делается в цикле while(l){}. Единица в круглых скобках
означает, что условие всегда выполняется, поэтому причину выхода из цикла нужно
искать у него внутри.
Но прежде посмотрим, как происходит чтение строк. Делает это собственная
функция getlineO:
infi1e.getline(buff. sizeof(buff)):
У функции два аргумента: имя массива1, где оказывается только что прочитанная
строка, и его размер — sizeof (buf), равный в нашем случае восьмидесяти. Итак, мы
поняли, как прочитать из файла отдельную строку. Но теперь перед нами
неумолимо встает вопрос: а до каких пор продолжать чтение, как узнать о том, что файл
1 Приходится использовать массивы, потому что более совершенные объекты (строки) функция
getline() не воспринимает.
Анаграммы
55
кончился? К сожалению, сделать это можно, когда «уже поздно и неприятность
произошла». О завершении файла можно узнать, лишь когда не удастся
очередная попытка чтения. Вот почему инструкция if(infile.eofO) break стоит раньше
инструкций передачи очередной прочитанной строки в контейнер. Если
очередное чтение не удалось, значит, достигнут конец файла, функция infile.eofO
возвращает «истину» и выполняется инструкция break, которая прерывает
выполнения цикла, после чего программа переходит к следующей за whi I e(){} инструкции
sort О. Заметим, что break можно применять и в циклах for О и do{}whi!e(), при
этом действие будет таким же: break мгновенно выводит программу за пределы
цикла к первой следующей за ним инструкции.
Анаграммы
Лучшее описание этого алгоритма из тех,
что я слышал, — размахивание руками
Т. Каргилла: «Отсортируйте это так
(горизонтальное движение рукой), а затем
так (вертикальный взмах)».
Д. Бептли.
Жемчужины программирования
Анаграммы — это наборы слов, состоящих из одних и тех же букв, например «сон»
и «нос» для русского или «excitation» (волнение) и «intoxicate» (отравиться) для
английского. Чтобы искать анаграммы на компьютере (вручную это вряд ли
возможно), необходим словарь соответствующего языка. Предположим, он у нас есть.
Но что делать дальше? Проще всего получить перестановки всех букв в слове (для
слова «нос» это «нос», «нсо» «сон» «сно», «онс», «осн») и затем искать каждую
перестановку в словаре. Все, что найдено, и будет набором анаграмм.
Этот план вполне реален, хотя компьютеру придется здорово потрудиться, чтобы
его выполнить. Ведь число перестановок уже десяти букв равно 10!, то есть более
трех миллионов, а при дальнейшем увеличении числа букв количество
возможных перестановок становится астрономическим. Ну и что же — скажете вы, — на
то и компьютер, чтобы выполнять нудную однообразную работу. Напишем
программу и подождем. Но очень часто попытка заставить компьютер работать
вместо себя приводит к тому, что ждать приходится вечно.
Чтобы успешно вычислять на компьютере, нужны алгоритмы — способы быстро
и эффективно справиться с определенной задачей. Конечно, алгоритмом
получения анаграмм можно считать и тупой перебор комбинаций всех букв с
последующей проверкой и отбором в словаре. Но гораздо уместнее назвать алгоритмом
следующую идею: раз анаграммы состоят из одних и тех же букв, то
отсортированная последовательность букв окажется для анаграмм одинаковой. Отсортировав
буквы в словах «excitation» и «intoxicate», получим одно и то же: «aceiinottx».
56
Глава 3. Строки и контейнеры
Итак, идея: возьмем словарь, составим пары одинаковых слов и отсортируем
буквы в одном из слов, составляющих пару, например левом. Тогда слева от
каждого слова появляется «отпечаток» — отсортированная последовательность букв,
а слова с одинаковыми отпечатками — и есть анаграммы. Чтобы легче было искать
одинаковые отпечатки, достаточно отсортировать пары слов по возрастанию их
отпечатков. А дальше — читать одинаковые отпечатки и выводить на экран
соответствующие им слова.
Итак, задача поиска анаграмм сводится к двум сортировкам. Сначала
сортируются буквы в словах, затем — сами слова. Эта задача все-таки остается трудной, но
в стандартной библиотеке C++ есть специальный контейнер multimap, идеально
подходящий для отыскания анаграмм.
В контейнере multimap объекты хранятся парами <ключ><объект>, по ключу их
можно сортировать. Поэтому план нахождения анаграмм обретает такие черты:
открыть файл словаря, прочитать слово, скопировать его в строку, отсортировать
буквы в строке и заслать пару из отсортированного и нетронутого слова в
контейнер, причем поступить так со всеми словами в словаре. Затем остается
отсортировать контейнер по «ключу» и вывести на экран анаграммы. Эту задачу решает.
программа, показанная в листинге 3.13.
Листинг 3.13. Поиск анаграмм
#iinclude <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std:
int mainO {
char buff[80];
string sbuff;
multimap<string, string> an;
multimap<string, string >::iterator im.ane;
ifstream infileCdiction"):
while A) {
infile.getline(buff. sizeof(buff));
if(infile.eofO) break;
sbuff - buff;
sort(sbuff .beginO. sbuff.endO);
an.insert(pair<string. string>(sbuff. buff));
}
im = an.beginO;
ane = an.endO;
vector<multimap<string, string >::iterator> chg:
chg.push_back(im);
while(++im != ane) {
chg.push_back(im);
if((*im).first != (*chg[0]).first) { // ключи не равны?
if(chg.size() > 2 ) { // накопились анаграммы?
for(unsigned int i = 0; i < chg.sizeO - 1; i++)
cout « (*chg[i]).second « endl;
cout « endl;
Анаграммы
57
}
chg.clearO; // очищаем вектор
chg.push_back(im); // сохраняем текущий итератор
}
}
if(chg.sizeC) > 1)
for(unsigned int i=0; i < chg.sizeO: i++)
cout « (*chg[i]).second « endl:
infile.closeO;
}
Основная структура данных в этой программе — контейнер an, объявляемый
инструкцией:
multimap<string. string> an;
Далее в программе создаются итераторы im, ane, помогающие двигаться от одной
пары строк, хранимой в контейнере, к другой.
Затем открывается файл diction, содержащий английские слова (его тоже можно
найти на сайте издательства «Питер» www.piter.com среди исходных текстов
программ).
Теперь все готово к заполнению контейнера. Делается это в цикле while(l){}.
Первая инструкция в цикле читает очередную строку из словаря и сохраняет
ее в массиве buff. Далее функция infile.eofO проверяет, удачно ли считана
очередная строка. Отрицательный результат говорит о том, что мы вышли за границу
файла, то есть все слова прочитаны и пора выходить из цикла (делает это
инструкция break). Если же конец файла не достигнут, прочитанное слово переписывается
в строку sbuf f, и следующая инструкция расставляет буквы в этой строке по
алфавиту (сортирует):
sort(sbuff.begin(). sbuff.endO);
Теперь у нас есть желанная пара: отсортированное слово (в строке sbuf f) и
нетронутое слово (в массиве buff). Для отправки этой пары в контейнер используется
функция insertO:
an.insert(pair<string. string>(sbuff. buff));
Обратите внимание: перед записью (sbuff, buff) стоит описание pair<string,
string>, говорящее компилятору, что в контейнер засылается пара строк.
Мы дошли до конца цикла whileO{}, но так и не поняли, где же сортируется сам
контейнер. Так вот, функция insert О автоматически вставляет очередную пару
строк в нужное место, и контейнер оказывается отсортированным по «ключу»
sbuff на любом этапе создания.
Казалось бы, с окончанием цикла whileO{} все самое страшное позади. Остается
только вывести на экран слова с одинаковым «ключом», хранимом в элементе
sbuff. Но именно эта задача и оказывается самой сложной — из-за того, что у нее
нет готовых решений (как, например, для сортировки контейнера multimap) и над
ней приходится думать самому.
58
Глава 3. Строки и контейнеры
Впрочем, общая идея вывода проста: заведем специальный контейнер (вектор)
для итераторов контейнера multimap. В нашей программе он объявляется
следующим образом:
vector<multimap<string. string >::iterator> chg;
Эта запись приводит поначалу в ужас, но по сути она ничем не отличается от
объявления vector<int>:: iterator... Просто название объекта, для хранения которого
предназначен контейнер:
multimap<string, string >: iterator
в нашем случае гораздо длиннее, чем int и вдобавок само содержит треугольные
скобки <>.
Теперь станем «запихивать» в этот вектор итераторы — один за другим. Делает
это собственная функция вектора chg.push_back(im), помещенная в самом начале
цикла while(++im |= апе){...}. Когда в векторе окажется текущий итератор im,
нужно проверить, равны ли «ключи» слов, соответствующих итераторам im и chg[0].
Делает это инструкция:
if((*im).first != (*chg[0]).first)
В ней im — текущий итератор, только что записанный в контейнер, a chg[0] —
итератор, сохраненный ранее. Чтобы получить значение ключа, нужно превратить
итератор в сам объект оператором *, а значения (*im).first и (*chg[0]).first
и будут искомыми ключами. Если эти ключи не равны, то либо накопление
анаграмм закончилось (когда в контейнере chg больше двух итераторов), либо не
начиналось (когда итераторов два и ключи у обоих разные ). В первом случае
накопленные анаграммы выводятся на экран, и затем вектор очищается функцией chg.
с!еаг() и туда помещается текущий итератор chg.push_back(im). Во втором случае,
когда в векторе хранятся всего два итератора и соответствующие им ключи
различны, вектор просто очищается, затем туда помещают текущий итератор и поиск
анаграмм повторяется. Фрагмент текста программы, описанный в этом параграфе,
выглядит так:
if((*im).first != (*chg[0]).first) { // ключи не равны?
if(chg.sizeC) > 2 ) { // накопились анаграммы?
for(unsigned int i = 0; i < chg.sizeO - 1; i++)
cout « (*chg[i]).second « endl;
cout « endl;
}
chg.clearO: // очищаем вектор
chg.push_back(im); // сохраняем текущий итератор
}
Этот фрагмент заключен в цикл
im - an.beginO;
ane - an.endO:
vector<multimap<string, string >::iterator> chg:
chg.push_back(im);
Стиль
59
while(++im !- ane) {
chg.push_back(im);
}
перебирающий все итераторы контейнера multimap. В условии выполнения цикла
while(++im != ane) применен префиксный оператор ++im, чтобы увеличить im перед
входом в цикл whileO. При входе в цикл один итератор уже хранится в контейнере
chg. Затем в chg отправляется второй итератор, указывающий на следующий
объект, а дальше наступает черед проверки равенства ключей:
if((*im).first !- (*chg[0]).first){...}
о которой мы уже рассказали.
Казалось бы, после выполнения цикла while(++im != ane) {} все анаграммы должны
оказаться на экране. Но это не так. Ведь итераторы в цикле whileO
проверяются на неравенство ключей. Значит, после окончания цикла whileO в контейнере
chg могут остаться несколько итераторов с равными ключами — последний набор
анаграмм.
Вот их-то (если они есть) и выводят на экран инструкции:
if(chg.sizeC) > 1)
for(unsigned int i-0; i < chg.sizeO; i++)
cout « (*chg[i]).second « endl;
Сначала программа проверяет, сколько итераторов осталось в контейнере. Если
их два и больше, то это анаграммы. После их вывода на экран остается только
закрыть файл инструкцией infile.closeO, и программа, дойдя до последней
фигурной скобки, завершится.
Обратите внимание на то, что в условной инструкции if (chg.sizeO > 1) нет
фигурных скобок, обрамляющих инструкции. Такая запись возможна, когда
инструкция, выполняемая при истинности условия, всего одна. Цикл for О,
показывающий анаграммы на экране, можно принять за две инструкции, но на самом деле это
одна большая инструкция, занимающая две строки, ведь количество инструкций
определяются числом точек с запятой, а не строк.
Задача, решенная в этом разделе, на порядок сложнее всего того, что нам
встречалось до сих пор. Но все-таки в ней стоит разобраться — если не сейчас, то спустя
некоторое время. Прежде всего потому, что она «настоящая», очень похожая на
те, что приходится решать профессиональным программистам. Эта задача —
своеобразный тест. Тот, кто легко сможет в ней разобраться, зря читает эту книжку.
Немедленно прекратите и отдайте книгу менее искушенному товарищу.
Стиль
В программе поиска анаграмм мы применили ряд «украшений»: выделили
отступами тела циклов, а также инструкции, выполняемые при истинности или ложно-
60
Глава 3. Строки и контейнеры
сти условий. Такая работа может показаться бесполезной, потому что компилятор
ничего этого не понимает и ему все равно, как будут расставлены скобки. Цикл
while (i <= N) {
sum = sum + i;
i++:
}
можно записать в одну строчку
while(i<=N){sum=sum+i ;i++;}
без всяких пробелов и отступов, или лесенкой
while
(i
<=
N
){sum=sum+i;i++;}
а компилятор все равно превратит его в ту же самую последовательность команд
процессора. Но поймет ли его программист две недели спустя?
Если говорить об этом конкретном цикле, то, наверное, — да. Но если циклы
и условия вложены друг в друга, небрежно оформленная программа очень скоро
становится непонятной даже ее создателю. Кто же тогда сможет исправлять ее,
добавлять новые возможности и т. д.? Вот почему программа должна быть простой
и понятной.
Простота — страшная сила. Как правило, она обманчива. И то, что выглядит
простым и легким, требует на самом деле огромного труда. Простые программы
создаются далеко не с первой попытки, и чем опытнее программист, тем чаще он
возвращается к написанной программе и старается сделать ее лучше.
Как и всякое произведение, программа должна быть красивой. Но красоту каждый
понимает по-своему. И далеко не всем нравится расположение фигурных скобок,
принятое в листинге 3.13. Многие предпочли бы расставить их так:
whileO
{
Г
Такое расположение скобок более симметрично, но требует лишней строки, а текст
программы воспринимается лучше, когда он записан компактно, но все же
отдельные инструкции нужно помещать в разных строках, потому что вытягивание
циклов в одну линию затрудняет их понимание и модификацию.
Так же трудно решить, стоит ли выделять фигурными скобками единственную
инструкцию в цикле или в условии. Запись без фигурных скобок
if(chg.size() > 1)
for(unsigned int i - 0; i < chg.sizeO: i++)
cout « (*chg[i]).second « end!;
Стиль
61
более компактна. Зато фигурные скобки не только делают условие и цикл
понятнее, но и позволяют легко добавить новые инструкции.
if(chg.size() > 1) {
forCunsigned int i = 0; i < chg.sizeO; i++) {
cout « (*chg[i]).second « endl;
} '"
} •
Поэтому многие программисты всегда выделяют фигурными скобками даже
единственные инструкции в цикле.
Очень много значат комментарии. Они должны быть краткими и содержать
новую информацию, которой нет в исходном тексте программы. Например,
комментарий
if(chg.size() > 1)// размер вектора больше 1?
совершенно бесполезен, потому что программист и так видит, что размер
контейнера сравнивается с единицей. А комментарий
if(chg.sizeO > 1)// остались еще анаграммы?
придает смысл абстрактному объекту chg, показывая, что там еще могут
оставаться анаграммы.
Комментарии очень важны, но они не могут подменить простой и логичной
структуры самой программы. Всякий комментарий немного портит исходный текст
и полезен только в том случае, когда сообщает о чем-то важном. Очень часто
комментарии помещают внутрь циклов
if(chg.size() > 1) {
// выведем на экран оставшиеся анаграммы
for(unsigned int i - 0; i < chg.sizeO; i++) {
// показать анаграмму и перевести строку
cout « (*chg[i]).second « endl;
}
L
что делает программу абсолютно нечитаемой. Нужно стараться располагать
комментарии правее инструкций и отводить им целые строки только перед большими
участками программы, выполняющими какую-то конкретную задачу.
Пониманию программ очень помогают и разумные имена объектов. Имена
должны соответствовать роли объектов в программе и быть достаточно длинны, чтобы
их легко можно было найти с помощью контекстного поиска. Имя, в котором всего
одна буква, невозможно найти в программе, что обернется кошмаром при попытке
что-то исправить или изменить. С другой стороны, слишком длинное имя трудно
набирать на клавиатуре и инструкции, содержащие несколько таких имен,
окажутся настолько растянутыми, что придется занять несколько строк, а это сделает
программу менее понятной. В листинге 3.13 я, пожалуй, выбрал слишком короткие
имена — просто потому, что листинги в книге слишком узки. Реальный листинг
может быть гораздо шире и содержать более длинные имена. Но и в листинге 3.13
62
Глава 3. Строки и контейнеры
имена имеют смысл, иногда скрытый. Так, например, называя вектор, хранящий
анаграммы, именем chg, я представлял себе обойму (по-английски charger), куда
анаграммы заталкиваются, подобно патронам. Такие зрительные образы очень
помогают ориентироваться в программе и понимать, как она работает. Естественно,
образы, возникающие при работе над программой, у каждого свои. И нужно
использовать то, что пришло в голову именно вам, а не чужому дяде.
В этом разделе я не пытался навязать вам правила хорошего тона в
программировании. Вы должны сами решить, что прилично, а что нет. Но раз решив, следует
неукоснительно придерживаться собственных правил, чтобы сделать программу
предсказуемой и легко изменяемой.
Глава 4
Функции, указатели и ссылки
Сделай функцию сам
Функции, встречавшиеся нам в предыдущих главах, были двух разновидностей:
«самостоятельные», то есть не связанные с какими-либо объектами (такова
функция sortO), и прикрепленные к объекту точкой (собственные функции объектов),
такие, например, как infile.openO.
Независимо от того, свободна функция или подчинена объекту, она чаще всего
принимает что-то извне (функция infile.openO принимает имя файла) и
довольно часто возвращает что-то тому, кто ее вызвал.
Функции очень широко используются в C++, потому что позволяют разбить
сложную задачу на несколько более простых. Один раз написанная функция
может применяться многократно. Собственно, таковы стандартные функции sortO,
random_shuffle(), accumulateO1, уже встречавшиеся нам.
Но как бы ни был велик набор стандартных функций, всегда возникает
потребность в своей функции, решающей часть конкретной задачи. Программа,
показанная в листинге 4.1, использует функцию пп'пЗО, возвращающую минимальное из
трех целых чисел.
Листинг 4.1. Устройство функции
#include <iostream>
using namespace std;
int min3(int x. int y. int z) {
int min = x;
if(y < min) min = y:
if(z < min) min = z;
return min;
}
int mainO {
cout « min3B. 5. -5) « endl;
}
1 Эти стандартные функции C++ работают с итераторами, их часто называют алгоритмами.
64
Глава 4. Функции, указатели и ссылки
В этой программе после строки using namespace std идет описание функции min3(),
состоящее из заголовка int min3(int x, int у, int z) и тела функции,
заключенного в фигурные скобки. В круглых скобках помещен список параметров функции,
то есть список объектов, с которыми функция имеет дело. В нашем случае это
три переменных типа int. Перед списком параметров тоже стоит идентификатор
типа int, а это значит, что функция min3() имеет тип int и ее можно использовать
в арифметических выражениях, как простое число. Посмотрите еще раз на
единственную строку главной программы:
cout « min3B. 5. -5) « endl;
В этой строке некое число min3B, 5, -5) выводится на экран. Разделенные запятой
цифры B,5, -5) — это три числа, поступающие на вход функции, чтобы она
определила самое маленькое из них. Эти числа называются аргументами функции. Число
и типы аргументов должны соответствовать числу и типам параметров,
перечисленных в заголовке функции. В нашем случае это три переменные типа int.
Теперь самое время взглянуть на внутреннее устройство функции, в которой
обрабатываются поступившие на вход аргументы:
int min3(int x. int у. int z) {
int min=x;
if(у < min) min=y;
if(z < min) min=z;
return min;
}
Когда вызывается функция min3B, 5, -5), аргументы занимают место
параметров. Иными словами, параметр х становится равным 2, параметр у — 5, параметр
z 5. Далее внутри функции создается переменная min и ей присваивается
значение первого параметра, то есть 2. Затем min сравнивается со вторым аргументом.
Если у < min, переменная min становится равной у. В нашем случае у = 5, поэтому
условие не выполняется, переменная min остается прежней, и программа
переходит к следующей строке, где сравниваются z и min. В нашем случае z = -5, поэтому
условие z < min выполняется, и переменная min становится равной -5. И наконец,
инструкция return min возвращает значение min во внешний мир, после чего на
экране появляется -5.
Познакомившись с тем, как функции обрабатывают аргументы и возвращают
значения, уместно вспомнить о значках int main(){}, обрамляющих каждую
программу на C++. Сейчас нам уже ясно, что main() — тоже функция, не имеющая
параметров1 и возвращающая значения int. Отличие main от других функций в том,
что она— самая главная, возвращающая значение int тому, кто еще главнее, то
есть операционной системе (например, Windows XP). Поэтому инструкции return
внутри main() обычно содержат коды завершения, например:
return 0;
1 Скоро (в разделе «Массив указателей») мы узнаем, что параметры у функции main() все-таки есть
и научимся ими пользоваться.
Функции-тезки
65
Число 0, возвращаемое функцией main, означает, что все в порядке. Другие
целые числа говорят операционной системе об аварийном завершении программы
и его причинах. До сих пор мы не использовали инструкцию return в функции
mainOnpocro потому, что ничего о ней не знали. Вообще, согласно правилам C++,
у всех функций, которые что-то возвращают, должна быть инструкция return. Но
для функции main сделано исключение: если инструкции return нет, то main все
равно возвращает 0 — признак удачного завершения. Поэтому в большинстве
наших программ mainO обойдется без инструкции return. Когда же хочется показать
операционной системе, что программа завершилась неудачно, mai п() должна иметь
инструкции return с подходящими кодами завершения, например return 2.
Функции-тезки
Функция пппЗО, созданная нами в предыдущем разделе, работает только с
целыми числами. Конечно, ее аргументы могут быть и типа double, например min3B, 5,
-5.5), в этом случае компилятор выдаст предупреждение и попытается привести
«инородную» переменную к типу int. Но тогда потеряется дробная часть, и в
результате минимальное значение, определенное функцией, все равно будет -5,
а не -5.5.
Значит, для переменных типа double нужна функция с другими параметрами
и другим типом возвращаемого результата. Программа, показанная в листинге 4.2,
содержит описания двух функций. Первая определяет минимальное из трех
целых чисел, вторая делает то же самое, но уже с числами типа double.
Листинг 4.2. Функции на все случаи жизни
#include <iostream>
using namespace std;
int min3(int x. int y. int z){
int min=x;
if(y < min) min = y;
if(z < min) min = z;
return min;
}
double min3(double x. double y. double z){
double min=x;
if(y < min) min = y;
if(z < min) min = z;
return min;
}
int main(){
int a = 2. b = 5. c= -5;
cout « min3(a*c. b. c) « endl; // -10
cout « min3@.03*a. 10.0. 5.0) « endl; // 0.06
}
Компилятор C++ ничуть не смущает, что две функции, работающие со
значениями разной природы, называются одинаково. Когда вызывается min3() с
целочисленными аргументами (обратите внимание, это могут быть не только числа, но
66
Глава 4. Функции, указатели и ссылки
переменные и выражения1 соответствующего типа), компилятор решает, что
нужно использовать первую функцию, возвращающую целое. Если же используются
аргументы типа double, вызывается вторая функция с тем же названием2, но, по
сути, совсем другая. Когда же мы попытаемся «смешать» типы аргументов, сделав,
например, вызов miпЗB,0.003.10), компилятор не найдет нужной функции среди
имеющихся и выдаст сообщение об ошибке.
Заметим, что функции с одним и тем же именем могут иметь в C++ разное число
аргументов. Если тип и число аргументов соответствуют описанию функции,
компилятор без колебаний выберет нужную.
Все возможные функции можно описать одним шаблоном, и компилятор,
проанализировав аргументы, сам создаст нужную функцию. В программе из листинга 4.3
набор функций, вычисляющих минимальные значения переменных, задается
шаблоном tempi ate <typename Т>, где Т обозначает тип аргументов и возвращаемых
функцией значений3 (ключевое слово typename переводится как «имя типа»).
Листинг 4.3. Пример шаблона функции
#include <iostream>
using namespace std:
template <typename T>
T min3(T x, T у. Т z){
T min = x;
if(y < min) min = y;
if(z < min) min = z;
return min;
}
int mainO {
int a = 2. b = 5. c= -5;
cout « min3(a*c. b. c) « endl; // -10
cout « min3@.03*a. 10.0. 5.0) « endl: // 0.06
}
Встретив вызов функции min3(a*c,b,c), компилятор обнаружит целочисленные
аргументы и, пользуясь шаблоном, сам создаст нужную функцию. Компилятор не
ошибется, если все три аргумента будут одного типа. Если же аргументы будут
разными, подходящий шаблон не найдется и компилятор выдаст сообщение об
ошибке.
Параллельные миры
В предыдущих разделах мы знакомились с функцией, принимающей три
аргумента и возвращающей минимальное из полученных трех значений. Но бывают функ-
1 Выражения — это переменные, соединенные операторами, например а+Ь.
2 При втором вызове функции используется «смешанный» аргумент -0.03*а — произведение
константы типа double на целочисленную переменную. Но поскольку целое число в смешанном выражении
«подтягивается» к типу double, сам аргумент тоже будет типа double.
3 Вместо буквы «Т» в шаблоне можно поставить любую другую букву или слово.
Параллельные миры
67
ции, которым нечего возвращать, созданные, например, для обмена значениями
двух однотипных переменных. Такие функции объявляются со словом void:
void exchngdnt a. int b){} // функция ничего не возвращает
Пусть, например, переменная а равна двум, а переменная b — пяти. Вызываем
функцию exchng(a, b) — и вот уже в переменной а — пятерка, а в переменной b —
двойка.
Программа, показанная в листинге 4.4, по идее, должна решать эту задачу.
Листинг 4.4. Попытка поменять местами аргументы
#include <iostream>
using namespace std;
void exchng(int. int):
int mainO {
int a = 2. b = 5;
exchng(a.b):
cout « "a = " « a « endl; // a = 2
cout « "b = " « b « endl; // b = 5
}
void exchng(int a. int b) {
int tmp;
tmp = a;
a = b;
b = tmp;
}
В листинге 4.4 функция exchgO сначала только объявлена
void exchngdnt . int );
чтобы компилятор не удивился, встретив в программе ее имя1. В объявлении
функции не обязательно указывать имена параметров, потому что компилятору
достаточно знать только их тип. Полное определение exchngO дается здесь после
функции mainO, чтобы вся программа стала более понятной. В дальнейшем мы
часто будем только объявлять функции в начале программы, оставляя их полное
определение «на потом».
Функция exchng(a, b) ничего не возвращает во внешний мир, поэтому она не
участвует в каких-то действиях, а просто вызывается инструкцией exchng(a, b);. После
вызова исполняются инструкции внутри функции, пока не встретится
завершающая фигурная скобка. Она — сигнал возврата в функцию, вызвавшую exchng(a, b),
то есть в mainO. После возврата программа должна перейти к следующей за
вызовом функции, в нашем случае это cout « "а = " « а « endl;, и хочется, чтобы она
показала на экране а = 5, а затем b = 2. Но программа упрямо выводит на экран а = 2,
b = 5, потому что функция exchngdnt a, int b) не может повлиять на переменные
а и b основной программы.
1 Как и человек, компилятор читает исходные тексты сверху вниз. Сначала ему нужно знать, какие
у функции параметры, чтобы правильно ее вызвать. А само устройство функции понадобится ему
позднее.
68
Глава 4. Функции указатели и ссылки
Все дело в том, что параметры а и b функции exchng( i nt a, i nt b) не имеют никакого
отношения к переменным а и b основной программы. То, что они так же
называются — простое совпадение. Переменные основной программы и параметры
функции соприкасаются лишь в момент ее вызова. Тогда параметр а становится
равным двум, а параметр b — пяти. Далее функция обменивает значения параметров,
так что b внутри функции становится равным 2, а параметр а — пяти. Но лишь на
мгновение. Когда происходит возврат в основную программу, переменные,
«жившие» внутри функции, пропадают, их больше нет. А переменные основной
программы а и b остаются теми же. Можно сказать, что переменные, попавшие в
функцию, оказываются в параллельном мире, из которого можно вернуться в «наш»
мир только с помощью инструкции return.
Рекурсия, или «Раз, два, три»
Не следует надолго уходить в себя, так как
близкие нуждаются в общении с вами.
Из гороскопа
Эти параллельные миры могут оказаться вложенными друг в друга, когда вызов
функций происходит «по цепочке», как, например, в программе из листинга 4.5,
выводящей на экран целые числа от 1 до 3.
Листинг 4.5. Вывод на экран трех чисел
#include <iostream>
using namespace std:
void CntTo3(int);
void CntTo2(int);
void CntTol(int);
int mainO {
int n:
CntTo3C);
}
void CntTo3(int p) {
CntTo2(p - 1);
cout « p « endl;
}
void CntTo2(int p){
CntToHp - 1);
cout « p « endl;
}
void CntToKint p){
cout « p « endl;
}
В ней работа все время перепоручается подчиненной функции — в надежде, что
вывод меньшего количества чисел на экран станет проще. Так, функция CntTo3C)
должна вывести на экран три числа: 1, 2, 3, но вместо этого она поручает функции
CntTo2() вывести два числа: 1 и 2. A CntTo2() в свою очередь вызывает функцию
CntTolO, которой уже некуда отступать, и она выводит на экран единицу, а затем
Рекурсия, или «Раз, два, три»
69
передает управление вызвавшей функции, то есть CntTo2(), где должна выполниться
инструкция, непосредственно следующая за вызовом функции CntTol(p-l), то есть
cout « р « end!;
Вопрос только в том, какое число окажется теперь на экране. Чтобы ответить на него,
вспомним, что у каждой функции свой собственный мир со своими значениями
параметров. Чему же равен параметр р в мире функции CntTo2()? Очевидно, р равен
переданному функции CntTo2() значению, то есть двойке, которая и покажется на
экране вслед за ранее выведенной единицей. И управление вернется к инструкции,
непосредственно следующей за вызовом CntTo2(p - 1), то есть опять к инструкции
cout « р « endl;
но уже внутри функции CntTo3(). А в мире функции CntTo3() p равно значению,
переданному этой функции, то есть тройке. Значит, на экран будет выведена
тройка, управление вернется в функцию mainO, и, встретив закрывающую фигурную
скобку, программа завершится.
Теперь можно немножко обобщить нашу задачу. Предположим, необходимо
вывести на экран п подряд идущих чисел от 1 до п. Эту задачу можно разбить
на две:
1. Вывести числа от 1 до п - 1.
2. Вывести п.
Точно так же задача вывода чисел от 1 до п - 1 разбивается на две:
1. Вывести числа от 1 до п - 2.
2. Вывести п- 1.
и так далее, вплоть до единицы.
Такой способ решения задачи называется рекурсией. По сути, рекурсия — частный
случай стратегии «разделяй и властвуй». Если от задачи можно «отколоть»
кусочек, то нужно это сделать, потому что оставшаяся задача будет проще.
Как же решать рекурсивные задачи на компьютере? Приведенный способ (см.
листинг 4.5) годится для вывода трех чисел, но для вывода сотни он слишком
громоздок и попросту глуп.
Но попробуем все-таки представить, как выглядела бы программа, показанная
в листинге 4.5, если бы ее задачей было вывести не три подряд идущих числа,
а сотню. Очевидно, такая программа принципиально мало отличалась бы от той,
что выводит три числа. Только вместо трех функций в ней было бы сто! Причем 99
из этих ста функций выполняли бы абсолютно одинаковые действия:
void CntTolOOOnt p) {
CntTo99(p - 1);
cout « р « endl:
}
void CntTo99(int p) {
CntTo98(p - 1);
cout « p « endl;
}
70
Глава 4. Функции, указатели и ссылки
void CntTo2(int p) {
CntToKp - 1):
cout « р « endl;
}
и только последняя, оказавшись «крайней» просто выводила бы на экран единицу!
То, что все эти 99 функций, по сути, отличаются только названием, наводит на
мысль: а нельзя ли записать это в виде одной-единственной функции, которая
99 раз обращается сама к себе?
И такая возможность действительно есть, причем у любой функции, кроме mainO.
Функции, которые вызывают сами себя, так же как задачи, для решения которых
они созданы, называются рекурсивными. Глядя на листинг 4.5, легко написать
программу, использующую рекурсивную функцию для вывода идущих подряд
цифр. Выглядеть она может так, как показано в листинге 4.6.
Листинг 4.6. Рекурсивный вывод чисел на экран
#include <iostream>
using namespace std;
void CntTo(int);
int mainO {
int n;
CntToO);
}
void CntTo(int n){
if(n > 0) {
CntTo(n - 1);
cout « n « end!;
}
}
Используемая здесь функция CntToO может вывести на экран любую
последовательность чисел, но для простоты мы по-прежнему будем выводить 1, 2,3.
При первом обращении к функции CntToO значение аргумента равно 3, и так как
выражение (п > 0) истинно, CntToO вызовется еще раз, но уже с аргументом п-1,
равным двойке. Далее, раз двойка больше нуля, то CntToO вызовется в третий раз,
уже с единичным аргументом. Единица тоже больше нуля, и CntTo() будет вызвана
в четвертый раз. На этот раз п равно 0, условие (п > 0) не выполнится, программа
сразу перейдет к закрывающей фигурной скобке в функции CntToO, и кажется, что
выполнение функции CntToO теперь должно завершиться. Но достижение
закрывающей фигурной скобки означает не завершение работы, а возврат в вызвавшую
функцию, причем к инструкции, непосредственно следующей за вызовом. Но
откуда вызывалась функция CntToO? Очевидно, из себя самой! Значит, и вернуться
она должна в себя саму, вернее в ту свою копию, из которой она была вызвана.
В нашем случае после четвертого вызова, когда п было равно нулю, функция
возвращается к инструкции, следующей непосредственно за вызовом, то есть к cout«
n « endl;. Причем п в данном случае — не какое-то абстрактное число, а значение
параметра п в третьей копии CntToO, то есть единица. Показав на экране «1», тре-
Рекурсия или «раз, два, три»
71
тья копия CntTo() вернется к вызвавшей ее второй копии, опять в то место, которое
следует непосредственно за вызовом, то есть снова к cout« п «1;, но на этот раз п
будет равно двум. Наконец, при третьем возврате будет выведена тройка.
Рис. 4.1. Закат и восход рекурсивной функции
На рис. 4.1 показано, как функция CntToO «углубляется в себя». Если бы не условие
if (n > 0){}, функция ушла бы в себя и не вернулась. Поскольку вызов каждой копии
функции требует памяти для хранения переменных, то после исчерпания памяти
программа должна была бы аварийно завершиться. Инструкция i f () разрешает
функции вызвать себя только четыре раза. С каждым разом функция уходит все глубже
в себя, она «закатывается». Когда п становится равным нулю, начинается «восход».
Функция возвращается в свои более ранние копии, пока не высветятся все три числа.
Затем она взойдет еще на одну ступеньку — и окажется в функции main().
Наш предыдущий пример был весьма искусственным. Выводить числа на экран
гораздо удобнее в цикле for:
for(int i =1: i <= N; i++)
cout « i « endl;
Но есть задачи, рекурсивные по своей природе, поэтому их удобней решать с
помощью рекурсивного вызова функций. Одна из таких задач — вычисление
факториала по формулам:
О! = 1
п! = п*(п-1)!
Как видите, факториал сам задается рекурсивно. Задача вычисления 3! сводится
к вычислению 2!, потому что 3! = 3x2!, в свою очередь, 2! = 2x1!, а 1! = 1x0! = 1.
В результате получим, что 3! = 3x2x1. Программа, вычисляющая 3!, показана
в листинге 4.7.
Листинг 4.7. Рекурсивное вычисление факториала п!
linclude <iostream>
using namespace std:
int factorial(int);
int mainO {
cout « factorialC) « endl;
}
int factorial(int n) {
if (n — 0).
return 1;
else
return n * factorial(n-1);
}
72
Глава 4. Функции, указатели и ссылки
В этой программе функция factorial О вызывает сама себя до тех пор, пока
аргумент не станет равен нулю. Тогда выполнится условие п = 0 и функция возвратит
единицу той своей копии, в которой п равно единице. Значит, «вверх» пойдет
единица (return 1*1);. Единица возвратится в ту копию функции, где п = 2, значит,
число 2 (return 1*2) будет возвращено туда, где п = 3 (return 2*3), и тогда уже
произойдет возврат в функцию mainO, где инструкция:
cout « factorialC) « endl;
выведет на экран число 6.
Функция factorial О как бы спускается в шахту (рис. 4.2), расставляя на этажах
цифры. На верхнем будет цифра 3, дальше 2, а в самом низу — единица.
Рис. 4.2. Рекурсивные вызовы функции factorialQ
Указатели
73
Расставив числа, функция начнет движение вверх, на ходу подбирая цифры и
умножая их. С нижнего этажа она возьмет единицу и умножит на единицу,
хранящуюся на верхнем этаже. Чуть выше найдутся двойка и тройка. Когда факториал
будет вычислен, мы окажемся на поверхности, в функции mainO.
Задача 4.1. Число п в степени р можно определить как
1,еслир = 0 и
п*пр - 1 еслир > 0.
Напишите программу, которая рекурсивно вычисляет пр.
Указатели
В своем собственном мире функция делает, что хочет, но внешний мир получает
лишь то, что возвращает инструкция return. Как же заставить функцию влиять на
переменные из внешнего мира? Ведь инструкции return может не хватить, когда
требуется вернуть несколько переменных (см. листинг 4.4, где функция пытается
поменять местами два объекта).
В этом случае поможет особый тип объекта C++, называемый указателем.
Указатель — это не сам объект, а его адрес. Чтобы превратить указатель в объект,
применяется оператор *. Если, скажем, р — это указатель на переменную типа int, то
*р — сама переменная типа int. Так, со звездочкой указатели и объявляются в
программе, чтобы показать, что *р — некое значение типа int:
int *p; // р - указатель на int
После объявления указатель еще не связан с объектом и указывает «пальцем в
небо». Чтобы привести указатель в рабочее состояние, необходимо присвоить ему
адрес реального объекта. Делается это оператором &.
Листинг 4.8 демонстрирует программу, где объявляется целочисленная
переменная а с начальным значением 2. Затем объявляется указатель р, и в него
засылается адрес переменной а, то есть р = &а. Как только в указателе р оказывается адрес,
возникает тайная тропа к переменной, и после выполнения инструкции *р = 5;
переменная а становится равной пяти!
Листинг 4.8. Указатель позволяет «подменить» объект
#include <iostream>
using namespace std;
int mainO {
int a = 2:
int *p; // p - указатель на int
p = &a;
cout « "*p = " « *p « end!; // *p=2
*p = 5; // a=5;
cout « "a = " « a « endl;
}
Теперь самое время вспомнить о неудачной функции exchngO из предыдущего
раздела и подумать, что случится, если передать ей не переменные, ^указатели на
74
Глава 4. Функции, указатели и ссылки
них. Конечно, сами указатели функция не сможет изменить, потому что получит
в свое полное распоряжение только их копии, то есть, по существу, совсем другие
объекты. Но что мешает применить к этим копиям оператор * и тем самым менять
переменные из внешнего мира?
Программа, показанная в листинге 4.9, пользуется измененной функцией exchngO,
принимающей указатели на две переменные. В ее заголовке voi d exchng( i nt *pl, i nt
*p2) имеется два параметра: указатель на целочисленную переменную pi и
указатель на такую же переменную р2.
Листинг 4.9. Передача функции указателей на объекты
#include <iostream>
using namespace std:
void exchng(int *pl. int *p2){
int tmp - *pl;
*pl - *p2:
*p2 = tmp;
}
int mainO {
int a = 2. b - 5;
exchng(&a. &b);
cout « "a-" « a « endl; // a=5
cout « "b=" « b « endl; // b=2
}
При вызове функции exchng(&a, &b) ей передаются два указателя (адреса): &а —
адрес переменной а, первоначально равной 2, и адрес переменной b — &b,
первоначально равной пяти. Внутри функции адрес переменной а оказывается в
параметре pi, а адрес переменной b — в параметре р2. Инструкция tmp = *pl пересылает
содержимое переменной а во временную переменную tmp, то есть tmp становится
равной двум. Далее переменной а присваивается значение b (*pl = *р2). В этот
момент а=5. И наконец, последняя инструкция *р2 = tmp пересылает в переменную Ь,
адрес которой хранит указатель р2, содержимое а, сохраненное в переменной tmp.
После возврата из функции а=5, Ь=2.
Листинг 4,9, в котором функция меняет переданные ей аргументы, пользуясь
указателями, заставляет вспомнить о ссылках, знакомых нам по разделу «Ссылки»
главы 1. Ссылка — это другое имя объекта в C++, и все, что делается со ссылкой,
отражается на объекте, и все, творимое с объектом, сказывается на ссылке. Отсюда
идея: будем передавать функции не объекты, а ссылки на них. И тогда функция
тоже сможет менять внешние объекты.
Программа, показанная в листинге 4.10, делает то же, что программа из
листинга 4.9 — меняет содержимое переменных а и Ь. Но на этот раз функция exchngO
принимает не указатели, а ссылки на переменные.
Листинг 4.10. Передача объектов по ссылке
#include <iostream>
using namespace std;
void exchng(int &a. int &b){
int tmp = a:
Указатели и массивы
75
а = Ь:
b e tmp;
}
int mainO {
int x - 2, у - 5;
exchng(x. у);
cout « "x = " « x « endl;
cout « "y = " « у « endl:
}
О том, что функция получает ссылки, а не указатели, говорит заголовок функции
void exchngCint &a, int &b), где в качестве параметров указаны две ссылки. Это
значит, что при вызове функции exchng(x.y) создаются две ссылки: а ссылается на х,
a b — на у. И теперь любые изменения а и b внутри функции отразятся на
связанных с ними внешних объектах х и у.
Хоть передача указателей и передача ссылок приводят к одному и тому же
результату, ссылки считаются более удобными и чаще используются в C++. Функцию,
принимающую ссылки, легче написать, потому что не нужны бесконечные
звездочки перед именами параметров. Но, с другой стороны, указатели делают
работу функции совершенно прозрачной, а ссылки скрывают от программиста то, что
происходит на самом деле1. Кроме того, вызов функции при передаче ссылок
смотрится так же, как и при обычной передаче аргументов (ее называют «передачей по
значению»), что может привести к путанице. Впрочем, выбора — применять или
не применять ссылки — не существует. Ссылки встречаются в программах на C++
постоянно. Значит, нужно к ним привыкнуть и умело ими пользоваться.
Указатели и массивы
Мы уже знаем, что указатели инициализируются оператором &. Инструкция int
*р = &а; объявляет указатель на int и присваивает ему начальное значение — адрес
переменной а. Но в указателе может храниться не только адрес ранее
объявленной переменной, но и начальный адрес области памяти, выделяемой специальным
оператором new. Следующие инструкции создают указатель р — (int *р;),
выделяют область памяти, куда умещается целочисленная переменная (new int), и
записывают в указатель адрес этой области:
int *p;
р = new int;
Теперь оператор *, примененный к указателю, позволит использовать эту область
памяти как обычную переменную. Например, инструкция *р = 2 засылает в память,
на которую указывает р, число 2.
1 А на самом деле компилятор рассматривает ссылки как указатели, которые нельзя «перенаправить»
на другие объекты. Встретив объявление ссылки int &b = а, компилятор преобразует его в int *b = &a
и дальше всюду, где встречается Ь, ставит *Ь.
76
Глава 4. Функции, указатели и ссылки
Оператор new, с которым мы только что познакомились, способен выделить
память и для нескольких расположенных рядом переменных. Их число
указывается в квадратных скобках вслед за типом переменной. Например, следующая
инструкция выделяет память для десяти расположенных рядом переменных типа
int и помещает адрес ее начала в указатель р:
int *p;
р = new int[10]:
К нулевой переменной в этом ряду можно, как обычно, обратиться с помощью
оператора *. Инструкция *р - 0 засылает туда число ноль. Но как получить доступ
к первой, второй и т. д. переменным?
Для этого в C++ существует правило: адрес следующей переменной получается
прибавлением единицы к указателю на предыдущую. Если р указывает на
нулевую переменную, выделенную оператором new, то р+1 — на первую, р+2 — на вторую
и т. д. Инструкция *(р+4)=5 засылает пятерку в четвертую из десяти
расположенных одна за другой переменных, выделенных с помощью оператора new (рис. 4.3).
Естественно, вместо цифр можно использовать целочисленные переменные, то
есть *(p+i) равно четвертой переменной в последовательности, если i=4.
Р
Рис. 4.3. Адресация переменных с помощью указателей
Иными словами, увеличение на единицу продвигает указатель к следующей
переменной, соответствующей его типу. Указатель на переменную int, занимающую
4 байта, после прибавления единицы продвинется вперед на четыре байта и будет
указывать на следующую переменную. Указатель на char от прибавления
единицы продвинется вперед на 1 байт, указатель на double — скакнет на целых 8 и т. д1.
В любом случае переход к следующей переменной происходит автоматически,
и программисту не нужно думать, сколько байтов она занимает.
А теперь попробуем взглянуть на память, выделяемую оператором new,
по-другому.
Что такое участок памяти, заполненный идущими друг за другом одинаковыми
переменными? Очевидно, это массив и разумно обращаться с такой памятью как
с массивом. А поскольку адрес начала этого участка хранит указатель р, в C++ раз-
1 В зависимости от процессора и компилятора переменная в C++ может занимать разное число байтов.
Используемый нами компилятор gcc выделяет для переменной int 4 байта. Этот размер, как впрочем и
все остальные, используемые gcc, очень типичен.
Указатели и массивы
77
решено писать вместо *(p+i)=2, где i — целочисленная переменная, просто p[i]=2.
То есть получается, что указатель р становится как бы именем массива.
Но что такое тогда настоящий массив? Раз имена массива и указателя
равноправны и практически неотличимы, то разумно и с именем массива обращаться как
с указателем на его нулевой элемент. Иными словами, если а — имя
«настоящего» массива, то загнать тройку во второй его элемент можно инструкцией *(а+2)=3,
а не только инструкцией а[2]=3. И это действительно так. Имя «настоящего»
массива компилятор рассматривает как указатель на его нулевой элемент и, встретив
инструкцию а[2]=3, превращает ее в *(а+2)=3.
Если имя массива указывает на его нулевой элемент, то можно «настроить»
указатель на начало массива, просто приравняв имя указателя и имя массива.
Вместо инструкции p=&z[0];, посылающей в указатель р адрес нулевого элемента
(то есть начала) массива z, можно просто записать p=z, после чего р становится
как бы вторым именем массива, а инструкции p[i]=2 и z[i]=2 оказываются
эквивалентными.
Следует ли из всего этого, что имя массива эквивалентно имени указателя? Ни
в коем случае. Ведь указатель — это некая область памяти, где хранится адрес
другой области памяти. Указателю можно придать новое значение, тем самым
перенаправив его на другой объект. Ничего подобного нельзя сделать с именем
массива, навсегда обреченного указывать в одно место — на его нулевой элемент. Если
р — указатель, то можно написать р++, увеличив указатель на единицу и тем самым
передвинув его к следующей переменной. Однако инструкция а++ (где а — имя
массива) вызовет сообщение компилятора об ошибке.
Из всего сказанного может сложиться впечатление, что массивы, созданные
оператором new, и «настоящие» массивы, объявленные в программе, ничем не
отличаются, ведь к элементу и того и другого можно обратиться по указателю p[i]
независимо от того, что такое р — имя указателя или имя массива. Но одно (причем
существенное) различие все же есть. Обычные переменные и массивы,
определенные внутри функций, пропадают при возвращении из функции во «внешний
мир». Однако память, выделенная оператором new, сама не освобождается. Если
ничего не делать при выходе из функции, память останется занятой, и программа
уже не сможет ею воспользоваться. Если функция часто вызывается, памяти будет
оставаться все меньше. Наступит момент, когда ее не останется вовсе, и программа
аварийно завершится. Чтобы этого не произошло, нужно перед выходом из
функции освобождать память оператором delete:
void something(){
int *p = new int[10];
delete []p;
}
Обратите внимание на квадратные скобки, стоящие между оператором delete
и именем указателя. Без них оператор delete освободил бы память, занимаемую
только нулевым элементом созданного оператором new массива. Квадратные
скобки показывают, что нужно освободить память, занимаемую всеми десятью пере-
78
Глава 4. Функции указатели и ссылки
менными. Подробнее о жизни переменных и освобождении памяти
рассказывается в главе 8.
Задача 4.2. Что делает представленная ниже программа? Какое число она
покажет на экране?
#include <iostream>
using namespace std;
int mainO {
int a[5]={l. 2. 3. 4. 5};
int *p = &a[0];
int s=0:
for(int i = 0; i < 5; i++)
s += *p++:
cout « s « endl:
return 0;
}
Зная связь указателей и массивов, легко понять, как массивы передаются
функции. Очевидно, функции, чтобы работать с элементами массива, достаточно знать
их число и получить указатель на нулевой элемент. Объявление функции f(),
принимающей массив а, состоящий из п элементов типа char, и возвращающей
значение int, выглядит так:
int f(int n. char a[]);
Запись char[] показывает, что функция принимает массив, а отсутствие размера
в квадратных скобках говорит о том, что это не важно, все равно функция знает
только указатель на нулевой элемент, а размер передается в другой переменной.
Чтобы подчеркнуть, что функции передается именно указатель, а не сам массив, ее
прототип можно записать так:
int f(int n. char *a);
Последняя запись лучше отражает суть происходящего, зато предыдущая более
понятна тем, кто знаком с языком Бейсик или Фортран. В листинге 4.11 показано,
как функция mean () вычисляет среднее значение элементов массива.
Листинг 4.11. Передача функции массива
#include <iostream>
using namespace std:
double mean(int n. double *a){
double sum = 0:
for(int i = 0: i < n; i++)
sum += a[i];
return sum / n;
}
mainO {
double t[>{36.6.37.6.38.5.41.5.36.3.36.5}:
cout « meanF.t) « endl:
}
Заметим, что размер массива t[] автоматически определяется компилятором по
числу начальных значений его элементов в фигурных скобках. Функции mean О
Массив указателей
79
передается размер массива (в нашем случае — 6) и указатель на его нулевой
элемент, то есть просто имя t. Доступ ко всем элементам массива, обеспеченный
указателем, функция mean О использует честно — для нахождения среднего. Но
ничто не мешает ей попробовать записать число туда, где массива уже нет, например
в седьмой его элемент, и тем самым разрушить программу. Нужно понимать, что
компилятор не контролирует выход за границы массива, считая, что этим должен
заниматься программист. Здесь таится большая опасность, поэтому обращаться
с массивами и указателями следует очень осторожно.
Занимаясь указателями, нельзя не вспомнить итераторы — объекты, созданные
для передвижения внутри контейнеров (см. раздел «Итераторы и алгоритмы»
в главе 3). Для итераторов тоже существует оператор *, с помощью которого
можно получить значение.элемента контейнера. В целом, итераторы и указатели очень
похожи. Указатели также можно использовать для передвижения, но не внутри
контейнера, а внутри обычного массива.
Массив указателей
Наши знания массивов и указателей возросли настолько, что становится
возможным изучить давно скрываемые параметры функции main(). Заглянув в
пространство между круглыми скобками, увидим два параметра:
int mainCint argc. char *argv[]){}
Первый из них (целочисленная переменная argc) нам вполне понятен. Зато второй
выглядит странно. Что такое char *argv[] — массив или указатель? И то и другое.
Запись char *argv[] обозначает в C++ массив указателей, то есть массив, в
котором хранятся указатели на тип char. Размер этого массива не указан, но, вспомнив
функцию mean() из предыдущего раздела, легко догадаться, что он задается
параметром argc.
Теперь нужно понять, что и при каких обстоятельствах передается функции mai n().
Оказывается, в массиве argv[] помещаются указатели на отдельные элементы
командной строки программы, запускаемой в окне DOS. Одна из таких программ
хорошо нам известна — это компилятор дсс. Представим себе, что необходимо
скомпилировать программу test.cpp. Для этого нужно перейти в папку, где
хранится соответствующий файл, и запустить компилятор, указав в его командной
строке следующую команду:
gcc test.cpp -о test.exe
В этом случае компилятор дсс получит параметры, показанные на рис. 4.4. Нулевой
параметр хранит имя программы и полный путь к ней. Если, скажем, путь к
программе дсс.ехе равен f:\gcc\bin\, то нулевой параметр будет равен f:\gcc\bin\gcc.exe1),
он передается всегда, поэтому argc не может быть меньше единицы. Остальные па-
1 Некоторые компиляторы передают в argv[0] указатель только на имя программы, а не на полный
путь к ней.
80
Глава 4. Функции, указатели и ссылки
раметры добываются из командной строки, причем считается, что один параметр
от другого отделяет пробел (или несколько пробелов).
Итак, параметров, на которые указывают элементы argv[], в нашем случае четыре:
путь к программе и три параметра, передаваемых в командной строке: test. срр, - о,
test.ехе. Очевидно, argv[l] — это указатель на нулевой элемент массива, в котором
хранятся символы test.cpp\0. Всего их девять — семь букв, точка и завершающий
символ \0, добавляемый компилятором, чтобы обозначить конец
последовательности (подробнее о нем см. раздел «Массивы» в главе 3).
Рис. 4.4. Параметры, полученные компилятором дсс
Теперь мы знаем, как устроены параметры функции mainO, и можно применить
полученные знания в простой программе, открывающей файл, чье имя получено
из командной строки (см. листинг 4.12).
Листинг 4.12. Открытие файла
#include <iostream>
#include <fstream>
using namespace std;
int maindnt argc. char *argv[]) {
if(argc < 2) {
cout « 412.exe <файл>" « endl:
return 1:
}
ifstream infile(argv[l]):
if (infile.fail()){
cout « "Ошибка при открытии файла" « endl;
return 3;
}
infile.closeO;
}
Массив указателей
81
В этой программе сначала проверяется параметр агдс. Если он меньше двух, то
в командной строке ничего нет и программа завершается, показав на экране
краткое напоминание, как ей следует пользоваться1:
cout « 412.exe <файл>" « endl;
Если агдс больше единицы, то в командной строке есть хотя бы один параметр.
Мы считаем, что первый параметр — это имя файла, а остальные параметры (если
они есть) отбрасываем.
Получив имя файла, можно попробовать одновременно объявить и открыть его
инструкцией
ifstream infile(argv[l]);
в которой имя файла задается указателем argv[l]. Цифра 1 в квадратных
скобках не случайна, ведь argv[0], как мы знаем, указывает на путь к программе
(см. рис. 4.4). До сих пор мы задавали имя файла явно:
ifstream infile("diction");
и кажется, что указатель argv[l] здесь неуместен. На самом деле, именно указатель
на нулевой символ имени файла argv[l] здесь как раз и нужен, и явное задание
имени infileCdiction") — самообман. Увидев символы в кавычках, компилятор
отведет им определенное место в памяти, а функция infileC") получит указатель
на нулевой символ, то есть то же самое, что и argv[l].
Кстати, мы не ошиблись, называя infileO функцией. Это действительно
функция, знакомая нам по разделу «Приговор» главы 3 и называемая конструктором.
Конструктор— специальная собственная функция объекта infileO, которая
создает его, пользуясь переданными ей параметрами. В нашем случае конструктор
файла получает указатель на нулевой символ его имени, пытается открыть файл
и устанавливает состояние объекта infile в зависимости от того— удалось это
или нет. Причин, по которым файл не удается создать, — множество. Но самая
распространенная — неправильно указанное имя файла. Значит, программу
нужно защитить от такой ошибки, проверив возвращенное конструктором значение
if (infile.fail()){
cout « "Ошибка при открытии файла" « endl;
return 3;
}
Если возникла ошибка, собственная функция fail О объекта infile возвращает
true, условие выполняется и программа, показав на экране сообщение об
ошибке, прекращает работу. Если же все нормально, можно использовать содержимое
файла, но наша программа просто закрывает его функцией closeO и завершается,
на этот раз — в обычном режиме.
1 Здесь мы предполагаем, что имя программы I412.exe соответствует имени файла с исходным текстом
1412.срр.
82
Глава 4. Функции, указатели и ссылки
Задача 4.3. Перепишите программу нахождения анаграмм из листинга 3.13 так,
чтобы ей можно было указывать имя словаря в командной строке.
Чтобы лучше освоиться с параметрами функции mainO, наметим контуры еще
одной программы — архиватора, управляемого ключами, состоящими из дефиса
и последующей буквы. В нашем «игрушечном» архиваторе их три: а (добавить
в архив), е (распаковать) и 1 (выдать список архивных файлов). Программа,
распознающая эти ключи, показана в листинге 4.13.
Листинг 4.13. Набросок программы-архиватора arc
#include <iostream>
using namespace std;
int mainCint argc. char *argv[]) {
if (argc < 2) {
cout « "arc <-ael> файл" « endl;
return 1;
}
if(*argv[l] !- '-'){
cout « "неверный формат" « endl:
return 1;
}
char ch =* (argv[l] + 1);
switch(ch) {
case'a':
cout « "добавить" « endl;
break;
case'e':
cout « "распаковать" « endl;
break;
case'l':
cout « "листинг" « endl;
break;
default:
cout « "ошибка" « endl;
break:
}
return 0:
}
Инструкция if (argc < 2){} проверяет, есть ли параметры в командной строке. Если
аргумент argc равен единице, параметров нет и программа завершается, показав на
экране, как ею пользоваться.
Если аргумент argc равен двум и более, в командной строке есть параметр и можно
переходить к его разбору. Первым делом нужно выяснить, начинается ли
параметр с дефиса (-). Если это не так, программа завершается сообщением об ошибке.
Проверка параметра начинается следующей инструкцией:
if(*argv[l] != '-') {
Г
Как мы знаем, argv[l] — это указатель на нулевой символ первого параметра.
Оператор * превращает указатель на символ в сам символ, таким образом, условие
Указатель на указатель
83
(*агду[1]!= ' •') проверяет, равен ли нулевой символ первого параметра дефису.
Если да — разбор продолжается, если нет — программа заканчивает работу,
предварительно сообщив об ошибке.
Если нулевой символ параметра равен дефису, то нужно получить первый
символ, что и делает инструкция char ch=*(argv[l]+l). Смысл ее прост: argv[l] — это
указатель на нулевой символ. Очевидно, argv[l]+l указывает на первый символ.
Чтобы из указателя получить сам элемент, необходимо применить к нему
оператор *. Получается, что ch=*(argv[l]+l) и есть первый символ параметра.
Теперь нужно проверить, тот ли это символ, для чего используется условная
инструкция switchO, с которой мы еще не знакомы. Смысл ее прост. В круглых
скобках после слова switch показывается выражение, а его возможные значения
перечисляются строками case. Если выражение приняло указанное значение, то
выполняются инструкции, стоящие после case, а дальше инструкция break
передает управление программой за пределы переключателя (так обычно называется
инструкция switch). Без инструкции break программа перейдет к проверке
следующего значения, что нежелательно и может привести к путанице. Наконец, строка
default — это своеобразный «сборщик мусора», сюда попадают все значения, не
упомянутые строками case.
В нашем случае переключатель выводит на экран название операции — в том
случае, если он ее узнал. Если же после дефиса стоит неизвестный символ (не а, не е
и не 1), то переключатель передает управление инструкции default, на экране
появляется сообщение об ошибке и программа завершает работу.
Указатель на указатель
Массив указателей, с которым мы познакомились в предыдущем разделе,
неявно определяет новый объект, до сих пор нам не встречавшийся. Это указатель на
указатель.
Действительно, попробуем понять, что такое argv. Очевидно, это имя массива
указателей. И согласно нашему пониманию массива, argv указывает на нулевой его
элемент. Но что такое этот элемент? Очевидно, указатель. Значит, argv — это
указатель на указатель. Такие объекты объявляются в C++ как char **argv, поэтому
прототип функции mainO можно записать и так1:
int mainCint argc. char **argv):
Зная, что argv — указатель на указатель, можно немного по-другому
организовать разбор параметров командной строки. Очевидно, argv указывает на argv[0].
Прибавив единицу к argv, получим указатель на argv[l]. Применив к такому
указателю оператор *, получим само значение argv[l]. Но argv[l] — это указатель на
char. Чтобы получить само значение (нулевой символ первого параметра команд-
1 Объявление показывает, что оператор *, примененный к объекту argv дважды, дает значение char.
84
Глава 4. Функции, указатели и ссылки
ной строки), нужна еще одна «звездочка» **(argv+l). Иными словами, проверка
if (*argv[l] !='-') из листинга 4.13 может быть записана как if (**(argv+l) != ' -').
Еще любопытней можно записать равенство char ch = *(argv[l]+l) из того же
листинга. В данном случае нам нужно получить не нулевой, а первый символ
параметра, на который указывает argv[l]. Начнем, как обычно, с argv. Прибавление
единицы к argv даст указатель на первый элемент массива argv[], то есть на argv[l].
Чтобы получить argv[l], применим к argv+1 оператор *: *(argv+l). Но argv[l] —
тоже указатель, и чтобы получить первый элемент параметра, нужно прибавить
к *(argv+l) единицу и снова применить оператор *:
ch = *(*(argv+l)+l);
Из раздела «Массивы» нам известно, что *(argv+l) можно записать как argv[l].
Цоэтому предыдущее равенство можно представить и так:
ch = argv[l][l];
В такой записи нет ничего удивительного, ведь argv[l] указывает на нулевой
элемент массива. Следовательно, сами элементы получаются оператором [],
иагду[1][1] — не что иное, как первый символ первого параметра командной
строки.
Наконец, верна и совсем уж дикая запись, поскольку операторы ++ и *, имея
одинаковый приоритет, выполняются справа налево:
Ch=*++*++argv;
Значит, сначала argv увеличивается на единицу и указывает на argv[l]. Первая
звездочка, превращая указатель на argv[l] в само значение argv[l], заставляет нас
«свернуть с дороги», и мы продолжаем путешествие, но уже не по массиву argv[],
а по массиву argv[l][]. Второй оператор ++ передвигает указатель к первому
элементу массива argv[l][], а звездочка добывает сам этот элемент.
Задача 4.4. После присваивания ch=*++*++argv указатель argv меняется. На
сколько позиций назад нужно вернуть указатель argv, чтобы он по-прежнему указывал
Haargv[0]?
Указатель на массив
Среди многочисленных способов обращения к символам, из которых состоят
параметры командной строки (см. предыдущий раздел), нам встретился и такой:
argv[l][l]. В квадратных скобках, конечно же, могут стоять и целочисленные
переменные: argv[i][j]. Эта последняя запись наводит на мысль, что из указателя
на указатель может вырасти двухмерная структура данных, в которой есть строки
и столбцы. Такие структуры идеально подходят для хранения изображений или
матриц, правда, для этого они должны быть прямоугольными, то есть содержать
строки одного и того же размера. Один из способов создания такой структуры
демонстрирует программа, показанная в листинге 4.14.
Указатель на массив
85
Листинг 4.14. Двухмерная структура данных
#include <iostream>
using namespace std;
#define NR 20 //определяем число строк NR. равное 20
#define NC 40 //определяем число столбцов NC. равное 40
mainO {
int **pic; //pic - указатель на указатель на int
pic = new int *[NR];
for(int i = 0; i < NR; i++)
pic[i] = new int[NC]:
for(int i=0; i < NR; i++)
for(int j=0; j < NC; j++)
pic[i][j] = i * j;
cout « pic[5][6] « endl; //30
// освобождение памяти
for(int i = 0; i < NR; i++)
delete [] pic[i]; //уничтожаем строки
delete [] pic; //уничтожаем массив указателей
}
Труднее всего понять в этом листинге инструкцию
pic = new int *[NR];
присваивающую начальное значение указателю на указатель pic. Но все станет
ясно, если понять, что же такое в этой инструкции int *[NR]. Если бы не было
звездочки, оператор new int [NR] выделял бы память для NR переменных int. Но
звездочка говорит нам, что выделяется память для NR указателей на int, то есть
для массива указателей на int. Как обычно, создавая массив, оператор new должен
вернуть адрес его нулевого элемента. А поскольку создается массив указателей, то
адрес его нулевого элемента будет указателем на указатель. Значит, переменная
pic, объявленная как int **pic, будет после инструкции
pic = new int *[NR];
хранить указатель на указатель или, другими словами, адрес начала массива
указателей размером NR. Этот массив служит заготовкой для NR строк нашей
прямоугольной матрицы. В каждой ее строке будет NC переменных int, память для
которых выделяется в цикле:
for(int i-0: i < NR; i++)
pic[i]=new int[NC];
Оператор new выделяет память для NC идущих подряд переменных и записывает
адрес начала этой памяти в pic[i]. В результате получается прямоугольная
структура данных, состоящая как бы из NR строк и NC столбцов1.
Отдельный элемент, принадлежащий z-й строке иу-му столбцу, записывается как
pic[i][j]. Программа, показанная в листинге 4.10, использует двойной цикл, что-
1 Значения NR и NC задаются директивами #define. Строка #define NR 20 означает, что еще до
компиляции в тексте программы имя NR всюду будет заменено значением 20. Это очень удобно, так как
нудная работа (замена числа строк и столбцов) поручается компьютеру.
86
Глава 4. Функции, указатели и ссылки
бы записать в каждый элемент матрицы число, равное произведению номера
столбца на номер строки. Далее ради проверки на экран выводится значение pic[5][6],
равное, как и ожидалось, 30.
В конце программы память, занимаемая двухмерной структурой, освобождается.
Делается это в два этапа и в порядке, противоположном ее выделению. Сначала
освобождается память, занимаемая каждой строкой, растущей из массива
указателей:
forCint i = 0; i < NR; i++)
delete [] pic[i]: //уничтожаем строки
а затем — и память, занимаемая самим массивом указателей:
delete [] pic;
Это поэтапное освобождение легче будет понять, если мысленно заменить pic[i]
простым указателем р. Тогда выделение памяти для одной строки будет
выглядеть простым и привычным: int *р = new int[NC]:. И столь же привычным будет
освобождение памяти delete [] p;. Если теперь взглянуть на освобождение памяти
в листинге 4.14, то окажется, что его непривычный вид связан только с тем, что
выделяется много кусков памяти для многих строк и потом эти куски
освобождаются в цикле for'(){}. Может показаться непонятным и заключительное
освобождение памяти delete [] pic;. Но ведь в цикле for О освобождается память,
занимаемая строками. А ведь место, откуда строки растут — массив указателей pic, — тоже
занимает память, которую так же нужно освободить оператором delete.
Кроме матриц, растущих из указателей на указатель, в C++ есть другая, очень
похожая структура: двухмерный массив. Объявление int spic[20][40] создает
массив, содержащий 20x40 = 800 элементов типа int. Каждый элемент принадлежит
определенным строке и столбцу, например spic[0][15] — число, стоящее в нулевой
строке и пятнадцатом столбце.
Поскольку доступ к элементу матрицы, получаемой с помощью операторов new
(см. листинг 4.14), и элементу двухмерного массива одинаков — в том и другом
случаях элемент, стоящий в пятой строке и третьем столбце, записывается как
<иия>[5][3], — можно подумать, что оба способа идентичны. Но это совсем не так.
При попытке присвоить указателю на указатель имя двухмерного массива
int **pic;
int spic[20][40];
pic=spic; //Нельзя!
компилятор выдаст сообщение об ошибке:
assignment to 'int **' from 'int (*)[40]'
говорящее о том, что spic относится совсем к другому типу int (*) [40].
Чтобы понять, что это за тип, познакомимся с тем, как двухмерный массив,
объявляемый как int spic[20][40], хранит данные. Оказывается, объекты
двухмерного массива занимают сплошную область памяти компьютера, а поскольку память
одномерна, массив хранится в ней построчно. Сначала идет нулевая строка из со-
Указатель на массив
87
рока элементов типа int, затем первая — и так до самой последней девятнадцатой.
Если spic — имя двухмерного массива, то *(spic+l), очевидно, указывает на начало
первой строки, ведь нулевой элемент первой строки можно записать так1:
spic[l][0]=*(spic[l]+0)=*(*(spic+l)+ 0)=**(spic+D
Так что же такое spic? Логично предположить, что это указатель на
одномерный массив (в нашем случае — из сорока элементов типа int). Если это так, то
spic+1 указывает на первую строку двухмерного массива, spic+2 — на вторую и т. д.
Правда, нужно сказать, что указатель на массив — довольно странный объект. Мы
ведь привыкли, что если р — указатель на тип char, то *р — само значение типа char.
С указателем на массив это не так. Если spic — указатель на массив, то *spic не
может быть самим массивом, так как непосредственно массивами C++ не
оперирует. Поэтому *spi с — это указатель на нулевой элемент массива. Еще один
оператор * — и мы получим само значение.
Важно понимать разницу между указателем на указатель и указателем на массив.
В первом случае прибавление единицы передвигает указатель вдоль массива
указателей, во втором — перебрасывает указатель на одну строку вперед. Различия
между указателем на указатель и указателем на массив быть может лучше всего
видны на рис. 4.5, где показано размещение в памяти матриц, «растущих» из того
и другого.
Рис. 4.5. Двухмерный массив и динамически созданная матрица
Матрица, берущая начало из указателя на указатель, разбросана по компьютерной
памяти, потому что оператор new выделяет память отдельно для каждой строки.
Двухмерный же массив занимает память «целым куском» — строка за строкой.
1 Здесь используется связь массивов и указателей (см. раздел «Указатели и массивы»).
88
Глава 4. Функции, указатели и ссылки
Теперь мы понимаем, что в сообщении компилятора об ошибке как раз и
содержалось правильное объявление указателя на массив из сорока значений типа int:
int (*)[40]
Круглые скобки здесь необходимы, потому что без них получилось бы объявление
массива указателей — совсем другой структуры данных.
Зная, что представляет собой двухмерный массив, легко понять, каким должен
быть прототип принимающей его функции. Как и в случае одномерных массивов,
функции незачем передавать сам массив. Функции достаточно указателя на
массив, и тогда она сможет переходить к началу любой строки, а дальше — к любому
элементу строки (столбцу). Программа, показанная в листинге 4.15, использует
функцию mi n для вычисления минимального элемента массива.
Листинг 4.15. Передача двухмерного массива функции
#include <iostream>
using namespace std:
#define NR 20
#define NC 40
int mindnt nr. int (*m)[NC]) {
int min=m[0][0];
for(int i=0;i<nr;i++)
for (int j=0:;j<NC;j++)
min = (m[i][j] < min) ? m[i][j] : min;
return min;
i
int mainO {
int spic[NR][NC];
for(int i - 0; i < NR; i++)
for(int j=0;j<NC;j++)
spic[i][j] = i * j;
cout « min(NR.spic) « endl:
}
Функция min имеет два параметра: число строк nr и указатель на массив (строку)
из NC элементов int(*m)[NC]. В самой функции минимум сначала полагается
равным элементу [0][0] массива, а дальше в цикле, перебирающем все его элементы,
проверяется, так ли это. Для проверки используется необычный оператор
min=(m[i][j] < min) ? m[i][j] : min;
Получаемый с его помощью результат зависит от условного выражения в круглых
скобках. Если условие m[i] [ j] < min истинно, переменная min становится равной
выражению, стоящему слева от двоеточия, то есть m[i ] [ j], если же условие в скобках
ложно, значение min не меняется, потому что выполняется присваивание min=min.
Следующая инструкция выводит на экран значение минимума, равное в нашем
случае нулю:
cout « min(NR.spic) « endl;
Чтобы убедиться в том, что программа работает правильно, можно записать
в какую-нибудь ячейку массива отрицательное число и посмотреть, что будет
выведено на экран.
Указатель на функцию
89
Указатель на функцию
Слова «указатель на функцию» звучат дико, мы ведь привыкли к тому, что
указывать можно только на объекты, такие как числа, строки и массивы.
Но ведь и функция, если подумать, — тоже объект, правда, состоящий не из букв
и чисел, а из команд процессора. Как и любые другие объекты, функция занимает
определенный участок компьютерной памяти, и, когда она вызывается, процессор
пересылает аргументы в особое известной каждой функции место памяти, а затем
просто получает адрес ее первой инструкции. Вот этот адрес и есть указатель на
функцию.
Теперь подумаем, как объявить этот указатель. Очевидно, больше всего функция
похожа на массив, поэтому и указатель на нее выбран таким:
int (*f)(int.int):
Здесь f — указатель на функцию, принимающую два целочисленных параметра
и возвращающую целое число.
Указатель на функцию, как и любой другой, инициализируется оператором &.
В программе из листинга 4.16 объявлена функция sum, принимающая две
целочисленные переменные и возвращающая их сумму. Кроме того, объявлен указатель f
на аналогичную функцию. Как только f получает адрес начала sum, открывается
«партизанская тропа» к этой функции. Оператор * превращает, как обычно,
указатель на функцию в саму функцию, следовательно, (*f)B.3) вернет пятерку,
потому что *f — это как бы второе имя sum. Заметим, что скобки вокруг *f нужны, ведь
int *f (int, int) — это совсем не указатель на функцию, а функция, возвращающая
указатель.
Листинг 4.16. Указатель на функцию
#include <iostream>
using namespace std:
int sum(int a. int b){
return(a + b);
}
int mainO {
int (*f)(int.int):
cout « sumB,3) « endl;
f = ∑
cout « (*f)B.3) « endl:
}
Обращение к функции через указатель (*f) B,3) весьма похоже на обращение к
переменным, но давайте вспомним о сходстве функций и массивов. Если m — массив
переменных типа char, a pch — указатель на char, то присвоить указателю адрес
нулевого элемента массива можно двумя способами: pch = &m[0] и просто pch=m. To
есть имя массива — это еще и адрес его начала. Проделать такое можно потому, что
с именем массива не связано значение какого-то его элемента. Если ср —
переменная типа char, a pch — указатель на char, то нельзя записать pch=ch, потому что
слева и справа от знака равенства стоят объекты «разной породы». Но если m — имя
90
Глава 4. Функции, указатели и ссылки
массива, состоящего из переменных char, наши руки развязаны и можно записать
pch=m, ничего не нарушая.
То, что справедливо для массивов, должно выполняться и для функций. Ведь имя
функции так же «свободно», как имя массива. Поэтому имя функции — и есть
адрес ее начала, а это значит, что инициализация указателя на функцию не
требует оператора &. Очевидно, аналогию с массивами нужно довести до логического
конца. Если адрес нулевого элемента массива используется наравне с его именем,
то такой же привилегией должен обладать и указатель на функцию. Именно это
и подтверждает листинг 4.17, где указатель на функцию и ее «настоящее» имя
оказываются равноправными.
Листинг 4.17. Имя функции == указателю на нее
#include <iostream>
using namespace std:
int sumCint a. int b){
return(a+b);
}
int mainO {
int (*f)(int. int);
cout « sumB.3) « endl;
f = sum;
cout « fB.3) « endl;
}
Нужно, правда, помнить, что имя функции — не указатель, поскольку не может
быть перенаправлено на другую функцию. Настоящий указатель свободен и
волен указывать куда угодно, в том числе и в никуда.
Чтобы у читателя не сложилось впечатление об указателе на функцию, как о чем-то
любопытном, но бесполезном, попробуем написать программу, реализующую
разные способы сортировки строк. До сих пор для этого использовалась
функция sort О, принимающая только два итератора s.beginO и s.endO, указывающие
на начало и конец контейнера (см. раздел «Файлы» в главе 3). При этом строки
размещались в лексикографическом порядке, грубо говоря, по алфавиту. Но
возможны и другие способы сравнения строк. Например, большей можно считать ту
строку, что длиннее, поэтому существует другая функция sort О — уже стремя
параметрами. Первые два — уже знакомые нам итераторы, а третий — указатель
на функцию, выполняющую сравнение строк. Эта функция принимает два
сравниваемых объекта (в нашем случае они имеют тип string) и возвращает булеву
переменную — результат сравнения. Если объекты сортируются «по
возрастанию», то функция должна возвратить true, когда первый объект больше второго,
и false — в противном случае (листинг 4.18).
Листинг 4.18. Использование указателя на функцию для сортировки строк
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std:
bool cmp(string xl. string x2) {
return xl < x2;
Указатель на функцию
91
}
bool cmpl(string xl, string x2) {
return xl.sizeO < x2.size();
}
int mainO {
vector<string> s;
s.push_back("uapb");
s.push_back("царевич");
s.push_back("король");
s.push_back("королевич");
s.push_back("сапожник");
s.push_back("портной");
sort(s.begin(), s.endO. cmp);
for(int i - 0; i < s.sizeO: i++)
cout « s[i] « endl;
cout « endl:
sort(s.begin(). s.endO. cmpl);
for (int i = 0; i < s.sizeO; i++)
cout « s[i] « endl:
}
Эта программа сначала создает контейнерный тип vector, содержащий 6 объектов
типа string, а затем сортирует его двумя разными способами, подставляя в
функцию sort О указатели на разные функции. В первом случае (функция cmp)
оператор < сравнивает строки лексикографически. Во втором (функция cmpl)
сравниваются размеры строк, для чего используются собственные функции sizeO
объектов string. Естественно, результаты сортировки оказываются разными и будет
поучительно на них посмотреть.
Глава 5
Разработка программы
Поосторожней с этим исходным текстом!
Я всего лишь доказал, что программа
работает правильно, но еще не тестировал ее.
Дональд Кнут
Алгоритм
Почему в работе, выполненной одним, все так гармонично и так подходит друг
к другу, а у другого все трещит по швам и разваливается?
Вовсе не потому, что кто-то — талант и баловень судьбы. Практически все
испытывают трудности, у всех что-то не получается, и если чужая работа напоминает
длинную цепь удач, то лишь потому, что автору пришлось много думать,
пробовать, переделывать, искать и находить ошибки, быть может, ему немного повезло,
а в результате получилось что-то легкое и красивое.
В этом разделе я попробую приоткрыть завесу над процессом программирования
и попытаюсь у вас на глазах решить довольно простую задачу: создать новый
словарь для нахождения анаграмм.
В разделе «Анаграммы» главы 3 мы пользовались словарем, содержащим около
20 000 слов, что довольно мало. Но где найти больший? Первое, что приходит
в голову, — конечно же, Интернет. Чтобы найти там словарь, достаточно поискать
документ, в котором есть несколько слов, наугад выбранных из старого,
маленького словаря. Если поискать с помощью Google (www.google.com) слова «aardvark»,
«aardvarks», «aback», «abaft», «abalone», то уже среди первых десяти адресов
появится словарь, содержащий почти 90 000 слов1.
Но и это не предел. Можно попробовать найти больший словарь, а можно
попытаться расширить его собственными силами. Для этого нужно извлекать слова из
всех доступных английских текстов и проверять, нет ли их в словаре. Естественно,
все новые слова добавляются в словарь, и поиск продолжается.
1 По адресу этого словаря http://www.gtoal.com/wordgames/cherry/anagram/words легко понять, для
чего он создан. Ведь «wordgames» — это «игры со словами», а что такое «anagram», вы и сами знаете.
Первая версия
93
Таков в самых общих чертах план создания нового словаря. Но чтобы начать
программирование, этот план нужно уточнить. Программа должна сначала
прочитать текущую версию словаря и превратить ее в некий контейнер. Далее
программе нужно указать имя читаемого файла. Это может быть другой словарь или
какой-либо текст («Алиса в стране чудес», «Пиквикский клуб», «Гамлет» — что
угодно). Затем нужно научиться извлекать слова из выбранного текста и каждое
извлеченное слово искать в старом словаре. Если слово уже есть, переходить к
следующему, если нет — включать его в словарь. После проверки всех слов остается
только записать новую версию словаря (лучше под другим именем) и закончить
работу. Только что описанная программа похожа на пылесос. Она глотает все
доступные ей тексты и постепенно наращивает свой словарный запас.
Первая версия
Начинать работу лучше с небольшого, но работающего фрагмента программы.
Полный текст только что написанной большой программы напоминает вражеское
минное поле, где мины (ошибки программиста) находятся в неизвестных местах
и могут сработать в любой момент.
Начнем нашу программу с фрагмента, который читает словарь, заполняет
контейнер, ищет в нем заданное (пока вручную) слово и затем выводит измененный
словарь на экран1. Соответствующий текст показан в листинге 5.1.
Листинг 5.1. Добавление слова в словарь
#include <iostream>
finclude <fstream>
#iinclude <string>
#indude <map>
using namespace std;
int main(){
char b[80];
string w = "zzzzzzzzz":
map<string.string> diet;
map<string,string>::iterator im;
ifstream infileC'diction");
while A) {
infi1e.get1ine(b. sizeof(b));
if(infile.eofO) break;
dict.insert(pair<string.string>(b. ""));
}
dict.insert(pair<string.string>(w." n"));
for(im=dict.begin(); im != dict.endO: im++)
cout « (*im).first « endl;
infile.closeO':
}
1 В словаре десятки тысяч строк и от вывода его на экран пользы немного. Но операционная система
позволяет легко перенаправить вывод. Если имя программы 151.exe, то, записав в командной строке
команду 161.exe > z и нажав клавишу Enter, мы перенаправим вывод в файл z — быстро и удобно.
94 Глава 5. Разработка программы
Этот фрагмент кажется очень простым, но у меня он заработал только с шестой
или седьмой попытки. Едва ли будет интересно смотреть на все промежуточные
варианты, поэтому попробуем ограничиться их кратким описанием.
Промежуточные варианты
Отчего у нас никогда нет времени сделать
что-либо хорошо, но всегда находится
время на переделку?
Д. Ван Тассел. Стиль, разработка,
эффективность, отладка и испытание
программ
В первом варианте программы использовался другой тип контейнера — вектор.
Попытка скомпилировать текст программы не удалась, потому что я забыл
включить заголовок #include <vector>. После включения заголовка, а также
исправления мелких ошибок (забыл объявить переменную и поставить в одном месте
точку с запятой) выяснилось, что программу все равно нельзя компилировать — из-за
того, что для вектора, оказывается, не существует собственной функции findO.
Пришлось написать ее самому:
int dict_find(vector<string> d. string s) {
for (unsigned 1=0: i < d.sizeO: i++)
if(d[i] == s) return i;
return -1:
}
Обратите внимание, переменная i цикла for() объявлена как unsigned, потому что
именно таков тип значения, возвращаемого функцией d.sizeO. Если объявить ее
как int, некоторые компиляторы откажутся сравнивать разнотипные
переменные.
После всех описанных изменений программа заработала, и тут выяснилось, что
контейнер vector не очень приспособлен к нашей задаче, потому что поиск в нем
идет слишком долго. Взгляните еще раз на функцию dictfindO. В ней строка s
сравнивается с каждым из элементов контейнера. Это означает, что в среднем
придется при каждом поиске уже известного слова перебрать половину
элементов контейнера. А с учетом того, что составление словаря потребует перебора
сотен тысяч слов (и каждое придется искать в словаре), затраты времени становятся
огромными.
В этом месте у многих читателей наверняка возникнет вопрос: а можно ли искать
элемент в контейнере быстрее, чем это делает функция dict_find()? Что может
быть быстрее линейного перебора элементов?
Ответ таков: если элементы расположены хаотично, то ничего другого не остается.
Но если они отсортированы, можно применить замечательный алгоритм, которым
восхищались еще древние греки — бинарный поиск.
Промежуточные варианты
95
Представим себе, что слова расставлены в алфавитном порядке и есть слово,
которое нужно отыскать в словаре. Начнем поиск с того, что поделим словарь
на две примерно равные части и сравним наше слово с первым словом второй
части. Если наше слово окажется больше, значит, искать его надо во второй
половине, если меньше — в первой. Выбранную половину опять делим на две части
и так повторяем до тех пор, пока слово не найдется или выяснится, что его нет
в словаре.
Задача 5.1. Целые числа расположены в массиве в порядке возрастания. Напишите
программу бинарного поиска заданного числа.
Бинарный поиск позволяет во много раз сократить число сравнений элементов,
поэтому он гораздо быстрее. В принципе, можно было бы сортировать контейнер
vector и написать для него функцию бинарного поиска. Но гораздо проще
использовать уже готовое решение — контейнер тар, который отличается от
контейнера mul timap, использованного нами при поиске анаграмм, тем, что ключ (первый
объект пары) должен быть уникальным. Для словаря это хорошо, потому что все
слова в нем — разные.
Конечно, контейнер тар не похож на отсортированный контейнер vector. Но
прелесть стандартных контейнеров в том и состоит, что нам не нужно знать, как они
устроены, достаточно знать о том, что собственная функция insert О контейнера
тар работает быстро (на манер двоичного поиска) и после каждой вставки нового
элемента контейнер оказывается отсортированным. Операция поиска в
контейнере тар так же быстра, как и вставка нового значения. Смущает только, что элемент
контейнера тар состоит из двух частей, и если назначение первой части (ключа)
понятно — там хранится само слово, то вторая часть остается свободной.
И тут меня посещает идея: вторую часть можно использовать для хранения
имени источника, откуда слово пришло в словарь. Таким именем может быть просто
название файла. Программа, показанная в листинге 5.1, помечает пустой строкой
слова, хранимые в первоначальном словаре:
diet.insert(pair<string,string>(b. "")):
А слово «zzzzzzzzz», включаемое в словарь, помечается буквой «п»:
dict.insert(pair<string.string>(w, " п"));
Нашу задачу можно считать наполовину решенной. Работоспособная версия
программы (см. листинг 5.1) способна быстро искать заданные слова в созданном ею
контейнере (словаре). Теперь остается научиться «добывать» слова из текстового
файла и включать в контейнер каждое полученное слово, если его там еще нет.
Обдумывая задачу, программист, как правило, оценивает несколько вариантов
ее решения и выбирает наиболее подходящий. Сделать это бывает нелегко, так
как у любого варианта есть недостатки и далеко не всегда понятны последствия
принятого решения.
Например, для получения слов можно считывать целую строку с помощью
функции getlineO, а затем выделять из нее последовательности идущих подряд букв.
Такой подход относительно быстр (читается целая строка), но очень зависит от
96
Глава 5. Разработка программы
структуры текста. Слишком длинная строка не уместится в буфере, и некоторые
слова могут потеряться или оказаться обрезанными.
Значит, надежнее читать из файла идущие подряд буквы — символ за символом
(для этого существует функция getO). Такой подход ничего не требует от файла,
в котором может не быть строк и даже самих букв. В этом случае программа тоже
будет работать, но не найдет ни одного слова. Недостаток такого подхода —
низкая скорость, ведь чтение из файла — медленная операция, а здесь приходится
читать каждый символ.
Но, все же, немного подумав, я выбрал второй вариант. Во-первых, он проще,
во-вторых, он универсален, и наконец, в третьих, он не должен сильно замедлить
работу программы, так как основное время будет, скорее всего, потрачено на
поиск слова в контейнере, а не на извлечение его из файла.
Добавление функций
Но все же любое решение может быть ошибочным, поэтому лучше его
изолировать в специальной функции, которая выдает из файла очередное слово или
признак окончания файла, когда все символы прочитаны. Если мое решение окажется
неудачным или захочется испытать другое, достаточно будет заменить функцию,
а не переделывать всю программу.
Теперь нужно решить, как будет выглядеть функция, каковы ее имя, параметры
и возвращаемое значение. Чтобы лучше понимать программу, нужно отразить
в названии функции то, что она делает, поэтому хорошо назвать ее GetWord (в
переводе с английского — «получить слово»). Функция будет возвращать значение
типа bool (true — при удачном чтении, fal se — при достижении конца файла).
А чтобы выделенным словом можно было воспользоваться, параметром функции
должна быть ссылка на строку. Получается, что прототип функции Getword должен
быть таким:
bool GetWord(string &w );
Если представить, что у нас такая функция уже есть, то цикл, в котором новые
слова включаются в словарь, выглядел бы очень просто:
while A) {
if (GetWord(nw) == false)
break;
else
dict.insert(pair<string.string>(nw, " n"));
}
В этом цикле новое слово даже не ищется, а сразу вызывается функция insertO,
потому что в контейнере тар не может быть двух одинаковых ключей, и вставка
уже известного слова ему не повредит.
Однако первый вариант программы с участием новой функции компилятор
отверг на том основании, что внутри функции GetWord О обнаружился неизвестный
Добавление функций
97
объект infile — файл, из которого считываются буквы. Здесь меня подвело знание
языка С, где файлы глобальны, то есть доступны всем функциям. Но в C++ это
не так, и приходится вводить в функцию еще один параметр. То есть ее прототип
должен выглядеть так:
bool GetWordCstring &w. ifstream infile);
Однако компилятор отказался работать и с этой функцией. На этот раз его
сообщения гораздо более туманны, но, взглянув на заголовок функции, я догадался,
что и файл нужно передать по ссылке, ведь функции нужен он сам, а не его копия.
Хотя программисты — народ горячий, и главное для них, чтобы программа
заработала, лучшие программисты предпочитают точно знать, что они делают, и почти
никогда не действуют наобум.
Поставив оператор & перед словом infile, я получил наконец функцию (см.
листинг 5.2), к которой компилятор отнесся благосклонно.
Листинг 5.2. Функция, получающая очередное слово из файла
bool GetWorcKstring &w. ifstream &infile){
char ch;
w.eraseO;
while(l) {
infile.get(ch);
if(infile.eofO)
if(w != "") return true;
else return false;
ifCisletter(ch))
w += ch:
else
if(w != "")
return true:
}
}
Функция GetWorcK) оказалась довольно сложной из-за того, что при чтении
каждого символа приходится проверять, не достигнут ли конец файла.
Все начинается с очистки строки, где окажется только что выделенное слово. Это
делает собственная функция w.eraseO. Далее из файла читается символ и
запоминается в переменной ch. После этого нужно проверить, не достигнут ли конец
файла. Если чтение неудачно, то вызов infile.eofO возвращает true. Но просто
выйти в этот момент из функции нельзя, ведь в строке w может храниться
последнее слово файла. Поэтому проверка выглядит так:
if(infile.eofO)
if(w != "") return true:
else return false;
Если достигнут конец файла, но строка не пустая (w != "" ), возвращается true и
делается попытка включить последнее слово, хранимое строкой w, в словарь. Если же
строка пуста, возвращается false — файл кончился — просто нет слов! Обратите
внимание на то, что в этом фрагменте нет фигурных скобок после i f (i nf i 1 e. eof ()).
98
Глава 5. Разработка программы
Это возможно потому, что if()...else, хоть и содержит две точки с запятой,
считается одной инструкцией.
Если конец файла не достигнут, то программе удалось прочитать какой-то
символ, и теперь нужно проверить, буква ли он, и если да — присоединить его к
строке. Если же символ — не буква, значит, слово найдено и его нужно возвратить во
внешний мир и попытаться включить в словарь. Проверка выглядит так:
if(isletter(ch))
w += ch;
else if(w != "")
return true:
Обратите внимание: значение true возвращается, лишь когда строка не пуста.
Если же в строке ничего нет, функция читает следующий символ, и так до тех пор,
пока не найдется буква или пока не кончится файл.
Вы, наверное, уже обратили внимание на функцию isletter(ch), очевидно,
возвращающую true, когда ch — буква, и fal se — в противном случае. Эту функцию
я придумал, когда писал функцию GetWordO, чтобы сосредоточиться только на
одной задаче, а выяснение — буква ли прочитанный символ — оставить «на потом».
Проверять ошибки компиляции это не мешает, нужно только в начале
программы поместить прототип функции isletterO. Но когда функция GetWordO готова
(или мы просто думаем так, потому что не проверяли ее «в деле»), приходит черед
и функции isletterO.
Чтобы ее написать, нужно знать, какие латинские символы — буквы, а какие —
нет. Можно, конечно, найти какую-нибудь справочную книгу, где есть различные
кодировки. Но можно понять, как кодируются буквы, и экспериментально с
помощью простой программы, показанной в листинге 5.3.
Листинг 5.3. Программа, показывающая коды различных символов
#include <iostream>
using namespace std:
int mainO {
char ch;
fordnt 1=1: i < 127; i++) {
ch = i;
cout « i « " "« ch « endl;
}
}
В этой программе перебираются числа от 1 до 126, каждое число записывается в
переменную ch, а затем дважды выводится на экран1: как переменная int (cout« i)
и как символ (cout « ch). Запустив программу, мы увидим, что прописные
латинские буквы идут плотно без просветов и кодируются числами от 65 (буква А) до
90 (Z). Строчные латинские буквы расположены также плотно и кодируются
числами от 97 (а) до 122 (z). Еще нам понадобится апостроф ('), который тоже нужно
считать буквой, потому что он есть во многих английских словах. Еще, пожалуй,
1 Вывод программы лучше перенаправить в файл командой 163.exe > z. Об этом уже говорилось в
начале главы.
Добавление функций
99
для составления словаря нужно привести все слова к одному виду, то есть
превратить все строчные буквы в прописные, и, поскольку буквы (прописные и
строчные) идут подряд, к числу, которым кодируется прописная буква, достаточно
прибавить одно число (легко понять, что оно равно 32).
Итак, функция isletterO готова (листинг 5.4).
Листинг 5.4. Проверка букв и «понижение» регистра
bool isletter(char &c){
if(с ~ 39) return true:
if(с >« 65 && с <- 90) {
с +« 32;
return true;
}
if (с >= 97 && с<= 122) return true;
return false;
}
Задача 5.2. Соберите все части программы подготовки словаря. Сделайте так,
чтобы имя обрабатываемого файла задавалось в командной строке. Создайте
словарь Диккенса, Шекспира, Льюиса Кэрролла.
Задача 5.3. Функция GetWordO возвращает true, только когда полученная строка
не пуста. Но может случиться так, что в файле вообще не окажется слов. В этом
случае GetWordO не возвратит ничего. Как исправить эту ошибку?
Задача 5.4. Подумайте, как определить и хранить в словаре частоту появления
каждого слова.
В этой суетливой главе нам приходилось начинать работу, бросать ее на полпути,
отвлекаться, думать, откладывать что-то на потом, исправлять ошибки и снова
думать. Это утомительно, но именно таково программирование, да и любая
другая сложная работа.
Глава 6
Примитивные объекты
Enum
С помощью готовых объектов, таких как контейнеры и строки, можно решить
множество сложных задач, но конечно, не все задачи на свете. Чтобы
моделировать окружающий мир, нам потребуются собственные объекты, которые мы
постепенно научимся создавать.
А начнем с примитивных, но очень полезных объектов, задаваемых ключевым
словом enum. Объекты, создаваемые с помощью enum, имеют несколько фиксированных
состояний; это могут быть месяцы, дни недели или масти карт. В программе из
листинга 6.1 ключевое слово enum создает новый объект suit (масть), имеющий четыре
состояния: diamonds (бубны), hearts (черви), spades (пики), clubs (трефы).
Листинг 6.1. Задание масти карт
#include <iostream>
using namespace std:
enum suit{diamonds, hearts, spades, clubs}:
void ShowSuit(suit);
int mainO {
suit si - spades;
for (si - diamonds: si <= clubs: si = static_cast<suit>(sl + D)
ShowSuit(sl):
cout « hearts + spades * clubs « endl: //1*2*3
//ShowSuitO): Ошибка: неправильный перевод int в suit
}
void ShowSuit(suit s) {
switch(s) {
case diamonds:
cout « "Бубны" « endl:
break;
case hearts:
cout « "Черви" « endl;
break;
case spades:
cout « "Пики" « endl:
break;
Enum
101
case clubs:
cout « "Трефы" « endl;
break;
default:
cout « "Неизвестная масть" « endl:
}
}
Строка
enum suit{diamonds. hearts, spades, clubs};
описывает новый объект suit, находящийся в четырех различных состояниях.
Зная описание объекта, можно создать сами объекты и задать их начальные
значения, что и делается в строке
suit si « spades;
Теперь объект si способен жить в программе самостоятельно, например, его
можно передать функции ShowSuitO, принимающей, согласно объявлению:
void ShowSuit(suit):
объект типа suit и показывающей на экране его состояние (масть). В листинге 6.1
функция ShowSuitO получает объект si типа suit, находящийся во всех
возможных четырех состояниях. Перебирает состояния цикл for О, в котором очередное
состояние объекта suit получается с помощью приведения типов:
static_cast<suit>(sl + 1)
Если, скажем, si равен diamonds, то static_cast<suit>(sl + 1) уже равен hearts.
Делается это потому, что перечисления создают объекты нового типа, которые
нельзя смешивать с целыми числами.
Если сами объекты, созданные словом enum, отличны от целых чисел, то их
отдельные состояния можно рассматривать как целочисленные константы, чьи
значения начинаются с нуля и последовательно увеличиваются на единицу. В нашем
примере масти diamonds соответствует целочисленный ноль, масти hearts -
единица, spades равно двум, a clubs — трем. Вот почему инструкция
cout « hearts + spades*clubs « endl; // 1 + 2*3
выводит на экран цифру 7 ( hearts равно 1, a spades — двум, clubs — трем.). Но то,
что отдельные значения перечислений — целые числа, не значит, что словом enum
задается целочисленная переменная. В нашем примере suit — это новый тип,
поэтому передавать функции ShowSuitO целое число ShowSuitO) нельзя, даже если оно
совпадает с одной из мастей.
Но если выдать целое число за объект типа suit не удается, то обратное действие —
превращение объекта suit в целое число происходит автоматически, там, где это
имеет смысл. В отрывке программы
enum suit{diamonds. hearts, spades, clubs};
suit si = spades. s2 = clubs. s3:
cout « (si * s2) « endl; //6=2*3
cout « (si < s2) « endl; //l=true
//s3=sl+s2 Неверно!
102
Глава 6. Примитивные объекты
на экран сначала выводится число 6, потому что объекты si и s2 автоматически
преобразуются в целые числа 2 (значение spades) и 3 (значение clubs), а затем
выводится 1, потому что выражение si < s2 имеет значение 2 < 3, то есть true. Наконец,
последняя инструкция нашего отрывка s3 = si + s2; не будет понята компилятором,
потому что сумма si + s2, стоящая справа, превращается в целое число, которому
нельзя приравнять объект s3 типа suit, стоящий слева.
Задача 6.1. Приравнять сумму двух объектов третьему можно с помощью
приведения типов s3 = static_cast<suit>(sl + s2). Пусть si равен spades, a s2 — clubs. Что
в этом случае покажет на экране инструкция cout « s3 « end!; ? Как отреагирует
на такой объект S3 функция ShowSuitO из листинга 6.1?
«Естественные» значения констант 0, 1, 2..., создаваемых с помощью enum,
можно изменить, если смыслу задачи соответствуют другие их значения. Например,
месяцы принято нумеровать с 1, что и указывается для первого месяца Jan при
объявлении типа month:
enum month
{Jan=l.Feb.Маг.Apr.May,Jun,Jul.Aug.Sep.Oct.Nov.Dec};
Теперь константа Jan равна единице, Feb — двойке, a Dec — двенадцати. Эти
константы также можно использовать в арифметических выражениях наравне с
целочисленными переменными. Так, например, выражение Nov % Feb равно 1. Здесь мы
применили новый бинарный оператор %, находящий остаток от деления Nov на Feb.
Поскольку Nov равен 11, a Feb — двойке, остаток от деления 11 на 2 равен единице:
11 = 5x2 + 1. Аналогично, Jul % Apr равно 3, потому что 7 = 1x4 + 3. Наконец, остаток
от деления меньшего числа на большее равен меньшему числу, например, Feb % Aug
равно 2%8 = 0х8 + 2. Подчеркнем, что Feb, Jan и т. д. — константы и могут
встречаться только справа от знака равенства. Инструкция Jan=3; бессмысленна.
Нам осталось только сказать, что словом enum можно задать любые значения
констант, но тогда придется все время использовать оператор =, чтобы побороть
«естественное» увеличение их на единицу. Инструкция
enum {SLEN = 12. BSIZE = 1024};
задает две целочисленные константы: SLEN, равную 12, и BSIZE, равную 1024. Такая
инструкция не описывает какой-то объект (обратите внимание, после enum не
указано имя), а просто создает две константы, которые можно использовать в
программе независимо друг от друга.
Записи
Перечисление suit, созданное в предыдущем разделе, описывает только масть
карты. Но у карты есть еще имя («тройка», «семерка», «туз»), и для полного описания
карты нужно объединить оба признака в одном объекте.
Для этого в C++ существуют записи, объявляемые с помощью ключевого слова
struct. Записи состоят из полей. Например, игральную карту описывает запись
с двумя полями — именем карты name и мастью csuit:
Записи
103
enum suit{diamonds. hearts, spades, clubs};
enum cname{six.seven.eight.nine.ten.jack.queen.king.ace}:
struct card {
suit csuit:
cname name;
}:
Объявление struct card {}; описывает новый объект card, состоящий из двух
перечислений — масти карты csuit и ее имени name. Программа, показанная в
листинге 6.2, создает колоду карт, тасует ее и показывает все карты на экране.
Листинг 6.2. Программа создает колоду карт и тасует ее
#include <iostream>
finclude <vector>
#include <algorithm>
using namespace std;
enum suit{diamonds.hearts.spades.clubs};
enum cname{si x.seven.eight.ni ne.ten,jack.queen.ki ng.ace}:
struct card {
suit csuit;
cname name;
}:
void print (card);
int mainO {
suit si:
cname cl:
card mycard;
vector<card> pack;
for (si = diamonds;sl <- clubs;sl = static_cast<suit>(sl + D)
for (cl-six:cl <=ace;cl=static_cast<cname>(cl+D) {
mycard.csuit = si:
mycard.name - cl:
pack.push_back(mycard);
}
random_shuff1e(pack.begin(). pack.end());
for_each(pack.begin(). pack.endO. print);
}
void print (card c) {
string s[] - {"Б". "Ч". "П", "T"};
string
crd[]={ ". "."8 "."9 V'lOV'B Н."Д "."К И,"Т "};.
cout « crd[c.name] « ' '« s[c.csuit] « endl;
}
Колоду карт pack хранит контейнер vector<card> pack;. Поначалу он пуст. И наша
первая задача — «затолкать» в контейнер все возможные карты. Делает это
двойной цикл:
for (sl=diamonds;sl <= clubs;sl=static_cast<suit>(sl+D)
for (cl=six;cl <=ace;cl=static_cast<cname>(cl+D) {
mycard.csuit=sl;
mycard.name= cl:
pack.push_back(mycard);
}
104
Глава б. Примитивные объекты
В нем заполняются поля csuit и name записи mycard, описывающей игральную
карту, а затем mycard «проталкивается» в контейнер его собственной функцией push_
backO:
pack.push_back(mycard);
Обратите внимание на то, что доступ к отдельному полю записи очень похож на
вызов собственной функции объекта: сначала указывается имя записи, затем идет
точка, а дальше имя поля. Например, явное задание масти карты mycard выглядит так:
mycard.csuit = diamonds:
Когда двойной цикл завершит работу, в контейнере pack окажутся карты всех
мастей, начиная с шестерки. Все они будут лежать по порядку: сначала бубны
(шестерка, семерка, ..., десятка, валет, ..., туз), затем черви и т. д. Перед игрой колоду
необходимо перетасовать. Делает это функция (алгоритм) random_shuffle():
random_shuffle(pack.begin(). pack.end()):
которой для успешной работы необходимы только начальный pack.beginO и
конечный pack, end О итераторы тасуемого контейнера.
И наконец, следует убедиться в том, что программа работает правильно, — просто
посмотрев на экране все карты нашей колоды. Раньше для вывода содержимого
контейнера на экран мы использовали цикл for О, перебирающий все значения
итераторов:
vector<card>::iterator ci:
for(ci = pack.beginO; ci< pack.endO: ci++) {
}
В программе из листинга 6.2 использован специальный алгоритм
for_each(IterIni. IterEnd. print)
просто передающий все объекты, попавшие между итераторами1 Iterlni, IterEnd
функции print. По сути, алгоритм for_each() — это тот же цикл, только иначе
записанный. И конечно, он проще, лучше и надежней, чем явный перебор итераторов
вцикле^гО.
Функция printO, подготовленная для алгоритма foreachO, принимает объект
типа card. Как видно из листинга 6.2, внутри функции printO объявлены два
массива строк, содержащие сокращенные имена карт и названия мастей:
string s[] = {}: //названия мастей
string crd[] = {}: //имена карт
А в инструкции
cout « crd[c.name] « ' '« s[c.csuit] « endl;
1 Как обычно, IterEnd указывает на объект, идущий непосредственно за последним объектом,
переданным функции (в нашем случае это print()).
Typedef
105
используется свойство констант, указанных в перечислениях, начинаться по
умолчанию с нуля и увеличиваться на единицу. Это позволяет использовать
перечисления как индексы в массиве. Если, скажем, поле suit записи с равно spades
(пики), то с.suit просто равно 2, и если взглянуть на массив строк, обозначающих
масти s[], то окажется, что s[c.csuit] равно s[2], то есть «П» (пики).
Typedef
Если ключевое слово enum создает новый тип на самом деле, то ключевое слово typedef
только иначе называет уже существующий тип. Не внося в программу ничего
нового, typedef тем не менее способен преобразить ее, сделать простой и понятной.
Работа typedef описывается простым правилом: указав ключевое слово typedef слева
от объявления объекта, мы уничтожаем сам объект, делая его именем нового типа.
Пусть, например, в программе объявлена переменная:
unsigned int ui:
Это, как мы знаем, беззнаковая целочисленная переменная ui, занимающая
определенное место в памяти. Если приписать слева typedef, то сама переменная
пропадает и возникнет новый тип ui — беззнаковая целочисленная переменная:
typedef unsigned int ui:
После этого показанное ниже объявление будет означать, что создается
переменная pmunauv.
ui p;
Тип ui по своей сути не новый, новое у него лишь название, ставшее гораздо короче.
Наш второй пример покажет, как легко с помощью typedef создать указатель на
запись. Но прежде нам нужно узнать, как совмещаются описание и объявление
записи. Ведь инструкция:
struct card {suit csuit: cname name;};
только описывает новый объект card. Это как бы идея объекта. А реальный объект
mycard, занимающий определенную область памяти, объявляется так:
struct card {suit csuit; cname name:};
card mycard;
Можно совместить объявление и создание объекта и объявить, скажем, указатель
на запись pcard:
struct card {suit csuit; cname name;} *pcard;
Здесь pcard — реальный указатель на запись, занимающий память компьютера.
Добавление слева слова typedef
typedef struct card {suit csuit: cname name;} *pcard;
106
Глава б. Примитивные объекты
делает pcard именем нового типа — указателя на запись, полями которой служат
два перечисления csuit и name. Теперь указатель на запись можно объявить
гораздо короче:
pcard p;
В программе из листинга 6.3 с помощью указателя на запись создается и
показывается на экране игральная карта — валет червей.
Листинг 6.3. Использование указателя на запись
#include <iostream>
using namespace std;
enum suit{diamonds. hearts, spades, clubs};
enum cname{six.seven.eight.nine.ten.jack.queen.king.ace};
string s[] - {"Б". ИЧ". "П". "T"};
string crd[] -
p6 V7 „/8 vg •V10VB \"д "."К "."Т "};
int mainO {
typedef struct card {suit csuit; cname name;} *pcard;
pcard p = new card; //p - указатель на запись card
p->csuit = hearts: //масть «черви»
p->name = jack; //валет
cout « crd[p->name] « " "; //«B»
cout « s[p->csuit] « endl; //«4»
delete p: //освободить память, занимаемую записью
}
Указатель можно использовать только для доступа к существующей записи,
которой выделена компьютерная память. Поэтому сначала нужно создать запись и
поместить ее адрес в указатель р. Делает это инструкция
pcard p = new card;
В программе нам встретился новый оператор ->, состоящий из двух значков:
минуса - и указывающей вправо стрелки >. Его смысл легко угадывается: оператор ->
обеспечивает доступ к полю записи через указатель на нее. Вместо
p->csuit
нам привычней и понятней такая запись:
(*р).csuit
Действительно, раз р — указатель на запись, то *р — сама запись, а (*р).csuit — ее
поле1. Недостаток такого способа доступа к отдельным полям — в лишних скобках
и звездочке, поэтому в C++ вместо (*р).csuit принято писать
p->csuit.
Заметим, что оператор -> можно использовать не только с указателями, но и с
итераторами. Поэтому выражение (*im).first, встреченное нами в листинге 3.13,
можно записать более красиво im->first. Ведь im в той программе хоть и не ука-
1 Заметим, что скобки (*р) здесь необходимы, потому что оператор «точка» сильнее «звездочки»,
a p.csuit не имеет смысла, когда р — указатель на запись.
Typedef
107
затель, а итератор контейнера multimap, подчиняется тем же правилам: оператор *
превращает итератор в сам объект (элемент контейнера) и поэтому доступ к
одному из двух элементов контейнера выглядит так же, как доступ к полю записи
через указатель.
Задача 6.2. Перепишите программу поиска анаграмм, заменяя в листинге 3.13 (*).
оператором ->.
Чтобы вы не относились к объекту typedef легкомысленно и не считали его
простеньким средством украшения программ, рассмотрим более сложный пример,
когда ключевое слово typedef применяется поэтапно и в результате создается
довольно замысловатый тип (листинг 6.4).
Листинг 6.4. Поэтапное создание двухмерного массива
#include <iostream>
using namespace std;
enum {NC - 30.NR = 40}:
int mainO {
typedef int ml[NC]:
typedef ml m2_2[NR]:
п2_2 р;
for(int i = 0: i < NR; i++)
for(int j - 0: j < NC; j++)
p[i][j] - i * j;
cout « p[35][l] « end!: //35
}
Сначала в листинге создается тип ml — массив NC переменных типа int:
typedef int ml[NC];
Тут все понятно. Но вот строку:
typedef ml m2_2[NR];
понять гораздо труднее. Поэтому начнем с объявления:
int m2_2[NR];
Очевидно, это массив NR переменных типа int. Теперь заменим в этой строке int
на ml:
ml m2_2[NR];
По аналогии с нредыдущим объявлением можно сказать, что это массив NR
переменных типа ml. To есть это массив NR массивов из NC переменных типа int, то
есть массив массивов, о котором мы знаем, что он занимает непрерывный
участок памяти, и к отдельным элементам которого можно обратиться как m2_2[i][j]1.
Но если приписать слева ключевое слово typedef, как показано ниже, то массив
массивов исчезнет и возникнет новый тип т2_2:
typedef ml m2_2[NR];
1 С таким массивом мы впервые встретились в главе 4 (раздел «Указатель на массив»).
108
Глава б. Примитивные объекты
Если объявить объект р типа ш2_2: т2_2 р, то теперь р — это массив из NR массивов,
в каждом из которых по NC переменных типа int. И к этому массиву можно
обращаться обычным образом, как показано в листинге 6.4.
Сложные объявления
До сих пор понимание каждого нового объявления в C++ вызывало у нас
трудности. Нам приходилось каждый раз напряженно думать — что же такое этот новый
тип — функция, возвращающая указатель, или указатель на функцию, массив
указателей или указатель на массив? Настало время подобрать ключ к различным
объявлениям, чтобы в дальнейшем понимать их без труда. В этом нам поможет
простое правило: при разборе сложных объявлений следует начинать с имени,
затем двигаться вправо, пока это возможно, затем поворачивать влево до очередной
скобки и затем опять стремиться вправо.
Рассмотрим, к примеру, уже известное нам объявление
char *words[]
Его разбор начинается с поиска имени. В нашем случае это words. Двигаясь от него
вправо, увидим квадратные скобки, означающие «массив». Получается, что «words
это массив...». Мы продвинулись вправо до упора и теперь остается двигаться
влево, перепрыгивая через уже разобранные квадратные скобки и слово «words».
Первый символ, на который мы при таком движении натыкаемся, — это
звездочка *, означающая «указатель». Значит, наше описание нового объекта можно
продолжить: «words это массив указателей на...». Дорога вправо нам закрыта, там все
знаки уже разобраны. Остается только сместиться влево и встретить там слово
«char». Значит, объявленный тип «words» — это «массив указателей на char».
Теперь изменим наше объявление, заключив звездочку и «words» в круглые
скобки, и попробуем снова разобрать его:
char (*words)[]
Как и прежде, начинаем со слова words — «words это...». Но теперь движению
вправо мешает круглая скобка. Значит, сворачиваем влево, перепрыгиваем через уже
разобранное слово «words» и натыкаемся на звездочку. Получается, что «words
это указатель на...». Дальнейшему движению влево мешает круглая скобка? и мы
поворачиваем направо, перепрыгиваем через звездочку, слово «words» и уже
разобранную круглую скобку ) и натыкаемся на квадратные скобки. Значит, «words
это указатель на массив». Теперь пути вправо нет, значит, сворачиваем влево, но
там остается только слово char, потому что все остальные символы (имя words, две
круглые скобки С) и звездочка *) уже разобраны. Значит, * words это указатель на
массив, хранящий объекты типа char».
Два предыдущих объявления уже встречались нам в этой книге. Но правила
разбора объявлений позволяют нам понимать гораздо более сложные объявления,
например:
int *(* foo())[15]
Сложные объявления
109
Как обычно, начинаем с имени foo: «foo это...». Двигаясь вправо, натыкаемся на
две круглые скобки. Значит, «foo это функция, возвращающая...». Слово
«возвращающая» всегда ставится после «функция», потому что функция всегда что-то
возвращает (например, void). Дальнейшему движению вправо мешает круглая
скобка, значит, идем влево, перепрыгиваем уже разобранные О и foo и
натыкаемся на звездочку *. Значит, «foo это функция, возвращающая указатель на...».
Дальнейший путь влево преграждает круглая скобка, поэтому нужно двигаться
вправо, где неразобранными остались только квадратные скобки [15]. Значит, «foo
это функция, возвращающая указатель на массив из пятнадцати...чего?». Двигаясь
влево и перескакивая через уже разобранные символы, натыкаемся на звездочку
и следующее за ней слово int. Значит, «foo это функция, возвращающая указатель
на массив из пятнадцати указателей на int».
Задача 6.3. Разберите объявление char *(*(**foo [8] [])())[];
Так разбираются «чужие» объявления. Но правила разбора легко применить
и при создании объявлений собственных. Пусть, например, нужно объявить
«массив N указателей на функцию, возвращающую указатель на функцию,
возвращающую указатель на char». Описание нового типа кажется безумно сложным, но,
действуя по шагам и следуя правилам разбора, легко написать объявление и затем
проверить его.
Прежде всего нужно придумать имя массива, пусть это будет агг. Массив из
N элементов записывается как
arr[N]
Но нам нужен массив из N указателей, поэтому приписываем справа круглую
скобку, которая отразит нас влево, и там поставим звездочку:
* arr[N])
Теперь из массива указателей нужно сделать массив указателей на функцию. Для
этого поставим слева скобку, чтобы отразиться вправо, а там припишем пару
круглых скобок — символ функции:
(* arr[N])()
Эта функция должна возвращать указатель, поэтому справа нужна круглая
скобка, чтобы изменить направление разбора, а слева — звездочка
*(* arr[N]) О)
Этот возвращаемый указатель — опять на функцию, поэтому нам нужно
поставить круглую скобку слева, чтобы отразиться вправо, где нужна еще одна пара
круглых скобок:
(*(* arr[N]) О) О
И наконец, эта последняя функция должна возвращать указатель на char.
Поскольку справа ничего больше не добавляется, круглая скобка там не нужна,
достаточно поставить слева звездочку и ключевое слово char - и объявление готово:
char *(*(* arr[N]H) О;
110
Глава б. Примитивные объекты
Тот же самый тип можно создать по шагам с помощью typedef. Но теперь начинать
нужно не с начала словесного описания (массив указателей на...), а с его конца
(...возвращающую указатель на char).
Итак, создадим сначала тип р — указатель на char
typedef char *p; //p - указатель на char
Затем — функцию fl(), возвращающую указатель на char:
typedef p fl(); //fl() возвращает указатель на char
Далее — указатель на функцию, возвращающую указатель на Char:
typedef fl *pfp; //указатель на функцию....
Теперь — функцию f2(), возвращающую указатель на функцию, возвращающую
указатель на char:
typedef pfp f2(); //f2() возвращает указатель на
Осталось создать pf2 — указатель на функцию, возвращающую указатель на
функцию, возвращающую указатель на char:
typedef f2 *pf2; //указатель на функцию
И наконец, создаем массив arr[N] указателей на функцию, возвращающую
указатель на функцию, возвращающую указатель на char:
pf2 arr[N]: //массив N указателей
Обратите внимание, в последней инструкции нет слова typedef, потому что она
создает не описание типа, а реальный объект, занимающий компьютерную
память — массив N указателей на... (продолжите сами).
Свобода создания сложных типов не означает вседозволенности. Правильно
расставляя скобки или используя typedef, можно создать нечто фантастическое, не
имеющее в C++ ни малейшего смысла. Поэтому стоит кратко перечислить то, чего
не бывает в C++.
¦ Не бывает массивов функций. Возможны только массивы указателей на...
¦ Функции не могут возвращать функции, а только указатели на функции.
¦ Функции не возвращают массивы, а только указатели на нулевой элемент
массива или указатели на массив
¦ В многомерных массивах только в самых правых скобках можно не указывать
размер. Например, char[10] [] — правильный двухмерный массив (по сути—
массив массивов), a char [][10] — это нечто бессмысленное, невозможное
(подумайте, почему). Точно так же int [10][20][] — правильный трехмерный
массив, a int[][10][20] — не имеющая смысла комбинация букв, чисел и
квадратных скобок.
¦ Несмотря на простоту, правила объявления сложных объектов все же
оставляют место для сомнений, особенно у человека неопытного. Проверить себя
Сложные объявления
111
позволит программа cdecl.exe, которую можно найти среди файлов к этой книге
в архиве sources.zip.
Эта старомодная программа управляется командами, набираемыми на
клавиатуре. Одна из самых полезных ее команд — explain — позволяет получить
словесное описание нового типа. На рис. 6.1 показано, как программа cdecl,
запущенная из оболочки FAR, разбирает некоторые сложные (и не очень)
объявления.
Рис. 6.1. Cdecl «разъясняет» смысл объявлений в C++
Глава 7
«Я» и мир объектов
Часы
Перечисления и записи, которые мы научились создавать в предыдущей главе, не
имели собственных функций; лишенные внутреннего, скрытого от посторонних
глаз устройства, они напоминали препарированную лягушку, у которой все
торчит наружу.
Что же касается «настоящих» объектов, имеющих собственные функции
(интерфейс) и внутреннее устройство, то пока мы пользовались только готовыми
объектами из стандартной библиотеки C++. Они уже помогли нам искать анаграммы
(см. раздел «Анаграммы» главы 3) и составлять словари (см. главу 5). Нет
сомнений, что с их помощью можно решить много других распространенных задач. Но
даже самый лучший набор готовых объектов нельзя применить ко всем задачам,
рано или поздно придется создавать объекты самому.
Нашим первым объектом будут часы. Конечно, не обычные часы, которые можно
положить в карман или носить на руке, а некий объект, показывающий время на
экране монитора.
В C++ для создания объектов служат объявления. Например, инструкция int sec
создает объект sec типа int. Компилятору не нужно рассказывать, что такое int,
встретив объявление int sec, он уже знает, какие операции возможны с
целочисленной переменной и сколько оца занимает памяти.
Но если объявляется новый объект, например myclock watch, компилятор должен
знать, что такое «myclock», сколько памяти отвести для объекта watch и как к нему
«подступиться», какие у него параметры, собственные функции и операторы.
Все это компилятор узнает из описания объекта, которое начинается ключевым
словом class. Согласно традиции языка C++ описания новых объектов, так же как
и описания стандартных, лучше хранить в отдельных файлах с расширением .hpp.
В листинге 7.1 показано содержимое файла myclock.hpp, хранящего описание
объектов типа mycl ock.
Часы
113
Листинг 7.1. Файл myclock.hpp — описание объекта myclock
using namespace std;
class myclock {
public:
void dispO:
void clockset(int h. int m. int s) {
hour_ = h; min_ - m: sec_ = s;
}
void tickO {
if(sec_ ==59) {
sec_ = 0; min_++;
}
else sec_++:
if(min_ == 60) {
min_=0: hour_++;
}
}
private:
void sw_() {
cout.fill('O'):
cout.widthB);
}:
int hour_: int min_: int sec_:
}:
void myclock::disp() {
sw_(): cout « hour_:
cout « ':':
sw_(): cout « min_:
cout « ':';
sw_(): cout « sec_:
cout « '\r';
}
Как видим, описание объектов типа myclock начинается словом class, далее идут
фигурные скобки (в них-то и содержится описание) и точка с запятой. Внутри
фигурных скобок выделены две области, помеченные словами private (тайный)
и public (открытый):
class {
public:
private:
}V
В области private описана «сердцевина» будущего объекта, все, что у него есть
самого дорогого, то, к чему внешний мир не имеет непосредственного доступа. Если
искать этому аналогию в реальном мире, то область private похожа на
внутренности телевизора и доступна только специалистам. Без области private само понятие
объекта теряет смысл, ведь если ко всему в объекте можно получить доступ извне,
то теряется его обособленность, он сливается с основной программой. Чтобы под-
114
Глава 7. «Я» и мир объектов
черкнуть особую роль объектов из области private, будем всюду в дальнейшем
выделять их имена «нижней чертой», например hour_, sw_() и т. д.
В области public описан как бы пульт управления объектом, то есть его
собственные функции, с помощью которых можно получить доступ к области private,
узнать или задать состояние объекта, заставить его выполнить какие-то действия.
Область public — это посредник между объектом и внешним миром. И конечно,
она должна быть спроектирована так, чтобы эффективно управлять объектом, не
нарушая его целостности (примерно как пульт управления телевизором).
Описание объекта, показанное в листинге 7.1, хранит в области private функцию
sw_(), помогающую правильно отобразить время1, а также переменные hour_ (час),
mi n_ (минута), sec_ (секунда).
В открытой области будущего объекта видны три его собственные функции:
¦ void dispO — показывает время; В этой функции применен специальный
управляющий символ '\г' не переводящий строку, а лишь перемещающий
курсор к ее началу2;
¦ void clockset(int h, int m, int s) — устанавливает время;
¦ void tick() — переводит часы на секунду вперед.
Заметим, что внутри объекта функция может быть полностью описана (например,
void tickO) или только объявлена (пример — void dispO;). «Тело» объявленной
функции располагается вне описания объекта и, чтобы стало ясно, что функция
«живет» не сама по себе, а принадлежит объекту определенного типа, ее имя
помечается названием типа и двумя двоеточиями:
void myclock::disp() {
Г
Задача 7.1. Функция tickO в листинге 7.1 содержит ошибку. Когда она
проявится? Напишите правильную функцию tickO.
Имея описание, можно создать сам объект — примерно так, как это делает
программа из листинга 7.2.
Листинг 7.2. Программа, использующая объекты класса myclock
#include <iostream>
#include "myclock.hpp"
using namespace std;
int mainO {
myclock a;
1 Функция sw_() нужна для того, чтобы всегда выводить на экран 01, а не 1. В ней используются две
собственные функции объекта cout: cout.widthB) задает ширину ноля вывода (у нас — два символа),
a cout.fill('O') — символ, которым заполняются оставшиеся слева поля (в нашем случае — '0').
2 Заметим, что endl — иное обозначение для управляющего символа \п\ вызывающего возврат
курсора и перевод строки.
Часы
115
a.clocksetdO. 59. 30):
forCint i-0: i < 1000; i++) {
a.tickO;
a.dispO:
}
}
Описание объектов типа myclock находится в файле myclock.hpp (см. листинг 7.1)
и включается в текст нашей программы директивой #include "rtiyclock.hpp".
Обратите внимание: название файла myclock.hpp обрамляется не треугольными
скобками о, а двойными кавычками, показывая, что включаемый файл создан
программистом, а не принадлежит компилятору.
Созданный нами объект объявляется в строке:
myclock a;
Здесь а — конкретный объект типа mycl ock, он занимает память и в некотором
смысле реально существует. Что же касается класса (будем называть так конструкцию
class <имя>{}), то это всего лишь идея объекта, его описание. Имея описание,
можно создать множество объектов, например массив, состоящий из десяти часов:
myclock clocks[10];
Только что созданным объектом можно управлять с помощью его собственных
функций. Следующая инструкция устанавливает начальное время — 10 часов,
59 минут 30 секунд:
a.clocksetdO.59.30):
Далее в цикле имитируется ход часов в течение 1000 секунд. После каждого
вызова функции a. tick О на экране функцией a.dispO высвечивается «время».
Заметим, что наши часы получились игрушечными — это лишь циферблат со
стрелками, переводимыми вручную. Настоящие часы вызывают функцию a.tickO
точно раз в секунду. Часам, показанным в листинге 7.2, не хватает самого
главного — механизма определения времени.
Завершим этот раздел важным замечанием, помогающим понять природу
объектов в C++ и их отличие от объектов реального мира. Вспомним, что часы, которые
мы носим на руке или в кармане, идут независимо от того, обращаем мы на них
внимание или нет. Они автономны, пока не кончится завод или не разрядится
батарейка. Совсем не таковы часы, показанные в листинге 7.2. Эти машинные часы
все время нужно подталкивать извне, вызывая функцию tick(). Часы не могут
«уйти в себя», потому что процессор будет занят только их обслуживанием и не
сможет отвлечься на что-то другое. Правда, современные операционные системы
многозадачны, и в программе на C++ можно создать несколько независимых
объектов, использующих процессор по очереди. Такие объекты могут принимать друг
от друга данные и сообщения, но все эти многозадачные операции нестандартны
и организуются в разных операционных системах по-разному. К тому же они
довольно сложны, и в этой книге, предназначенной для начинающих
программистов, мы о них больше говорить не будем.
116
Глава 7. «Я» и мир объектов
Часы с «кнопочками»
— Конечно, бормотограф — сложный прибор,
но зачем усложнять и без того сложную
вещь?
— А разве он усложнял? — сочувственно
спросил Винтик.
— Конечно, усложнял. Стал делать не просто
бормотограф, а какой-то комбинированный
бормотограф с пылесосом.
Н. Носов. Приключения Незнайки и его друзей
Первый созданный объект, как первый блин, — очень несовершенен. Глядя на
него, понимаешь, что краткость — не всегда сестра таланта. Вспомним хотя бы
собственную функцию clocksetO, способную как угодно менять переменные из
зоны private: hour_, min_, sec_. А ведь на то и зона private, чтобы к ней не было
доступа из внешнего мира.
Но самое страшное не это. Гораздо хуже то, что наш объект clock способен
показывать только то, для чего и предназначены часы — время. Если же взглянуть
на реальные часы, то окажется, что все они — настоящий гибрид бормотографа
и пылесоса. Современные часы показывают день недели, число, месяц, год,
работают в режиме секундомера и способны выполнять много других задач. Поэтому,
моделируя их (и любые другие объекты) на компьютере, нужно думать о том, как
этот объект менять, легко ли будет добавлять новые функции и возможно ли это
вообще.
К сожалению, наши первые часы вообще не способны меняться, потому что
созданы только для показа времени на экране. Чтобы сделать их более
универсальными, нужно включить в часы специальный объект, в зависимости от состояния
которого те будут менять свое поведение. Новый вариант класса clock показан
в листинге 7.31.
Листинг 7.3. Часы со многими состояниями (файл stateclock.hpp)
#include <iostream>
using namespace std;
class myclock {
public:
void resetO:
void changejnodeO:
void increment);
1 При подготовке этого раздела использовалась статья Kevlin Henney «From Mechanism to Method:
State Government», «C/C++ Users Journal», C++ Experts Forum, June 2002.
Часы с «кнопочками»
117
void tickO;
void show_time();
private:
enum mode {
DisplayingTime_. SettingHours_. SettingMinutes_
}:
mode behavior_;
int hour_. minute_. second_;
}:
void myclock::show_time() {
cout.widthB); cout « hour_ « " .":
cout.widthB): cout « minute_ « " ";
cout.widthB); cout « second_ « " ";
cout « '\r':
}
void myclock::reset О {
behavior_ = DisplayingTime_;
hour_ - 0: minute_ = 0: second_= 0:
}
void myclock::change_modeО {
behavior_ - mode((behavior_ + 1) % 3):
}
void myclock: increment О {
switch(behavior_) {
case DisplayingTime_:
break:
case SettingHours_:
hour_ = (hour_ + 1) % 24:
break:
case SettingMinutes_:
minute_ = (minute_ + 1) % 60:
break:
}
}
void myclock::tick() {
switch(behavior_) {
case DisplayingTime_:
if(++second_ ==60. {
second_ = 0:
if(++minute_ ==60) {
minute_ = 0:
hour_ = (hour_ + 1) % 24:
}
}
break:
case SettingHours_:
break:
case SettingMinutes_:
break:
}
}
Эти новые часы способны находиться в трех состояниях: DisplayingTime_
(показываем время), SettingHours_ (устанавливаем часы), SettingMinutes_
(устанавливаем минуты). Состояние часов хранится в объекте behavior, который может
118
Глава 7. «Я» и мир объектов
быть изменен двумя собственными функциями reset О (делает behavior_ равным
DisplayingTime_ и зануляет часы, минуты и секунды) и change jnodeO (циклически
переключает режимы часов с помощью оператора X1).
Переводом «стрелок» часов занимается собственная функция incrementO. Если
behavior_ равен SettingHours_, функция увеличивает часы:
Hour_ = (hour_ + 1) % 24;
Здесь также используется оператор X, напрабляющий часовую «стрелку» по
кругу, ведь 25%24 = 1. Аналогично, при behavior_, равном SettingMinutes_, переводится
и минутная «стрелка».
Если же behavior_ равен DisplayingTime_, то функции incrementO нечего делать
и она просто завершается, встретив инструкцию break.
В зависимости от состояния часов по-разному работает и функция tickO. Если
behavior_ равен DisplayingTime_, она увеличивает время на секунду и смотрит — не
нужно ли изменить минуты и часы. Если же часы находятся в другом состоянии,
функция tickO просто ничего не делает.
Часы, созданные в этом разделе, как бы управляются несколькими кнопками.
Кнопка RESET, которой соответствует собственная функция resetO, задает
начальное состояние часов. Кнопка CHANGE_MODE (ей соответствует функция change_
modeO), циклически меняет состояние часов, а кнопка INCREMENT может, в
зависимости от числа нажатий кнопки CHANGE_MODE, увеличивать минуты, часы или
просто ничего не делать. Программа, показанная в листинге 7.4, имитирует
нажатие кнопок таких часов.
Листинг 7.4. Управление «кнопочными» часами
#include <iostream>
#include "statedock.hpp"
int mainO {
myclock С; //создаем объект С
C.resetO:
C.changejnodeO:
for(int i = 0; i < 6: i++)
C.incrementO; //устанавливаем часы
C.changejnodeO:
fordnt i = 0; i < 30; i++)
C.incrementO; //устанавливаем минуты
C.changejnodeO; //переходим к отсчету времени
while(l) {
C.tickO; //прошла секунда
C.show_time(); //показать время
}
}
1 Напомним, что оператор %, с которым мы познакомились в разделе «Enum» главы 6, вычисляет
остаток от деления делимого на делитель. Например, 1%3, как и 4%3 равно 1.
Карты
119
Сначала нажимается кнопка RESET (C.resetO;). Затем кнопка CHANGE_MODE
(С. change_mode();) переводит часы в состояние Setti ngHours_, и шестикратное
нажатие кнопки INCREMENT (С. increment О;), заданное циклом for(), установит время —
шесть часов. Затем снова нажимается кнопка CHANGE_MODE, и часы переходят в
состояние ChangingMinutes_. Тридцатикратное нажатие кнопки INCREMENT означает
для часов, находящихся в этом состоянии, установку минут C0 минут). После еще
одного нажатия CHANGEJ40DE часы переходят в состояние Di spl ayi ngTime_ и,
крутясь в бесконечном цикле while(l){}, поочередно вызывают собственные функции:
C.tickO; (увеличить время на секунду) и C.show_time(); (показать его на экране).
Войдя в бесконечный цикл, наши часы уже не способны поменять состояние, их
ход можно прервать только средствами операционной системы, одновременно
нажав на клавиатуре кнопки Ctrl+Break.
Объект типа Clock, созданный в этом разделе, гораздо совершеннее наших
первых часов. В нем уже разделяются интерфейс и внутреннее устройство, и
собственные функции не имеют доступа к внутренним переменным hour_, minute_,
second_. Поведением этих часов можно управлять, меняя состояние внутреннего
объекта behavior__. Эти часы уже можно модифицировать, добавляя новые
состояния Bbehavior_ и дописывая новые варианты поведения в функциях icrementO
HtickO.
Задача 7.2. Добавьте режим секундомера к часам из листинга 7.3.
Карты
Приступая к решению задачи, нужно, прежде всего, думать о том, какие объекты
должны быть созданы, какими данными будет владеть каждый объект, какие
сообщения они будут посылать друг другу. От удачного подбора объектов во многом
зависит общий успех.
Но, к сожалению, не существует «самого лучшего» способа выделения объектов.
Даже опытный, программист, проделав значительную часть работы, может понять,
что шел по ложному пути, и нужно все начинать сначала. Поэтому единственный
способ научиться — это программировать самому, причем не так, как можешь,
а так, как нужно. Программисты учатся, постоянно переписывая программы и
постепенно приближая их к «идеалу».
Для примера попробуем наметить контуры программы, играющей в карты. В
отличие от других более серьезных задач, где объекты могут быть весьма
абстрактными, основные объекты для игры в карты очевидны: карта, колода, игроки и, быть
может, некий судья — контролирующий объект, подсчитывающий очки, ставки,
ведущий статистику и определяющий победителя.
Начнем с описания основного объекта — игральной карты. Будем для простоты
считать, что в нашей колоде есть карты, начиная с шестерки. Поэтому объект типа
card (класс card) может быть таким, как в листинге 7.5.
120
Глава 7. «Я» и мир объектов
Листинг 7.5. Игральная карта
string suits[4] = {"Б \"Ч \"П "."Т "};
string
cds[9]={ \ "."8 "."9 ,,.0".,,В \"Д "."К "."Т "};
enum suit{diamonds, hearts, spades, clubs};
enum cname{9ix.seven.eight.nine.ten.jack.queen.king.ace};
class card {
public:
void set_suit(suit s) { ms_ = s; }
void set_cn(cname c) { cn_ = c:}
string get_suit() { return suits[ms_J; }
string get_cn() { return cds[cn_]; }
private:
suit ms_;
cname cn_;
}:
В области private нашего объекта хранятся масть (переменная ms_ типа suit) и
наименование карты (переменная сп_ типа cname). И suit и cname— перечислимые
типы, отдельные значения которых задаются целыми числами. Но мы
привыкли называть карты иначе, не @,0), а «шестерка бубен», поэтому вне класса
размещены два массива строк, хранящие более привычные человеку обозначения
карт. Массив suits[4] хранит четыре масти «Б» (бубны), «Ч» (черви), «П» (пики),
«Т» (трефы), а массив cds[] — имена карт. Если первым ставить имя карты, то пара
10П будет обозначать десятку пик, а КЧ — короля червей.
Массивы suits[] и cds[] используются двумя собственными функциями класса
card: функция getsuitO возвращает масть карты (одну из букв: «Б», «Ч», «П»
или «Т»), а функция getcnO — ее название. Две другие собственные
функции класса card устанавливают значение масти (set_suit()) и название карты
(set_cn()).
Обратите внимание: массивы suits и cds расположены вне класса card, их
называют глобальными, и в программе, состоящей из одного файла, они доступны
всюду — из любого объекта, функции mainO и вообще из любой функции. Возникает
вопрос: где должны находиться эти массивы? Правильно ли они сделаны
глобальными, всем доступными? По идее, обозначения мастей и карт принадлежат самой
карте, но C++ запрещает присваивание значений переменным внутри класса, ведь
реальные переменные со значениями есть только у объекта, а класс — это всего
лишь описание объекта. Поэтому нам придется оставить массивы suits[] и cds[]
вне описания объекта card.
После определения карты можно подумать о колоде и связанных с ней
собственных функциях. Очевидно, колоду нужно тасовать, а также вынимать из нее карты
для раздачи игрокам. Поскольку карты при раздаче снимаются сверху, для
колоды больше всего подойдет контейнерный тип под названием deque
(двусторонняя очередь), похожий на vector, но позволяющий удалять и добавлять элементы
с обоих концов. То есть карты можно будет вынимать сверху и снизу и точно так
же возвращать в колоду. Описание колоды, показанное в листинге 7.6, содержит
три собственные функции:
Карты
121
¦ i ni () — создание колоды;
¦ shuf f 1 е() — тасование;
¦ get() — снятие карты с верха колоды.
Листинг 7.6. Колода
class deck {
public:
void ini() {
card tmp;
for(cname i = six ;n <= ace; i = static_cast<cname>(i +1))
forCsuit j = diamonds: j <= clubs; j = static_cast<suit>(j+D) {
tmp.set_suit(j);
tmp.set_cn(i);
tdeck_.push_back(tmp);
} "
shuffleO:
}
void shuffleO {
random_shuffle(tdeck_.begiri(). tdeck_.end());
}
card get О {
card tmp = tdeck_.front();
tdeck_.pop_front();
return tmp;
}
private:
deque<card> tdeck_;
}:
Функция ini () создает колоду, где сначала располагаются бубны, начиная с
шестерки и кончая тузом, затем черви и т. д. Далее колода «тасуется» функцией
random_shuffle().
Функция get О создает временную переменную temp, затем переписывает в нее
первую карту колоды (tmp = tdeck.frontO), а потом эта карта удаляется из колоды
функцией tdeck_. pop_f ront ().
Чтобы вынуть последнюю карту колоды, нужны следующие инструкции:
Tmp = tdeck_.back(); // прочитать последнюю карту
tdeck_.pop_back(): // удалить последнюю карту
Только что созданная нами колода довольно универсальна и годится для самых
разных игр («дурака», «козла», «преферанса»). Без особых усилий можно создать
колоду из 52 карт, пригодную для любой карточной игры.
В этом разделе нам, конечно, не удастся создать полноценную программу,
играющую в карты. Эта задача слишком сложна, поэтому ограничимся раздачей карт,
предположив, что игроки будут играть в «козла» — одну из примитивных
карточных игр. Но перед раздачей необходимо описать еще один объект — самого
игрока (объекта player). У нас он будет очень простым: с функциями showO (пока-
122
Глава 7. «Я» и мир объектов
зать карты), ini () (подготовиться к раздаче карт) и add() (взять карту из колоды).
Описание объекта pi ауег приведено в листинге 7.7.
Листинг 7.7. Игрок
class player {
public:
void showO {
for(unsigned i = 0; i < hand_.size(): i++)
cout « hand_[i].get_cn() « " ";
cout « endl;
for(unsigned i = 0; i < hand_.size(); i++)
cout«hand_[i].get_suit() « " ":
cout « endl « endl;
}
void iniО {
hand_.clear();
}
void add(card c) {
hand_.push_back(c);
}
private:
deque<card> hand_;
}:
Как видим, карты игрока, как и в целой колоде, хранит контейнер deque<card> hand_.
Функция hand_.cl ear О, используемая в функции ini О, очищает контейнер, а
функция hand_.push_back() добавляет в него карту.
Для игры в «козла» нужны две пары игроков, то есть каждому достанется по 9 карт.
Программа, раздающая карты, показана в листинге 7.8.
Листинг 7.8. Раздача карт
#include <iostream>
#include <deque>
#include <algorithm>
using namespace std:
#include "play.hpp"
#define NOFP 4
#define NOFC 9
int mainO {
player pls[N0FP]:
deck mydeck:
mydeck.ini():
for(int i = 0; i < NOFP: i++) {
pls[i].ini():
for(int j = 0; j < NOFC: j++)
pis[i].add(mydeck.get()):
}
for(int i = 0: i < NOFP: i++)
pls[i].show():
}
Карты
123
В этой программе описания объектов card, deck и player помещены в файл play.hpp
и включаются в текст директивой #include. Далее директивы #define задают две
константы: N0FP — число игроков и N0FC — число карт у каждого игрока.
Выполняемые программой действия крайне просты. Сначала создаются массив
игроков (player pls[N0FP];) и колода (deck mydeck;). Затем в колоду
добавляются карты и она тасуется. Все это делает функция mydeck.iniО. После этого
карты «сдаются» игрокам. Делают это два цикла for(). Во внешнем цикле for(int
i=0; i < NOFP; i++){} функция pls[i].iniO отнимает карты у каждого игрока, если
они у него остались от прежней игры. Далее цикл fordnt j=0; j<N0FC; j++) pls[i].
add(mydeck.getO) вытаскивает из колоды NOFC карт подряд и передает их одну за
другой игроку под номером ! Очередная карта вытаскивается из колоды
функцией mydeck. get О и передается i-му игроку функцией pls[i] .add(). В конструкции
pls[i].add(mydeck.getO) нет ничего страшного. Ведь функция pls[i].add()
принимает объект типа card, а функция mydeck. get О возвращает объект типа card. Так
что mydeck. get() можно считать неким объектом типа card, передаваемым функции
p!s[i].add() в качестве аргумента.
Завершается наша программа выводом на экран карт, оказавшихся у игроков.
Делается это в цикле:
fordnt i = 0; i < N0FP;i++) pls[i].show():
В этом разделе мы успели только сдать карты игрокам. Читателю было бы крайне
полезно научить программу какой-нибудь примитивной игре (например, тому же
«козлу»). Освоить C++ можно, только самостоятельно размышляя над
устройством и взаимодействием объектов, а затем пытаясь воплотить эти мысли в
работающей программе.
Подумаем вместе, какие объекты должны быть доступны игроку (объекту типа
player). Прежде всего, это текущая взятка, количество карт в которой
колеблется от 0 (первый ход) до 3-х (игрок кладет последнюю карту). Наверное, нужно
создать класс trick (взятка) и передавать соответствующий объект игрокам для
записи в него очередного хода. Чтобы не запутаться, лучше создать сначала
игрока, знающего только текущую взятку. Пусть он тупо ходит «в масть», не думая
о числе набранных очков.
Обучившись примитивной игре «по правилам», можно переходить к
определенной стратегии (если взятка «наша», кладем карту с максимальным числом очков
и т. д.). Такая игра уже требует знания ценности карт, а также предыдущих взяток.
Видимо, придется создать еще один объект, хранящий прошлые взятки и
подсчитывающий очки после завершения партии. При этом нужно позаботиться о том,
чтобы игроки даже в принципе не смогли подглядеть чужие карты. Тогда
можно устроить конкурс на лучшую пару игроков (для одной пары создается класс
playerl, для другой — player2). Описания (классы) соревнующихся игроков
можно включить в программу директивой #i ncl ude, а победу присуждать тем, кто чаще
выигрывает.
124
Глава 7. «Я» и мир объектов
Константы
Программа, сдающая карты игрокам, с которой мы познакомились в предыдущем
разделе, использует два массива suits[] и cds[], хранящие названия мастей и карт
(см. листинг 7.5). Эти массивы определены вне классов и функций и потому
доступны всюду в программе. Значит, их может поменять любая функция любого
объекта. И будет очень трудно понять «кто это сделал», если программа велика.
Вот почему глобальных (то есть видимых в программе отовсюду) объектов
следует избегать.
Впрочем, глобальные объекты можно защитить словом const. Объявление
const string suits[4] .= {"Б ". "Ч ". "П ". "Т "};
говорит компилятору, что suits[] — массив постоянных строк. И теперь любая
попытка поменять какую-нибудь строку в массиве вызовет сообщение компилятора
об ошибке. Такая защита очень уместна в нашем случае, потому что названия
мастей и карт вообще никто не должен менять в программе.
Естественно, защитить словом const можно и другие объекты. Так, например,
объявление const int i=5 говорит о том, что i — константа и попытка присвоить ей
другое значение (например, так: i=10;) будет пресечена компилятором.
Заявить о том, что некий объект не меняется, можно и косвенным образом, через
указатель:
int a;
const int *p; //указатель на константу
Здесь р — указатель, с помощью которого нельзя менять то, на что он указывает.
Например, в следующем фрагменте программы инструкция *р=10 будет
отвергнута компилятором на том основании, что этот указатель не может менять
переменную а:
int а;
const int *p; //указатель на константу
р = &а:
*р = 10: // Нельзя! р - указатель на константу!
Но поскольку сама переменная объявлена обычным образом, ее может менять
другой указатель, объявленный без ключевого слова const.
Раз переменная может быть постоянной, то может быть постоянным и сам
указатель. В этом случае он объявляется так:
//постоянный указатель
char * const p = "Учись хорошо! ":
С помощью такого указателя можно менять то, на что он указывает, но сам он
остается постоянным и перенаправить его на другую область памяти невозможно.
Программа, показанная в листинге 7.9, присваивает постоянному указателю р ад-
Константы
125
pec массива а[]. Как и всякой константе, указателю присваивается значение при
объявлении.
Листинг 7.9. Постоянный указатель
#include <iostream>
using namespace std;
int mainO {
char a[] = "Тимирязев";
char b[] = "Мечников";
char * const p = a:
рСЗЗ-'е':
р[5]-'з':
p[6]-V:
cout « p « endl;
//p=b: Нельзя, указатель постоянный
}
Но поскольку постоянен указатель, а не то, на что он указывает, можно спокойно
менять отдельные буквы в массиве а. Нельзя только присваивать указателю
другое значение.
В отличие от указателей постоянную ссылку нет нужды объявлять, потому что
она и так постоянная и навеки связана с одним объектом. А вот ссылка на
константу, объявляемая так, как показано ниже, может быть очень полезна:
int i:
const &г = i;
Получается ссылка, которая может только пассивно следить за объектом, но не
в состоянии его изменить. Программа, показанная в листинге 7.10, сначала
создает переменную int, затем связывает с ней ссылку на константу.
Листинг 7.10. Пассивная (следящая за объектом) ссылка
finclude <iostream>
using namespace std;
int mainO {
int a;
const int &b = a;
a = 3;
cout « b « endl; //3
//b=5; Нельзя!
}
Сама переменная а «не подозревает», что ссылка считает ее константой.
Переменную легко можно изменить, что и отражает выводимая на экран ссылка. А вот
изменить ссылку, раз она ссылается на константу, нельзя.
Нам осталось только понять, как же объявляются объекты, указатели и ссылки
со словом const. Оказывается, секрет прост: нужно читать объявление в обратном
порядке: справа налево. Посмотрим еще раз на объявление:
int * const a = &р;
126
Глава 7. «Я» и мир объектов
Читая его справа налево, получим: «постоянный (const) указатель (*) на int».
Точно так же, читая справа налево объявление
const int * const a = &р:
получим «постоянный указатель на константу типа int»1. Такое объявление
гарантирует нам неизменность как указателя, так и объекта, чей адрес он хранит.
Задача 7.3. Просмотрите все объявления переменных со словом const и убедитесь,
что правило чтения справа налево работает.
1 Слово const может быть и перед int: const int* const a - &p; //постоянный указатель на
константу int.
Глава 8
Рождение и смерть объектов
Богатые и бедные классы
В начале предыдущей главы мы сначала создавали объект а типа myclock, а затем
«переводили его стрелки» с помощью собственной функции void clocksetd'nt h, int
m.int s), хотя нам уже известно, что начальное состояние стандартных объектов
C++, таких, например, как string, можно задать при объявлении:
string sC'MAMA"):
Настало время познакомиться со специальной собственной функцией —
конструктором, задающей начальное состояние объекта при его создании. В
листинге 8.1 показан класс myclock с конструктором, позволяющим установить время при
объявлении объекта, например, так:
myclock cldl. 0. 0):
Собственные функции в этом классе для краткости только объявлены; чтобы
класс заработал, нужно определить их как в главе 7 (см. листинг 7.1).
Листинг 8.1. Конструктор для класса myclock
class myclock {
public:
myclock(int h. int m. int s){ //конструктор класса
sec_ = s;
min_ = m;
hour_ = h:
} •
void dispO:
void clockset(int h.int m.int s);
void tickO;
private:
void sw_():
int hour_; int min_: int sec_:
}:
128
Глава 8. Рождение и смерть объектов
Как видите, имя конструктора совпадает с именем класса myclock. С таким
определением класса myclock программа из главы 7 (см. листинг 7.2) может быть
переписана так, как показано в листинге 8.21.
Листинг 8.2. Вызов конструктора при объявлении объекта
#include <iostream>
#include "myclockl.hpp" //класс с конструктором
using namespace std;
int mainO {
myclock aA0. 59. 30); //вызывается конструктор
fordnt i = 0: i < 1000; i++){
a.tickO:
a.dispO;
}
}
В отличие от других собственных функций класса, конструктор нельзя вызвать
явно, присоединив точкой его имя к имени объекта:
//a.myclockdO. 59. 30); Неверно!!!
Конструктор вызывается только при создании объекта
myclock aA0. 59. 30); //вызывается конструктор
и не имеет возвращаемого значения, потому что ему просто некуда его вернуть.
Чтобы подчеркнуть непохожесть конструктора на другие собственные функции,
перед его именем не ставится даже void.
Задача 8.1. Как узнать, что конструктор вызывается при объявлении объекта?
Впрочем, конструктор недаром считается одной из собственных функций, потому
что он, во-первых, действительно функция, имеющая параметры, принимающая
аргументы и ничего не возвращающая. Во-вторых, как и всякая функция,
конструктор в C++ может иметь несколько разновидностей, отличающихся
количеством и типом параметров.
Если, скажем, объявить объект а тийа myclock как
myclock aA0. 59):
то компилятор станет искать определение конструктора, принимающего два
целочисленных аргумента. Если такого конструктора нет, компилятор выдаст
сообщение об ошибке и откажется работать дальше.
Любопытно, что с новым определением класса, данным в листинге 8.1, уже не
проходит объявление объекта myclock «по старинке» (myclock а;). Компилятор
сообщит о том, что не может вызвать конструктор, то есть функцию myclockO,
описанную в классе myclock:
no matching function for call to 'myclock::myclock ()'
1 Обратите внимание на другой включаемый файл myclockl.hpp, содержащий новое определение класса
с конструктором myclock (int h.int m.int s){}.
Анаграммы-2
129
Причина сообщения пбнятна: у нас есть конструктор, принимающий три
аргумента, а при объявлении (myclock а;) должен вызываться конструктор, вообще не
имеющий аргументов, а мы такой еще не создали1.
Но почему же тогда работала программа из листинга 7.2, ведь там тоже
использовалось объявление myclock а, а в описании класса myclock вообще не было
конструкторов?
Все дело в том, что классы без конструктора компилятор C++ считает «бедными»
и снабжает их «социальным» конструктором, который, правда, ничего не делает,
но исправно вызывается при каждом объявлении объекта.
Если же компилятор видит хотя бы один конструктор, то считает, что класс
«вырвался из нищеты» и должен заботиться о себе сам. Теперь конструктор,
вызываемый по умолчанию при объявлении объекта, нужно создавать самому.
Простейший конструктор для наших часов может выглядеть так:
class myclock {
public:
myclock(){min_=hour_=sec_=0:}
Г
Он, как видим, не имеет параметров и поэтому будет вызываться при
объявлении объекта (myclock а;). В отличие от конструктора, предоставляемого «бедным»
классам, этот действует: он устанавливает нулевое время. Если же переменная
объявляется как myclock аA1, 0, 0), вызывается другой конструктор и отсчет
времени начинается с одиннадцати часов.
Завершим этот раздел маленьким открытием: оказывается, конструкторы есть
абсолютно у всех объектов C++, даже таких примитивных, как целочисленные
переменные (типа int). Начальное значение переменной можно указать в скобках,
потому что у нее, как и у всякого объекта, есть конструктор:
int iE):
Анаграммы-2
Знакомство с конструктором и общим устройством объекта позволяет по-новому
взглянуть на старые, уже решенные задачи, такие, например, как «добывание»
анаграмм из словаря (см. листинг 3.13). Программа, созданная в конце третьей
главы, была записана «одним куском». И хоть в ней встречались стандартные
объекты языка C++, вряд ли можно назвать ее объектно-ориентированной.
Та программа, хоть она и делала то, что требовалось, была слишком открытой, она
показывала всем свое внутреннее устройство, хотя «потребителя» интересуют,
1 Напомню, что C++ разрешает использовать несколько функций с одинаковыми именами —
втом случае, когда список параметров позволяет отличить одну функцию от другой (см. раздел
«Функции-тезки» главы 4).
130
Глава 8. Рождение и смерть объектов
прежде всего, сами анаграммы, а не то, каким способом их добывает программа.
Поэтому попробуем спрятать от посторонних глаз внутреннее устройство
программы, оставив на поверхности интерфейс — конструктор, создающий структуру
данных, где хранятся анаграммы, и собственную функцию AnagrOutO, выводящую
анаграммы на экран. И конструктор, и собственная функция будут теперь
принадлежать классу Anagram, показанному в листинге 8.3.
Листинг 8.3. Класс Anagram (файл anagram.hpp)
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class Anagram {
public:-
enum {BSIZE - 80}:
AnagramCchar *FName);
void AnagrOutO:
private:
multimap <string. string> an_:
bool status_:
}:
Anagram::Anagram(char *FName) {
char buff[BSIZE]:
string sbuff:
ifstream infile(FName):
if(infile.failO) {
status_efalse;
cout << "Ошибка при открытии файла" « endl:
return:
}
while A) {
infile.getl1ne(buff. BSIZE):
if(infile.eofO) break:
sbuff-buff:
sort(sbuff.begin(),sbuff.endO):
an_.insert(pair<string.string>(sbuff. buff)):
}
infile.closeO:
status^ = true:
}
void Anagram::AnagrOut() {
if(status_ == false) return:
multimap<string. string >::iterator im. ane:
im = an_.begin():
ane = an_.end():
vector<multimap<string. string >::iterator> chg:
chg.push_back(im):
while(++im != ane) {
chg.push_back(im):
if(im->first != chg[0]->first) { //ключи не равны?
if(chg.size() > 2 ) { //накопились анаграммы?
Анаграммы-2
131
for(unsigned int i = 0:i < chg.sizeO - 1; i++)
cout « chg[i]->second « endl:
cout « endl:
}
chg.clearO; //очищаем вектор
chg.push_back(im); //сохраняем текущий итератор
}
}
if(chg.size() > 1)
for(unsigned int is0:i < chg.size();i++)
cout « chg[i]->second « endl;
}
В нем контейнер multimap <string,string> an_ попал в зону private класса Anagram,
потому что способ хранения анаграмм — личное дело самого объекта. Объект
класса Anagram должен «переварить» словарь и показать анаграммы на экране. Первым
ведает конструктор AnagramCchar *FName), принимающий указатель на имя файла,
хранящего словарь, а вторым — собственная функция AnagrOutO. Программа для
поиска анаграмм показана в листинге 8.4.
Листинг 8.4. Поиск и вывод анаграмм на экран
finclude "anagram.hpp"
int maindnt argc. char **argv) {
if (argc < 2){
cout « "Usage: anagram <dictionary_file>." « endl;
return 1;
}
Anagram af(argv[l]);
af.AnagrOutO:
}
Указатель на имя файла, содержащего словарь, программа получает из командной
строки1. Поэтому нужно проверить число переданных параметров. Если их
меньше 2 (argc < 2), имя файла не передано, и программа, показав на экране краткую
инструкцию, как ей пользоваться, завершается.
Если же число аргументов командной строки не меньше двух, то считается, что
указатель на имя файла содержится в первом аргументе argv[l]. Он и передается
конструктору объекта af: Anagram af(argv[l]);. В отличие от конструктора часов
конструктор анаграмм выполняет большую работу: читает словарь и заполняет
контейнер multimap. А перед этим он должен еще проверить, «хорошее» ли имя
файла ему передали. Если файл открылся удачно, функция infile.faiK) вернет
значение false и только тогда начнется чтение словаря. Если же infile.fail О
вернет true, то произошла ошибка при чтении файла (скорее всего, потому, что
указано неверное имя), конструктор установит status_ = false и завершит работу с
помощью инструкции return. Такая инструкция возможна, потому что она ничего не
возвращает, а только прерывает работу конструктора.
1 См. раздел «Массив указателей» главы 4.
132
Глава 8. Рождение и смерть объектов
После создания объекта af остается только вызвать собственную функцию
af .AnagrOutO. Перед выводом AnagrOutO проверит статус объекта status_ и, если
все нормально, покажет найденные анаграммы на экране.
Второй вариант программы поиска анаграмм выглядит гораздо сложнее первого.
В каком-то смысле это действительно так, потому что он использует несколько
новых понятий, таких как «объект», «конструктор», «зона private». Но если
освоиться с этими понятиями, программа из листинга 8.4 окажется гораздо
удобней своего предшественника, потому что ее сложностью гораздо проще управлять.
Первая программа из листинга 3.13 была единым целым. В ней не было разделения
на тайные и общедоступные объекты, и хранение анаграмм не было в ней
отделено от их поиска. Поэтому такую программу очень трудно менять, добавлять в нее
новые функции. При каждом изменении такой программы приходится подбирать
новые имена объектов, чтобы они не мешали старым. Часто в таких программах
изменение в одном месте приводит к необходимости что-то поменять в
нескольких других. И, достигнув размера в 100-200 строк, программа, написанная «одним
куском», становится полностью неуправляемой.
Совсем по-другому ведет себя программа, в которой правильно выделенные объекты
обмениваются сообщениями. В ней эффект изменений ограничен, как правило,
пространством одной собственной функции. Если к тому же не менять интерфейс
объектов, а только их внутреннее устройство, то необходимые изменения будут
минимальны. В идеале программа должна быть полностью аддитивной, такой, чтобы
любое изменение сводилось только к добавлению чего-то в одно определенное место
программы. Добиться этого совсем не просто даже при объектно-ориентированном
подходе, а программируя «одним куском», — и вовсе невозможно.
Видимость
Казалось бы, объявить объект очень просто: напишем myclock a; — и все. Но теперь
мы знаем, что каждое объявление вызывает конструктор, а когда и сколько раз он
будет вызван — зависит от места, где объект создается. Значит, для понимания
работы конструктора необходимо познакомиться с общей структурой программы.
Итак, программа на C++ состоит из обязательной функции mainO и всего
остального (листинг 8.5).
Листинг 8.5. Различное расположение объектов относительно main()
#include <iostream>
#include "myclock2.hpp"
myclock a;
void nonsenseO {
a.dispO;
cout « end!:
}
int mainO {
nonsenseO;
a.dispO:
cout « endl;
}
Видимость
133
Прежде всего, обратите внимание на включаемый файл myclock2.hpp, содержащий
определение класса myclock с двумя конструкторами — один вызывается по
умолчанию и устанавливает нулевое время, второй имеет три параметра и способен
задать любые час, минуту и секунду.
Теперь посмотрим на листинг 8.5. В нем объект а типа myclock объявляется вне
функции mainO и вне функции nonsenseO. Начальное время при этом не
указывается, значит, при создании объекта будет вызван конструктор по умолчанию
и время станет нулевым @ часов, 0 минут, 0 секунд).
Объект а типа myclock, о котором мы сейчас говорим, называется глобальным,
потому что он виден из любого места программы. При создании глобальных
объектов конструктор вызывается только один раз — еще до вызова функции mainO.
Глобальный объект а типа myclock видят и функция nonsenseO, и функция main О
(см. листинг 8.5), поэтому на экран дважды будет выведено одно и то же время:
00:00:00
00:00:00
А теперь посмотрим на программу, показанную в листинге 8.6.
Листинг 8.6. Доступ к глобальному объекту
#include <iostream>
#include "myclock2.hpp"
myclock a:
void nonsenseO {
a.dispO:
cout « endl:
}
int mainO {
myclock aA0. 59. 59);
nonsenseO: // 00:00:00
a.dispO: cout « endl: // 10:59:59
::a.disp(): cout « endl: // 00:00:00
}
В этой программе объявляются два объекта myclock, один — вне функции mainO,
другой — внутри. Имена обоих объектов одинаковы, но для инициализации
первого используется конструктор по умолчанию, время вторых часов (без секунды
одиннадцать) устанавливается другим конструктором.
Задача 8.2. Как доказать, что глобальный объект создается еще до вызова
функции mainO?
Раз в программе объявлены два объекта с одним именем, то непонятно, какое
время будет показано после вызова функции nonsenseO, а какое — после выполнения
следующей инструкции — вызова функции a.dispO?
Что касается нашей функции nonsenseO, то ей виден только глобальный объект
clock, значит, инструкция a.dispO внутри функции nonsenseO выведет на экран
время 00:00:00, задаваемое конструктором по умолчанию.
А вот внутри функции mai n () объявлен локальный объект mycl ock а A0,59,59), и
получается, что функции mainO доступны два одноименных объекта, причем один
показывает 00:00:00, а второй — 10:59:59. Какое же время появится на экране?
134 Глава 8. Рождение и смерть объектов
Запуск программы показал, что это время — без секунды одиннадцать. То есть
из двух одноименных объектов функция предпочитает локальный. Но это не
значит, что глобальный объект перестал для нее существовать. Чтобы получить
доступ к нему, достаточно поставить перед его именем пару двоеточий, называемую
оператором расширения области видимости. Мы уже встречались с ним при
определении собственных функций вне класса. Чтобы показать, что функция set О
принадлежит классу myclock, нужно поставить перед именем функции префикс
myclock::. Когда же объект глобален, перед двоеточием ставить нечего, поэтому
пишут просто ::a.set(), и это значит, что имеется в виду объект, определенный
вне функции main() и вообще вне любой функции. Теперь нам ясно, что программа,
показанная в листинге 8.4, выведет на экран три значения времени:
00:00:00
10:59:59
00:00:00
В отличие от глобальных локальные объекты, определенные внутри функции,
создаются при каждом ее вызове. Но когда происходит передача управления из
функции, такой объект перестает существовать1, а при следующем вызове
создается вновь. Естественно, конструктор такого объекта2 вызывается при каждом его
создании, то есть при каждом вызове функции.
Правда, локальный объект, объявленный внутри main(), создается только один раз
и один раз (перед завершением программы) уничтожается. Это происходит
потому, что функция main() вызывается, как правило, лишь однажды.
Пространства имен
Объявляя объекты, нужно следить за тем, чтобы у них были разные имена. И это
становится трудно сделать, когда объектов много или когда ими занимаются
разные люди. Кроме того, есть объекты, имена которых менять не хочется, например
функция, вычисляющая косинус, обычно называется cos(), а функцию,
выводящую что-то на экран, разумно назвать printO.
Для всех этих случаев в C++ существуют пространства имен, задаваемые
ключевым словом namespace.
Листинг 8.7. Создание пространств имен
#include <iostream>
namespace xxx {
void showO {
1 Чаще всего такой объект уничтожается автоматически, но иногда нужно создать специальную
функцию — деструктор, подробнее о которой мы поговорим в следующем разделе.
2 Локальные объекты называют еще автоматическими, потому что они автоматически создаются при
каждом вызове функции.
Пространства имен
135
std::cout « "namespace xxx" « std::endl:
}
}
namespace zzz {
void showO {
std::cout « "namespace zzz" « std::endl;
}
}
int mainO {
xxx::show()://namespace xxx
zzz::show();//namespace zzz
}
В программе из листинга 8.7 создаются две функции showO: одна в пространстве
имен ххх, другая — в пространстве zzz. Теперь можно вызвать нужную функцию,
указав оператором :: подходящее пространство имен. Инструкция xxx::show():
вызовет функцию showO из пространства имен ххх, которая выведет на экран
слова «namespace xxx». Что делает инструкция zzz::show(); — догадайтесь сами.
Обратите внимание: перед именами cout и endl стоит признак стандартного
пространства имен std::. Раньше мы предпочитали использовать директиву using
namespace std;, говорящую компилятору, что все имена без явного указания
пространства относятся к std. Естественно, директиву using namespace ... можно
применить к любому пространству имен. Программа, показанная в листинге 8.8,
использует по умолчанию пространство имен ххх, и функция showO, записанная без
«опознавательных знаков», выведет на экран «namespace xxx», потому что
компилятор сообразит, что она относится к пространству имен ххх.
Листинг 8.8. Пространство имен, используемое по умолчанию, может быть любым
#include <iostream>
namespace xxx {
void showO {
std::cout « "namespace xxx" « std::endl:
}
}
using namespace xxx:
int mainO {
showO: //namespace xxx
}
Явное указание пространства имен, используемого по умолчанию, по-разному
воспринимается программистами. Многие считают, что директива using namespace
std; разрушает то, для чего были созданы конструкции namespace{}, потому что
делает видимыми множество стандартных имен. И теперь у программиста есть
реальный шанс выбрать для своего объекта имя, уже используемое стандартной
библиотекой. Чтобы уменьшить вероятность конфликта, можно указывать
директивой using... только имена, реально используемые программой:
using std::cout:
using std::endl:,
136
Глава 8. Рождение и смерть объектов
а самым радикальным решением будет полный отказ от директив using и явное
указание пространства имен перед каждым объектом (листинг 8.9).
Листинг 8.9. Явное указание пространств имен
#include <iostream>
#include <string>
namespace mynames {
std::string message;
}
int mainO {
mynames::message = "Мама!";
std::cout « mynames::message « std::endl:
}
К сожалению, программа при этом становится совершенно нечитаемой, поэтому
в дальнейшем мы вернемся к директиве using namespace std;, но при этом будем
помнить о том, что в «настоящих», больших программах, где вероятней
конфликты имен, нам придется частично или полностью отказаться от директивы using...1.
Static
Кроме локальных и глобальных переменных, о которых мы говорили в разделе
«Видимость», существуют еще статические объекты, которые создаются один раз
(соответственно, и конструктор таких объектов вызывается один раз) и помнят
свое прежнее состояние. Чтобы лучше понять, о чем идет речь, посмотрим на
программу, показанную в листинге 8.10.
Листинг 8.10. Испытание статического объекта
#include <iostream>
using namespace std;
void nonsensO {
int i;
static int j:
cout « "i=" « i « endl;
cout « "j=" « j « endl;
i - 1;
j - 3;
}
int main(){
nonsensO; //i=? j=0
nonsensO; //i=? j=3
}
При первом вызове функции nonsenseO обычная переменная i содержит
«мусор», то есть то, что было в памяти компьютера до того, как ее заняла переменная.
В отличие от i статическая переменная j содержит ноль, то есть все ее биты равны
1 Сразу нужно отказаться от ее использования во включаемых файлах, где описаны ваши собственные
объекты, потому что это нарушит «чужую» политику использования директив using.... Во включаемых
файлах пространства имен нужно указывать явно.
Деструкторы
137
нулю. После выполнения инструкций i = 1; и j = 3; обе переменные получают
определенные значения, но после выхода из функции значение автоматической
переменной i забывается, а значение статической j — нет. И при следующем вызове
функции nonsenseO значение i может быть каким угодно, в том числе 1, а
значение j гарантированно равно 3. Происходит так потому, что статическая
переменная статична — она создается один раз и остается в одном месте компьютерной
памяти до полного завершения программы. Обычные же или автоматические
переменные живут только во время работы функции. Они создаются при каждом ее
вызове и уничтожаются при каждом возврате из функции.
Поскольку статическая переменная всегда занимает одно и то же место в памяти,
она не может иметь разные значения в разных объектах одного класса. Для всех
таких объектов она общая. Значит, создать ее и задать начальные значения (если
это действительно переменная) можно только вне класса. Подробнее об этом
будет рассказано в разделе «Пример: снова часы» главы 10.
А в этом разделе мы научимся задавать с помощью static константы. До сих пор
мы использовали для этого конструкцию enum. Например, в классе Anagram из
листинга 8.3 размер буфера задавался так:
class Anagram {
public:
enum {BSIZE - 80};
Зная, что статические объекты одни и те же для всего класса, можно задать
константу гораздо естественней:
class Anagram {
public:
const static BSIZE - 80:
}':'
Заметим, что слово static здесь обязательно, объявление const BSIZE = 80; внутри
класса компилятор бы не понял, потому что так задаются переменные,
независимые в разных объектах, а какая может быть независимость у констант?
Деструкторы
Я тебя породил, я тебя и убью!
Н. В. Гоголь. Тарас Булъба.
Создавая новые объекты, мы надеялись, что они будут правильно и своевременно
уничтожены. До сих пор эти надежды оправдывались, потому что объекты,
состоящие из стандартных переменных типа int, double, string, а также массивов,
уничтожаются автоматически, например, при выходе из функции.
138
Глава 8. Рождение и смерть объектов
Сделать это легко, потому что такие объекты состоят либо из переменных заранее
известного размера (таких как int, double или массивы), либо из стандартных
объектов (таких как string), уже способных к самоуничтожению.
Но стоит создать более сложный объект, чей размер определяется во время
исполнения программы (то есть динамически), и возникает потребность описать не
только его рождение, но и смерть.
Рассмотрим для примера массив с двумя индексами, где положение объекта
определяется номером строки и номером столбца1. В массиве, объявленном как int
m[3][2], три строки и два столбца. Если учесть, что нумерация в C++ начинается
с нуля, то элементы этого массива будут такими: т[0][0], т[0][1], т[1][0], т[1][1],
т[2][0], т[2][1]. Массивы с двумя индексами называют двухмерными, но
поскольку память компьютера одномерна — это просто ряд идущих друг за другом
байтов, — массив хранится в ней построчно, сначала все элементы нулевой строки
(у нас это т[0][0], т[0][1]), за!ем все элементы первой строки и т. д.
Стандартные двухмерные массивы C++ идеально подходят для хранения
изображений и матриц, но пользоваться ими неудобно, потому что нельзя задавать
размеры массива в процессе выполнения программы; объявление int m[3][2] создает
массив с тремя строками и двумя столбцами — не больше и не меньше. Кроме того,
со стандартными двухмерными массивами трудно работать функциям, ведь
массивы с разным числом столбцов — это разные типы, вот и придется писать одну
функцию для массивов с двумя столбцами, другую — для тех, что с тремя.
Задача 8.3. Прототип функции, принимающей двухмерный массив, может быть
таким: int f(int [][2]). Почему?
Попробуем поэтому создать собственный двухмерный массив, размеры которого
можно задавать в процессе выполнения программы. Постараемся сделать так,
чтобы этот новый массив легко передавался функциям независимо от того, сколько
в нем строк и столбцов.
Если размеры массива заранее не известны, нужно научиться выделять память
динамически, в C++ есть для этого специальный оператор new, с которым мы
познакомились в разделе «Указатели и массивы» главы 4. Следующие инструкции
создают сначала указатель на double, а затем засылают по этому указателю адрес
начала последовательности из двадцати переменных типа double:
double *m;
m = new double [20];
А теперь вернемся к двухмерному массиву, создаваемому работающей
программой «на лету». Нас не должно смущать, что у массива два измерения, просто
выделим оператором new память для одномерного массива и разместим в нем наш
двухмерный массив — строка за строкой. Проект класса matrix, содержащего описание
двухмерного массива, показан в листинге 8.11.
1 См. раздел «Указатель на массив» главы 4.
Деструкторы
139
Листинг 8.11. Определение класса matrix (matrix.hpp)
class matrix {
public:
matrix(int г = 2.int с = 2) {
nrows_ = r:
ncols_ = c;
m_ = new double[r*c];
}
double get(int r.int c) {
return m_[r * ncols_ + c];
}
void put(int r, int c. double d) {
m_[r * ncols_ + c] = d:
}
int rget(){return nrows_;}
int cgetO{return ncols_;}
private:
int nrows_;
int ncols_:
double *m_;
}:
Конструктор класса matrix имеет два параметра — число строк г и число столбцов с.
Значения (int r=2, int c=2), показанные в списке, нужны при создании массива без
явно указанных размеров (например, инструкцией matrix а;).
Указание параметров по умолчанию позволяет иметь один конструктор,
создающий массив либо заданных размеров (если они указаны), либо размером 2x2.
Оператор new выделяет для массива память, вмещающую г*с переменных double.
Двухмерный массив хранится в памяти построчно. Поэтому элемент,
хранящийся в г-й строке и с-м столбце, занимал бы позицию г * ncols + с в одномерном
массиве1.
В листинге 8.12 показана программа, «обкатывающая» вновь созданный объект а
класса matrix (его описание, знакомое нам по листингу 8.11, хранится в файле
matrix.hpp).
Листинг 8.12. Испытание нового класса matrix
#include <iostream>
using namespace std:
#include "matrix.hpp"
const int NOFCOLS = 100:
const int NOFROWS - 200;
void foo(matrix mat) {
cout « "foo: "« mat.getA0.9) « endl;
}
int mainO {
int i.j;
matrix darray(NOFROWS. NOFCOLS);
for(i - 0; i < NOFROWS; i++)
Продолжение &
1 Как обычно, нумерация его элементов начинается с нуля.
140
Глава 8. Рождение и смерть объектов
Листинг 8.12 (продолжение)
for(j - 0; j < N0FC0LS; j++)
darray.put(i. j. i * j):
foo(daгray);
cout « darray.get(NOFROWS D 1. NOFCOLS D 1);
}
В этой программе сначала создается объект darray — двухмерный массив из
200 строк и 100 столбцов, затем каждому элементу присваиваются начальные
значения, равные произведению номера строки на номер столбца. Далее
созданный массив передается функции foo(), выводящей на экран содержимое элемента
A0,9). Наконец, после возврата из функции на экран выводится «крайний»
элемент из последней строки и последнего столбца, равный 99 х 199, то есть 19 701.
Конструктор, описанный в классе matrix (см. листинг 8.11), вызывается в нашей
программе всего один раз — потому что массив darray создается в функции main().
Далее созданный массив передается по значению функции foo(). Нам уже
известно, что функция получает в этом случае копию передаваемого объекта. Эту копию
создает специальный конструктор копирования. Но если он не описан в
соответствующем классе, вызывается конструктор копирования по умолчанию, который
передает функции копии всех элементов объекта. В нашем случае передадутся
копии числа строк nrows, числа столбцов ncol s и копия *т — указателя на область
памяти, где хранятся элементы массива. Эти копии уничтожатся после выхода из
функции, так что программа из листинга 8.12 должна работать нормально.
Но представим себе, что объект типа matrix создан внутри функции foo(). Тогда
при его объявлении создадутся две целочисленные переменные, хранящие число
строк и столбцов, и вызовется конструктор, который выделит с помощью
оператора new память для двухмерного массива. Адрес начала этой памяти будет хранить
указатель т_.
Перед завершением работы функции будут уничтожены все эти три объекта
(число строк, столбцов и указатель на область памяти, занимаемую массивом), но сама
память, выделенная массиву, останется занятой, потому что функция владеет
только указателем на эту память, но не ей самой! Произойдет так называемая утечка
памяти. С каждым вызовом foo() свободной памяти, доступной программе, будет
все меньше, в конце концов она кончится и программа аварийно завершится.
Как же спастись в этом случае? Очевидно, объекту нужен деструктор,
освобождающий память, выделенную внутри функции foo(). Нужно сказать, что деструктор
по умолчанию у объекта есть всегда. Но в нашем случае нужен специально
заданный деструктор, который освободит выделенную для объекта память. Выглядит
он как собственная функция с таким же именем, как у класса, но предваренным
тильдой (-matrixO). Внутри нашего деструктора всего одна инструкция
освобождения памяти (delete [] m_;), на которую указывает т_. Все вместе это выглядит
следующим образом:
class matrix {
public:
Конструктор копирования
141
-matrixO {
delete [] m_;
}
private:
}*'
Деструктор не имеет ни параметров, ни возвращаемого значения и вызывается
автоматически, когда истекает время жизни объекта, например при выходе из
функции. Можно искусственно ограничить жизнь объекта, заключив его в фигурные
скобки. В листинге 8.13 показано, что даже внутри функции можно устроить свой
«мирок», в котором объекты рождаются и исчезают.
Листинг 8.13. Ограничение времени жизни объекта фигурными скобками {}
void foo(){
classl а; //конструктор по умолчанию а
{
class2 b; //конструктор по умолчанию b
class3 с: //конструктор по умолчанию с
//вызывается деструктор с
//вызывается деструктор b
}
//вызывается деструктор а
}
Объекты, созданные последними, уничтожаются первыми. В нашем примере
объекты создаются конструкторами по умолчанию (вспомните, почему), а
деструкторы автоматически вызываются, когда истекает время жизни объектов.
Конструктор копирования
Итак, задача создания и уничтожения объектов решена. Если конструктор и
деструктор соответствуют друг другу, память, временно занятая объектом, будет
возвращена программе деструктором. Но при этом появляется новая трудность:
программа из листинга 8.12 при использовании определения класса с деструктором
аварийно завершается. Какую же недопустимую операцию она выполняет?
Чтобы понять природу катастрофы, нужно еще раз представить себе, как
передаются аргументы функции. Внутри функции foo(matrix m){...} из листинга 8.12
никакие объекты не создаются, функция просто принимает в качестве аргумента
объект типа matrix. Этот объект передается по значению (см. раздел «Параллельные
миры» в главе 4), то есть функция получает не сам объект, а его копию,
создаваемую специальным конструктором копирования. А поскольку в определении
нашего класса такого конструктора нет, вызывается конструктор копирования
по умолчанию, который создает полный аналог нашего аргумента. В объекте,
создаваемом конструктором копирования, — две целочисленные переменные nrows
и ncol s, а также указатель т_, содержащий адрес памяти, где расположен
двухмерный массив.
142
Глава 8. Рождение и смерть объектов
С копией объекта внутри функции foo() можно делать что угодно: изменять
массив или выводить на экран значения отдельных элементов, но раз объект (копия
аргумента) создан, перед выходом из функции должен вызываться деструктор,
который мы уже написали — себе на погибель. Ведь в деструкторе есть
инструкция delete [] m_, значит, он освободит память, занимаемую нашим объектом, ведь
именно на нее указывает m_! To есть при выходе из функции двухмерный массив
перестанет существовать, и попытки записать что-то в область памяти, которой
программа уже не обладает, приводят к ее аварийному завершению.
Что же делать? Очевидно, деструктор трогать нельзя, ведь он помогает уничтожать
объекты, явнс^ созданные внутри функции. Значит, придется самим написать
настоящий конструктор копирования, который выделяет память для копии объекта
и переписывает туда все значения массива. Тогда при выходе из функции снова
будет вызван деструктор, но теперь он уничтожит не исходный объект, а лишь его
копию, занимающую совершенно другую область памяти.
Написать конструктор копирования довольно легко, если догадаться, что объект,
копия которого должна быть создана, передается по ссылке. Действительно, по
значению его передать никак нельзя, ведь тогда для создания копии объекта, с
которой будет работать конструктор копирования, придется вызывать конструктор
копирования — получится бесконечный вызов функции без всякой надежды
вернуться в основную программу. А при передаче по ссылке копия объекта не
создается, что и требуется.
Итак, конструктор копирования для класса matrix может выглядеть так:
matrix(matrix &mc) {
nrows_ = mc.rgetO:
ncols_ = mc.cgetO;
m_ = new double[nrows_ * ncolsjl;
forCint i = 0; i < nrows_; i++)
for(int j = 0; j < ncols_; j++)
put(i. j. mc. get(i.j)):
}
В этом конструкторе сначала выясняются размеры копируемого объекта, затем
оператор new выделяет новую область памяти, куда и переписываются элементы
исходного двухмерного массива тс.
Снабдив объект обычным конструктором, конструктором копирования, а
также деструктором, мы решаем основные проблемы его использования и передачи
функциям. Правда, если объект не захватывает память в процессе выполнения
программы, то можно задействовать конструктор для задания его начального
состояния, доверив компилятору вызовы деструктора и конструктора копирования,
когда это требуется.
Но во всем полагаться на компилятор нельзя, потому что не всегда можно
правильно понять его логику. Например, следующая инструкция создает указатель р
на объект типа matrix:
matrix *p = new matrixdOO.200);
Конструктор копирования
143
При этом вызывается конструктор класса matrix, выделяющий память для
двухмерного массива. Но автоматического вызова деструктора в этом случае не
происходит, потому что компилятор считает, что создает всего лишь указатель, а не
двухмерный массив. Значит, при выходе из функции или там, где объект,
созданный оператором new, должен исчезнуть, необходимо записать инструкцию:
delete p;
Тогда память, занимаемая двухмерным массивом, освободится.
До сих пор конструктор копирования работал у нас внутри функции, где он
вызывался неявно и создавал копию объекта, передаваемого по значению. Так же
неявно конструктор копирования вызывается при возврате функцией значения
в основную программу. Кроме того, конструктор копирования используется,
когда при объявлении объекта ему присваивается начальное значение типа matrix:
b = a;
Примеры использования конструктора копирования демонстрирует программа,
показанная в листинге 8.14.
Листинг 8.14. Конструктор копирования в действии
#include <iostream>
#include "matrixl.hpp"
#define NOFCOLS 100
#define NOFROWS 200
using namespace std;
int mainO {
int i. j:
matrix a(N0FR0WS. NOFCOLS); //constr a
cout « "" « endl;
for(i - 0: i < NOFROWS; i++)
for(j = 0: j < NOFCOLS; j++)
a.put(i. j, i * j);
matrix b = a://copy constr
matrix c(b); //copy constr
cout « c.getA55. 2) « endl; //310
//destr с
//destr b
//destr a
}
В файле matrixl.hpp находится определение класса matrix с конструктором,
деструктором и конструктором копирования. Обычный конструктор вызывается
в программе один раз — при создании объекта а. Далее вызывается конструктор
копирования при создании объекта b — (matrix b = а), а также при создании
объекта с— (matrix c(b)).
Наша программа создает три объекта, которые занимают разные области памяти,
но содержат одинаковые двухмерные массивы. После вывода на экран элемента
A55,2) массива с программа должна уничтожить все объекты, причем объект,
созданный последним, погибает первым: сначала вызывается деструктор массива с,
затем b и, наконец, а.
144
Глава 8. Рождение и смерть объектов
Прочитав этот раздел, читатель, возможно, окажется в замешательстве, так и не
поняв, когда вызываются конструкторы, когда деструкторы, а когда этого не
происходит. Чтобы достичь полной ясности, можно поставить ряд экспериментов
с компилятором и программой. В конструктор можно добавить инструкцию
вывода на экран:
matrix(int г. int c){
nrows = г;
ncols = с;
m = new double[r*c];
cout « "constr" « endl;
}
Пусть деструктор выводит строку «destr», а конструктор копирования — строку
«сору». Тогда, следя за тем, что программа показывает на экране, можно понять,
когда и что вызывается. Для большей ясности создадим дополнительные
инструкции вывода и в основной программе:
cout « " « endl;
matrix b - a; // copy constr
cout « " « endl;
matrix c(b); // copy constr
cout « " « endl;
Программисты часто ставят подобные эксперименты, чтобы лучше понять, как
работает компилятор. Конечно, существует стандарт C++, где поведение
компилятора точно описано, но собственные эксперименты гораздо быстрее учат языку,
чем самые толстые руководства. Надо, правда, знать, что стандарт некоторые вещи
оставляет на усмотрение компилятора. Нельзя, например, выяснять, каким будет
результат инструкции i = i++, потому что другой компилятор может выдать все
что угодно (см. об этом раздел «Унарные и бинарные операторы» в главе 2), здесь
эксперименты бесполезны.
Глава 9
Операторные функции
Доступ к массиву
У двухмерного массива matrix, который мы начали строить в предыдущей главе,
есть пока множество недостатков. Один из самых заметных — неуклюжие
собственные функции put О и get(), предназначенные для работы с отдельными
элементами массива. Вместо записи m=a.getB,2) хотелось бы видеть т=а[2][2] или
го=аB,2), а вместо a.putB,2,10.0) — более привычное а[2][2]=10.0 или аB,2)=10.0.
Как вы уже догадались, это возможно за счет специальных собственных
функций объекта, называемых операторными. Так эти функции называются не только
потому, что они содержат слово «operator» в своем имени, но и потому, что
позволяют придать новый смысл обычному оператору, такому как +, = или «. Ведь
операции сложения, присваивания или вывода на экран для разных типов
объектов разные.
Попробуем создать оператор О для класса matrix. Соответствующая операторная
функция принимает два параметра — номер строки и номер столбца — и
возвращает значение типа doubl e:
class matrix{
double operatorOdnt r. int c){
return mini_[r * ncols_ + cj:
}
Поскольку операторная функция — это собственная функция объекта (ее
название operatorO), вывести на экран значение элемента массива можно, записав:
cout « a.operator()B,2)« endl;
Но для собственных функций, начинающихся словом «operator», язык C++
предусматривает удобную сокращенную запись, позволяющую убрать точку и само
146
Глава 9. Операторные функции
это слово. В нашем случае получится запись, естественная для двухмерного
массива:
matrix a;
cout. « аB.2) « endl;
Увидев скобки () справа от объекта а типа matrix, компилятор станет искать в
определении класса операторную функцию operator()(int r.int с)и, найдя ее,
заменит скобки вызовом функции, которой передаст стоящие в скобках аргументы.
Так что оператор () только украшает исходный текст программы, а по сути это
лишь иначе записанный вызов собственной функции объекта.
Но даже не содержа ничего принципиально нового, операторные функции
оказываются очень полезны, потому что делают программу короче и понятней. И надо
стремиться к тому, чтобы операторы, примененные к нашим собственным
объектам, смотрелись так же естественно, как операторы языка +, -, [], примененные
к «родным» для C++ объектам, таким как целые числа и двухмерным массивы.
К сожалению, операторе), определенный чуть выше, не таков, потому что его
нельзя использовать для изменения значений элементов массива. Инструкция
аB.2)=10.0 вызовет сообщение компилятора об ошибке, что-то вроде «non-lvalue
in assignment» (присваиваем не «левому» значению). Все дело в том, что заданная
нами функция возвращает значение переменной типа double, а не саму
переменную. А конкретное значение не может стоять слева от оператора присваивания.
Разве можно записать 2=10.0?
Возвращаемое нашей операторной функцией значение — типично «правое»
(rvalue), оно может стоять только справа от оператора присваивания. Чтобы стать
«левым», оно должно превратиться из значения элемента массива в ссылку на него.
Ведь ссылка — и есть сам элемент; а все, что происходит со ссылкой, происходит
и с самим элементом. Поэтому нужно, чтобы операторная функция возвращала
ссылку, а не значение. Новое определение операторной функции должно
выглядеть так1:
matrix::double & operator()(int r.int с) {
return mini_[r * ncols_ + c];
}
Оператор & командует операторной функции вернуть ссылку на элемент
массива m[r*ncols+c], а ссылка может стоять как справа от оператора присваивания, так
и слева. Программа, показанная в листинге 9.1, демонстрирует возможности
оператора (), приспособленного для доступа к элементу двухмерного массива matrix.
Листинг 9.1. Новая роль оператора ()
#include <iostream>
#include "matrix2.hpp"
using namespace std:
1 Напомню, что собственные и операторные функции можно определять вне класса, но в этом случае
их имена предваряются именем класса (в нашем случае — matrix) и оператором расширения области
видимости (::).
Доступ к массиву
147
const int NOFCOLS - 100;
const int NOFROWS - 200;
int mainO {
int i.j:
matrix a(NOFROWS.NOFCOLS);
aB.2) - 10;
double &m - aB.2);
cout « m « end!; //10
m = 5;
cout « aB.2) « endl; //5
}
В файле matrix2.hpp хранится определение класса matrix с оператором О,
возвращающим ссылку на элемент массива. В тексте программы элемент B,2)
сначала становится равным десяти, затем создается ссылка m на элемент массива B,2).
Далее на экран выводится 10 — значение этой ссылки. Наконец, инструкция т=5
записывает в элемент массива B,2) число 5, которое и выводится на экран в
последней строке:
cout « аB.2) « endl;
Созданный нами оператор доступа к элементу двухмерного массива делает
ненужными функции get О и put(), теперь их можно убрать из определения класса
matrix. Получившийся класс показан в листинге 9.2.
Листинг 9.2. Новая редакция класса matrix matrix2.hpp
class matrix {
public:
matrix(int г = 2.int с = 2){
nrows_ - r:
ncols_ = c:
mini_ = new double[r * c]:
}
matrix(matrix &mc){
nrows_=mc.rget();
ncols_=mc.cget();
mini_*new double[nrows_*ncols_];
forCint i=0;i<nrows_;i++)
for(int j=0;j<ncols_;j++)
mini_[i*ncols_+j]=mc(i j);
double & operator()(int r. int c){
return mini_[r * ncols_ + c]:
-matrixO {
delete [] mini_;
int rgetO {
return nrows_;
int cgetO {
return ncols_;
private:
int nrows :
Продолжение &
148
Глава 9. Операторные функции
Листинг 9.2 (продолжение)
int ncols_;
double *mini_;
}:
Обратите внимание: насколько проще стал конструктор копирования.
Инструкцию put (i, j, mc. get (i. j)) теперь заменяет гораздо более понятная запись:
mini_[i * ncols + j] - mc(i.j):
Равенство
Работа над классом matrix, начатая в прошлой главе, сопровождалась цепью
катастроф. Создав конструктор, мы получили утечку памяти, устраненную
деструктором. Но с появлением деструктора объект уничтожался при передаче его
функции. Бороться с этим приходилось путем создания специального конструктора
копирования, выделявшего для копии объекта, с которой работает функция,
отдельную область памяти.
Оказывается, наши злоключения еще не кончились. Представим себе, что объект
типа matrix, снабженный конструктором, деструктором и конструктором
копирования, передается функции fooO, показанной в листинге 9.3.
Листинг 9.3. Коварство оператора присваивания
#include <iostream>
#include "matrix2.hpp"
using namespace std:
const int NOFCOLS - 100;
const int NOFROWS = 200:
void foo(matrix &m) {
matrix tmp;
tmp - m;
cout « tmpB.10)« endl;
}
int mainO {
int i.j;
matrix a(NOFROWS. NOFCOLS);
for(i » 0; i < NOFROWS; i++)
for(j - 0; j < NOFCOLS; j++)
a(i.j) - i * j:
foo(a);
cout « aB.10) « endl;
}
Внутри функции fooO объявляется объект tmp типа matrix. Поскольку число строк
и столбцов при этом не указывается, конструктор создает массив размером 2x2.
Затем объекту tmp приравнивается массив а (функции передается ссылка на а).
И хотя размеры обоих массивов не совпадают (tmp имеет размеры 2x2, в то время
как а — 200 х 100), ничего страшного не происходит, потому что оператор
присваивания по умолчанию копирует объекты бит за битом. После присваивания
переменная nrows_ объекта tmp будет равна 200, переменная ncols_ — 100, a mini_ будет
Указатель This
149
указывать на область памяти, где хранятся переменные массива а. То есть tmp
становится после присваивания точной копией массива а. Вот в этом как раз и таится
страшная опасность, потому что деструктор уничтожит объект tmp перед выходом
из функции, а вместе с ним освободит и память, на которую указывает mini_. Но
память, используемая массивами tmp и а, — общая; погибая, объект tmp тянет за собой
и своего сиамского брата-близнеца, и теперь попытки использования массива а
в основной программе приведут, скорее всего, к ее аварийному завершению.
Чтобы устранить этот недостаток, нужно, очевидно, так определить оператор
присваивания, чтобы для копии объекта выделялась другая область памяти. Тогда
деструктор уничтожит копию, но не тронет основной объект, передаваемый функции.
Оператор присваивания имеет много общего с конструктором копирования1
и первый вариант операторной функции для него может выглядеть так, как
показано в листинге 9.4.
Листинг 9.4. Первый вариант оператора =
void matrix::operator=(matrix &mc) {
delete [] mini_;
nrows_ = mc.rgetO;
ncols_ = mc.cgetO;
mini_ - new double [nrows_ * ncols_];
for(int i = 0; i < nrows_; i++)
for(int j = 0: j < ncols_; j++)
mini_[i * ncols_ + j] = mc(i.j):
}
Как видим, сначала освобождается память, занимаемая объектом, затем оператор
узнает размеры копируемого массива, выделяет столько памяти, чтобы он смог
там поместиться, и копирует элементы массива — строка за строкой.
Задача 9.1. Всегда ли можно в операторе присваивания уничтожать память,
занимаемую массивом?
Указатель This
— Ладно. Пусть лучший вариант будет. Представь,
что он из своей машины вылез, подошел к твоей,
и только ты шмалять собрался, глядь... — Володин
выдержал значительную паузу. — Глядь, а это не
он, а ты сам и есть. А тебе шмалять надо. Скажи,
поедет от такого крыша?
Виктор Пелевин. Чапаев и Пустота
Казалось бы, оператор присваивания, операторная функция для которого
показана в листинге 9.4, делает все, что от него требуется. Но давайте представим, что
1 Но это не одно и то же: конструктор копирования используется при создании объекта, а оператор
присваивания приравнивает существующий объект другому.
150
Глава 9. Операторные функции
в программе объект а копируется сам в себя (а=а;). Хоть смысла в этом немного, но
такая операция разрешена, а значит, должна выполняться. Но рассматриваемый
оператор присваивания сначала освобождает память объекта, а потом выделяет
для него другую ее область. Это значит, что в случае присваивания а=а
копирование элементов массива а будет идти из уже освобожденной области памяти. А язык
C++ устроен так, что освобожденная память может в любой момент быть занята
чем-то другим. Получается, что копирование идет из того места, где объекта уже
нет: присваивание оборачивается уничтожением.
Ясно, что избежать уничтожения при «самоприсваивании» можно, лишь имея
возможность отличать себя от других. Для этого каждый объект располагает
специальным указателем «на себя» — this (в переводе с английского «это»). Указатель
this не объявляется, но все происходит так, как будто он незримо присутствует
в классе. Например, в классе matrix наряду с размерами массива и указателем на
область памяти как бы присутствует еще и указатель this:
class matrix {
public:
private:
matrix *this; //невидимый указатель на
//объект
int nrows_:
int ncols_:
double *mini_;
} :
Этот указатель this, в отличие от nrows_ и ncols_, возникает автоматически при
создании объекта и хранит его адрес. Поэтому this можно использовать, чтобы
отличить «свой» объект от «чужого» в новом варианте оператора присваивания
(листинг 9.5).
Листинг 9.5. Проверка на самоприсваивание в операторе =
void matrix::operator=(matrix &mc) {
ifCthis != &mc) {
delete [] mini_;
nrows_ = mc.rgetO;
ncols_ = mc.cgetO;
mini_=new double [nrows_ * ncols_]:
for(int i = 0;i < nrows_: i++)
for(int j = 0: j < ncols_: j++)
mini_[i * ncols_ + j] = mc(i.j):
}
}
Смысл условия if (this != &mc) прост: если указатель на текущий объект this равен
адресу копируемого объекта, то «это ты сам и есть», и тогда вообще ничего делать не
надо — остается только покинуть операторную функцию оператора присваивания1.
1 Условие (this !e &m) может быть неправильно понято. В нем this — указатель на объект, &тс — адрес
объекта, поскольку & — это оператор получения адреса, а вовсе не ссылка.
Указатель This
151
Указатель this в операторной функции из листинга 9.5 позволяет избежать
самоприсваивания. Но и ему не дано сделать оператор присваивания идеальным.
Представим себе, что в программе появилась цепочка операций присваивания:
matrix'a.b.c;
с = b = a;
С точки зрения языка C++ такая запись правильна и в соответствии со свойствами
оператора присваивания выполняется справа налево (см. приложение А): сначала
объект b становится равным а, а затем и с становится равным Ь. Если C++
оперирует стандартными объектами, такими как целые числа или строки (тип string),
присваивание происходит правильно и в нужном порядке.
Но оператор присваивания, операторная функция для которого представлена
в листинге 9.5, ожидает ссылку на объект, но сам ничего не возвращает. Поэтому
цепочка операций присваивания, выполняемая с его помощью, прервется уже на
первом шаге: при выполнении инструкции Ь=а операторная функция ничего не
возвращает, а значит, объект с не сможет стать равным Ь. Совершенно ясно, что
цепочка операций присваивания возможна, лишь когда оператор присваивания
возвращает и принимает объекты одного типа. Если наш оператор принимает ссылку
на объект, то и возвращать он должен тоже ссылку, примерно так, как в листинге
9.6.
Листинг 9.6. Оператор =, работающий в «цепочке»
matrix & operator=(matrix & mc){
if(this !- &mc){
delete [] m; •
nrows = mc.rgetO;
ncols - mc.cgetO;
m = new double [nrows * ncols]:
forCint i = 0; i < nrows;i++)
for(int j = 0; j < ncols; j++)
m[i * ncols + j] = mc(i.j):
}
return *this;
}
Посмотрим, как выполняется с новым оператором цепочка операций
присваивания с=Ь=а. Сначала вызывается оператор присваивания объекта Ь, то есть this
указывает в этом случае именно на Ь. После присваивания оператор возвращает
ссылку на b — (return *this) оператору присваивания с. Оператор, как и положено,
принимает ссылку, копирует данные из объекта b в объект с и снова возвращает
ссылку — на этот раз на объект с и, поскольку дальнейших операций
присваивания нет, эта ссылка исчезает в пустоте1.
1 Заметим, что возврат ссылок, так же как их передача, выглядят в языке C++ довольно странно. Ведь
return (*this) означает возврат объекта, а не ссылки на него (this — указатель, а оператор * превращает
указатель в сам объект). Но поскольку указано, что функция возвращает ссылку, то объект
испытывает еще одно невидимое превращение.
152
Глава 9. Операторные функции
Раз уж речь зашла об указателе this, стоит сказать несколько слов о внутреннем
устройстве объектов. Оказывается, собственные функции и данные хранятся
в памяти компьютера отдельно. Объектов, принадлежащих одному классу, может
быть много, а набор собственных функций у них один. Собственной функции
передается при вызове указатель this, с помощью которого она получает доступ к
данным именно этого объекта. Даже функции, не имеющей явных параметров,
тайно передается указатель this, и, например, функция cget класса matrix (см.
листинг 9.2) на самом деле устроена так:
int matrix::cget(matrix *this){
return this->ncols;
}
Указатель this чаще всего остается незамеченным, но иногда, как, например, в
операторе присваивания, его приходится использовать явно.
Родственники и друзья
Как бы мне не обменяться личностью: он
войдет в меня, а я в него, — я охвачен
полной безразличностью и боюсь решительно
всего...
Саша Черный. Школа дураков
До сих пор мы считали операторные функции языка C++ просто иначе
названными собственными функциями. Но это не совсем так.
Операторные функции в C++, независимо от того, для какого класса определены,
сохраняют неизменными приоритет (оператор умножения * всегда выполняется
раньше, чем оператор сложения +) и порядок выполнения (оператор +
выполняется слева направо, независимо от того, какие объекты «складываются»1)
операторов. В отличие от собственных функций допустимое число параметров
различных операторных функций определяется природой реализуемых ими операторов.
Например, операторная функция для оператора вызова функции О, который мы
использовали в разделе «Доступ к массиву», может, как и собственная функция,
иметь сколько угодно параметров, а операторная функция для унарного
(действующего на один-объект) оператора вовсе не должна иметь параметров, потому что
может найти объект по указателю thi s. А вот операторной функции для бинарного
оператора достаточно передать один объект, потому что второй (тот, на который
указывает thi s) всегда «под рукой».
1 Операцию сложения можно определить самым изуверским способом. Например, можно под видом
сложения вычитать или умножать числа. Но разумнее использовать одноименные операторы для
схожих операций. Например, создать оператор + для сложения комплексных чисел, ведь запись а+b
выглядит для них вполне естественно. Если же не удается подобрать подходящий по смыслу оператор,
лучше использовать собственную функцию.
Родственники и друзья
153
Чтобы прояснить сказанное, попробуем определить операторную функцию для
сложения комплексных чисел — объектов, представляющих собой
упорядоченную пару обычных чисел, где первое число называют реальной частью, а
второе — мнимой. При сложении комплексных чисел отдельно суммируются их
реальные и мнимые части. В результате сложения получается новое комплексное
число.
Чтобы проверить новый оператор, нам придется создать класс complex, в котором
реализовать конструкторы и некоторые собственные функции, без которых не
обойтись при выводе результатов на экран. «Остов» нового класса показан в
листинге 9.7.
Листинг 9.7. Набросок класса complex (файл complexl.hpp)
class complex {
public:
complex(double г. double i) : re_(r). im_(i) {}
complex(double r) : re_(r), im_@) {}
complexO : re_@). im_@) {}
complex operator*(complex a) {
complex tmp;
tmp.re_ - re_ + a.re_;
tmp.im_ = im_ + a.im_;
return tmp;
}
double getre(){return re_:}
double getim(){return im_;}
private:
double re_:
double im_;
}:
Прежде всего обратим внимание на необычный вид конструкторов (всего их три).
Как и раньше, в круглых скобках показаны параметры, а после двоеточия
перечислены данные и (в скобках) параметры, на эти данные влияющие. Следующая
запись означает, что реальная часть комплексного числа ге станет после вызова
конструктора равной г, а мнимая im — параметру i:
complex(double r. double i) : ге_(г). im_(i) {}
Такая запись часто встречается в программах на C++ и ее нужно знать, хотя
«старый» способ записи конструктора как обычной функции без возвращаемого
значения ничуть не хуже:
complex(double r. double i) {
re_~r:
im_=i;
}
Всего в нашем классе complex три конструктора: первый устанавливает реальную
и мнимую части комплексного числа, второй — только реальную, а мнимую
полагает по умолчанию равной нулю, третий конструктор совсем не имеет параметров
и устанавливает в нуль обе части комплексного числа.
154
Глава 9. Операторные функции
Разобравшись с конструкторами, обратимся к операторной функции для
сложения. В ней создается временный объект tmp, причем при его создании
вызывается конструктор по умолчанию, «обнуляющий» реальную и мнимую части. Далее
к реальной части tmp прибавляется реальная часть «текущего» объекта ге_ и
реальная часть объекта а. ге_, передаваемого в качестве аргумента операторной
функции. Мнимая часть получается аналогично, и затем весь объект tmp возвращается
во внешний мир.
Казалось бы, все понятно и просто. На самом же деле оператор + для класса
complex разрушает все наши представления о доступе к секретным данным
объектов. Раньше мы думали, что к области private имеют доступ только собственные
функции объекта, которому и принадлежит эта область. Но взгляните еще раз на
операторную функцию для оператора сложения:
complex operator+(complex a){
complex tmp;
tmp.re_ = re_ + a.re_;
tmp.im_ = im_ + a.i.m_;
return tmp;
}
В этой функции ге_ и im_ — действительная и мнимая части текущего объекта, на
который указывает this. А вот tmp.re_ и а.ге — действительные части чужих
объектов! Причем доступ к ним идет самым непосредственным образом, без всяких
собственных функций!
Разгадка в том, что C++ считает все объекты одного класса близкими
родственниками, разрешая любому такому объекту иметь доступ ко всем закрытым частям других
родственных объектов. Вот почему наш оператор работает! Но если бы мы посмели
написать а.ге_ внутри функции mainO или в любой функции любого объекта, не
принадлежащего классу matrix, компилятор выдал бы сообщение об ошибке.
Задача 9.2. Перепишите конструктор копирования из раздела «Конструктор
копирования» главы 8 с учетом того, что объектам одного класса разрешен доступ
к закрытым частям друг друга. Где еще мы использовали собственные функции
объекта вместо непосредственного доступа к нему?
Поняв родственные отношения объектов одного класса, вернемся к оператору
сложения и проверим его «в деле». Программа, показанная в листинге 9.8, использует
включаемый файл complex.hpp, в котором содержится определение класса complex
из листинга 9.7.
Листинг 9.8. Испытание оператора сложения
#include <iostream>
#include "complex.hpp"
using namespace std;
void out(complex a) {
cout« a.getre()«\" « a.getimO « endl;
}
int mainO {
complex a.bC.2).cE.4).d:
c=b + 20;
// c=b.operator+B0):
Родственники и друзья
155
out(c); // 23.2
d = а + b + с;
// d=(a.operator+(b)).operator+(c);
out(d): // 26.4
}
В этой программе результаты сложения выводит вд экран, функция out().
Поскольку она не принадлежит классу complex, в ней непосредственный доступ к
области private объектов этого класса невозможен. Потому и приходится
использовать собственные функции getreO и getimO. В программе показаны операции
сложения с использованием операторов в привычной записи и (в комментариях)
в виде операторных функций. Заметим, что оператор сложения независимо от
определения всегда выполняется слева направо, то есть к объекту а прибавляется b
и к новому объекту прибавляется с.
Любопытно проследить за тем, как прибавляется число 20 к объекту Ь. Поскольку
оператор — это всего лишь иначе записанная операторная функция, следует
ожидать, что для передачи по значению будет использован конструктор копирования.
Но так было бы при передаче объекта типа complex. У нас же передается целое
число. Поэтому вызывается один из конструкторов, который преобразует целое
число 20 в комплексное B0.0,0.0) и передает его оператору.
Кстати, а что будет с нашей программой, если ей предложат не к b прибавить 20
(с=Ь+20), а к 20 прибавить b — (c=20+b)? Очевидно, ничего хорошего, ведь запись
20.operator+(b) — это полная бессмыслица, и компилятор наверняка откажется
работать, выдав на экран что-то вроде (нет подходящего оператора):
no match for 'int + complex &'
Как же поступить в этом случае? Ведь новый оператор должен во всем
напоминать привычный оператор +, «равнодушный» к порядку слагаемых. Очевидно,
операторная функция для оператора сложения должна принимать два
аргумента, но как раз это и запрещено функции класса. Единственный выход в данном
случае — создать операторную функцию с двумя параметрами вне класса. Увидев
оператор +, соединяющий два объекта типа compl ex, компилятор попробует найти
определение оператора + внутри класса compl ex. He найдя его там, он станет искать
определение вне класса. Поскольку компилятор знает, что + — бинарный оператор,
соединяющий два объекта, он ожидает увидеть два параметра у соответствующей
операторной функции. Найдя такую функцию, компилятор преобразует al + a2
в operator+(al,a2). Сделает он это только в том случае, когда параметров два. Если
их будет, скажем, три, то появится сообщение об ошибке.
Но тут возникает другая трудность. Операторная функция, определенная вне
класса, перестает быть собственной функцией, и ей уже недоступны закрытые
области объектов. В нашем случае придется написать функции setreO и setimO,
чтобы извне менять реальную и мнимую части числа. С этими функциями наша
операторная функция будет выглядеть следующим образом:
complex operator+Ccomplex al. complex a2) {
complex tmp;
156
Глава 9. Операторные функции
tmp.setre(al.getre() + a2.getre()):
tmp.setim(al.getim() + a2.getim());
return tmp:
}
Выглядит это, согласитесь, не очень красиво. Поэтому в языке C++
предусмотрена возможность разрешать некоторым функциям доступ к закрытым областям
объекта. Такие функции называются дружественными, их прототипы
помещаются внутрь класса и предваряются словом «friend» (друг).
Класс complex, использующий дружественную операторную функцию, вместе с
самим оператором, определенным вне класса, показан в листинге 9.9.
Листинг 9.9. Определение оператора + с помощью
дружественной функции (файл complexl.hpp)
class complex {
public:
complex(double r. double i) : re_(r). im_(i) {}
complex(double r) : re_(r). im_@) {}
complexO : re_@). im_@) {}
friend complex operator+(complex al. complex a2);
double getre(){return re_;}
double getimO{return im_;}
private:
double re_;
double im_:
}:
complex operator+(complex al. complex a2) {
complex tmp:
tmp.re_ = al.re_+a2.re_:
tmp.im_ = al.im_+a2.im_;
return tmp:
}
Программа, использующая для сложения комплексных чисел оператор,
реализованный с помощью дружественной операторной функции, показана в
листинге 9.10.
Листинг 9.10. Испытание оператора +, заданного дружественной функцией
#include <iostream>
using namespace std:
#include "complexl.hpp"
void out(complex p) {
cout « p.getreO « "." « p.getimO « endl « endl:
}
int mainO {
complex a. bC.2). cE.4). d:
out(a): //a-(O.O)
с - b + 20; //C-C23.2)
out(c);
d=a + b + с; //(Н26.4)
out(d):
a - 20 + c: //a-D3.2)
out(a):
}
Ввод-вывод
157
Только что мы использовали дружественную операторную функцию для
построения оператора сложения. Но хорошо ли это? Вообще говоря, не очень. Разрешая
доступ извне, объект подвергает себя опасности, потому что не может полностью
контролировать свои внутренние переменные. У объекта появляется как бы
второй пульт управления, а кто и когда начнет нажимать на нем кнопки —
неизвестно. Дружественные функции разрушают представление об объектах, как о чем-то
цельном, защищенном от внешних воздействий, и пользоваться ими нужно только
в крайнем случае. Однако операторы, реализованные с помощью дружественных
операторных функций, действительно очень близки объекту, поэтому в данном
случае использование дружественных функций оправдано.
Но вряд ли можно оправдать более свободный доступ к объекту с помощью
произвольных дружественных функций (не операторных) особенно со стороны другого
класса, объявленного как friend:
class A {
friend class В;
}:
При таком объявлении В — это дружественный А класс1, то есть собственные
функции В имеют доступ к внутренним структурам класса А, что не только опасно, но
и неудобно. Ведь изменение внутренних структур класса А с большой
вероятностью скажется на его дружественном классе В, имеющем доступ к этим
структурам. Иными словами, нарушается важнейший принцип надежного
программирования: последствия изменений в программе должны быть как можно локальней:
перемены внутренних структур класса должны (в идеале) сказываться только на
устройстве его собственных функций. Но чем больше дружественных классов
и функций в программе, тем больше она становится похожей на кошмарное
минное поле, где разминирование в одном месте приводит к появлению нескольких
новых мин, причем в самых неожиданных местах.
Ввод-вывод
До сих пор нам казалось, что объект cout и оператор « волшебным образом знают,
как выводить на экран объекты разных типов. Но дело, конечно, не в волшебстве,
а в том, что все эти объекты (строки, переменные int и double) стандартны, стоит
попытаться вывести на экран объект класса complex, о котором оператору « ничего
не известно, и компилятор сообщит:
'operator«' not implemented in type 'ostream' for arguments of type 'complex' in function
mainO
(в классе ostream не определен оператор « для аргументов типа complex)
В этом сообщении есть важная информация. До сих пор мы не знали, к какому
классу относится объект cout. Компилятор подсказывает нам: это класс ostream,
1 Заметим, что дружба в C++ не взаимна. Если класс В объявлен дружественным для А классом,
то А не становится от этого другом В. Для этого необходимо специальное объявление в классе В: friend
class A.
158
Глава 9. Операторные функции
и теперь у нас есть все необходимое, чтобы построить операторную функцию для
вывода оператором « комплексных чисел.
Затруднение только в том, что этот оператор нельзя определить в созданном нами
классе complex. Ведь собственные функции объекта (а операторные функции,
определенные в классе, — такие же собственные) «пристегиваются» к самому
объекту, а значит, вывод переменной должен быть в этом случае записан так:
complex a;
а « cout;
Только при такой записи компилятор вызовет операторную функцию a.opera-
tor«(), которая сможет вывести на экран комплексное число. Но в C++ принята
другая запись cout « а, что приводит к вызову одной из операторной функции (а
именно cout.operator«0) класса ostream. В этом классе не предусмотрено
операторной функции для вывода переменных нашего класса complex, что и вызывает
сообщение об ошибке. Значит, вывод на экран стандартным для C++ способом
(cout «а;) можно организовать двояко: дописать в класс ostream операторную
функцию для оператора вывода комплексных чисел, или же использовать
внешнюю функцию с двумя параметрами: первым должна быть ссылка на объект
cout1, вторым — объект класса complex. Дописывать класс ostream мы, конечно, не
будем. А вот написать собственную операторную функцию сможем. Если
объявить ее дружественной классу complex, то выглядеть она будет так, как показано
в листинге 9.11.
Листинг 9.11. Определение оператора << в классе complex (файл complex3.hpp)
class complex {
friend ostream& operator«(ostream & stream, complex a);
public:
private:
}:'
ostream& operator«(ostream & stream, complex a) {
stream « a.re_ « "." « a.im_ « endl « end! :
return stream:
}
Встретив в исходном тексте программы инструкции
complex a:
cout « a:
компилятор вызовет операторную функцию, показанную в листинге 9.11, и
передаст ей два аргумента: ссылку на объект cout (объект класса ostream) и объект2 а
1 Если передать объект, а не ссылку, то компилятор попытается создать внутри операторной функции
копию cout. Но нам нужен сам объект, ведь cout, как и экран монитора, у нас один. Кстати, создать
копию не получится, потому что в классе ostream нет конструктора копирования.
2 Если конструктор копирования объекта выполняется медленно, лучше передать ссылку. Объект
класса comlex «маленький» и его можно передать по значению.
Функции-объекты
159
класса complex. Внутри операторной функции реализуются действия, характерные
для оператора « класса ostream: вывод чисел типа double и последовательностей
символов.
Возвращает наша операторная функция ссылку на объект класса ostream — с тем,
чтобы обеспечить вывод объектов по цепочке. Представим, что на экран
выводится несколько объектов:
cout « А « В « С:
Если учесть, что оператор независимо от переопределений всегда выполняется
слева направо, то сначала выполнится часть cout« А. Если объект А известен
классу ostream, вызывается операторная функция cout.operator«(A), которая
возвращает ссылку, то есть другое имя объекта cout. Так что можно сказать, что помеле
вывода А цепочка укоротится:
cout « В «С:
Если объект В не известен классу ostream (потому, например, что это объект
созданного нами типа complex), то вызывается внешняя операторная функция, такая
как в листинге 9.11. Но поскольку она принимает и возвращает ссылку на ostream,
порядок вывода не нарушится: в зависимости от типа объектов будут вызываться
либо операторные функции класса ostream, либо внешние операторные функции,
подобные той, что показана в листинге 9.11. И так будет до тех пор, пока не
исчерпается список выводимых объектов.
Задача 9.3. Переопределите оператор « для класса myclock, описанного в разделе
«Богатые и бедные классы» главы 8.
Фу н кци и -объекты
Оператор О, приспособленный нами для доступа к элементам матрицы (см.
раздел «Доступ к массиву»), можно использовать и по прямому назначению — для
создания объекта, ведущего себя как функция. Такие объекты, часто называемые
функторами, более универсальны и больше соответствуют духу
объектно-ориентированного программирования, где абсолютно все стремится стать настоящим
объектом с интерфейсом и скрытыми от посторонних глаз данными.
Посмотрим, как функторы можно применить для вывода на экран или в файл
элементов контейнера. Первый вариант программы, выводящей на экран строки из
контейнера vector, показан в листинге 9.12.
Листинг 9.12. Вывод строк на экран
#include <iostream>
#include <vector>
#include <string>
using namespace std;
void output(const string &arg) {
cout « arg « endl;
}
Продолжение &
160
Глава 9. Операторные функции
Листинг 9.12 (продолжение)
int mainO {
vector<string> words;
words.push_back("На"):
words.push_back("златом");
words.push_back("крыльце"):
words.push_back("сидели");
for_each(words.begin(). words.endO. output);
}
В нем используется простейший алгоритм for_each(), требующий двух итераторов
и функции, определяющей, какие действия произвести с каждым объектом
контейнера. В нашем случае функция output () принимает ссылку на строку и выводит
соответствующий объект на экран.
Но представим себе, что программа должна выводить элементы контейнера не
только на экран, а куда укажут, например, в файл. Обычно для этого в функциях
заводят еще один параметр — тип устройства вывода, но в нашей функции output ()
этого сделать нельзя, потому что алгоритм foreachO использует функции только
с одним параметром.
Значит, объект вывода нужно сделать элементом данных класса, а роль функции
выполнит правильно определенный оператор () — примерно так, как в
листинге 9.13.
Листинг 9.13. Вывод строк с помощью объекта-функции
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
using namespace std:
class output {
public;
output (ostream &chosen_out) ; out_(chosen_out){}
void operatorO(const string &arg){
out_ « arg « endl;
}
private:
ostream &out_;
}:
int mainO {
ofstream foCzzz");
vector<string> words;
words.push_back("Ha");
words.push_back("златом");
words.push_back("крыльце");
words.push_back("сидели");
for_each(words.begin().words.end().output(cout));
f or_each (words. beginO. words.endO. output (fo));
fo.closeO;
}
В классе output из листинга 9.13 объект out_ класса ostream, куда нужно вывести
элементы контейнера, задается конструктором:
output (ostream &chosen_out) : out_(chosen_out){}
Оператор !
161
а поведение функции обеспечивает оператор (). Теперь при выводе на экран объект
класса output получает при создании имя объекта cout: for_each( output(cout));,
а при выводе в файл — объект класса ofstream: for_each( output(fo));
С классом ofstream мы до сих пор не встречались, потому что занимались только
чтением готовых файлов — объектов класса i fstream. Класс ofstream отличается
тем, что файлы, принадлежащие ему, можно заново создавать и потом записывать
в них данные.
Оператор!
В разделе «Анаграммы-2» главы 8 нам приходилось проверять, открылся ли файл,
в котором хранится словарь. Эта проверка понадобилась из-за того, что в
командной строке можно по ошибке указать имя несуществующего файла, что сделает
дальнейший поиск анаграмм бессмысленным.
Для проверки мы использовали собственную функцию fail О, возвращающую
fal se, когда файл не удается открыть:
ifstream infile(FName);
if(infile.fail()){
status_ - false;
cout « "Ошибка при открытии файла" « endl;
return:
}
Но чаще в текстах программ можно увидеть другую, гораздо более непонятную
проверку:
ifstream infile(FName);
if(Mnfile) {
}
В ней оператор логического отрицания применяется к объекту infile класса
ifstream — как будто infile — логическая или целочисленная переменная. Но
в C++ случаются и не такие чудеса, потому что оператор ! можно определить для
любого, самого экзотического класса и тем самым заставить любой объект
возвратить true, когда все хорошо, или false — в случае каких-то неприятностей.
Оператор ! для класса Anagram крайне прост:
class Anagram {
public:
bool operator !(){
return status_:
}
private:
bool status_;
}:
162
Глава 9. Операторные функции
По сути, это замаскированный вызов функции, возвращающей значение
переменной status. Проверка состояния объекта с помощью оператора ! будет выглядеть
так:
Anagram af(argv[l]); //получить анаграммы,
if(!af) af.AnagrOutO; //если все в порядке - показать
//анаграммы на экране
Обратите внимание на то, что компилятор не смущает «префиксность»
оператора !. Встретив его слева от имени объекта, он организует вызов соответствующей
операторной функции, которую «пристегнет» к объекту точкой, стоящей справа:
af.operator!О; //!af
Заметим в заключение, что не всем понравится проверка состояния объекта с
помощью оператора !. Быть может, кто-то захочет использовать простую
собственную функцию ok О:
if(af.okO) af.AnagrOutO;
Но и тем и другим нужно знать об операторе !, чтобы не удивляться, встретив
в чужой программе запись !<объект, не похожий на число>.
Увеличение
Эта глава неожиданно оказалась длинной, но картина не будет полной, если
пройти мимо унарных операторов ++ и --. Оператор ++ (для оператора -- все
аналогично) может быть определен в соответствующем классе, в этом случае операторной
функции operator++() не нужны параметры, достаточно указателя на текущий
объект this.
Попробуем переопределить оператор ++ для одного из известных нам классов.
Объект этого класса должен быть таким, чтобы автоувеличение смотрелось так же
естественно, как и для «родных» переменных типа int. Вряд ли стоит определять
оператор ++ для объектов класса compl ex, потому что для них увеличение не имеет
явного смысла (что увеличивать — реальную часть, мнимую часть или, быть
может, обе?).
Поэтому вспомним о классе myclock, созданном в разделе «Богатые и бедные
классы» главы 8. Для объектов этого класса автоувеличение вполне естественно: это
перевод часов на секунду вперед. Соответствующая операторная функция
показана в листинге 9.14.
Листинг 9.14. Префиксный оператор ++
myclock & operator++() {
tickO:
return *this:
}
Эта операторная функция сначала «переводит стрелки» на секунду вперед
функцией tickO, а затем возвращает ссылку на измененный объект.
Увеличение
163
Очевидно, эта функция реализует префиксный оператор, ведь объект
возвращается после перевода стрелок. Программа, использующая такой оператор, показана
в листинге 9.15.
Листинг 9.15. Использование префиксного оператора ++
finclude <iostream.h>
using namespace std:
#include "myclock3.hpp"
int main(){
myclock aA0.59,29):
forCint i-O; i<1000;i++)
cout « ++a;
}
В файле myclock3.hpp вместе с другими собственными функциями класса myclock
есть и операторная функция для префиксного оператора ++. Кроме того, там
определена операторная функция для оператора «. Если вам не удалось решить
задачу 9.3, можете найти ответ в этом файле.
Программа из листинга 9.15 показывает 1000 значений времени, начиная с 10
часов 59 минут и 30 секунд, потому что значение, установленное конструктором,
перед выводом на экран A0:59:29) увеличивается на секунду оператором ++.
Обратите внимание на то, что префиксный оператор, операторная функция для
которого показана в листинге 9.14, возвращает ссылку на объект а. Как мы
знаем, функции (в том числе операторной), возвращающей ссылку, можно
присвоить значение, поэтому в полном соответствии со стандартом языка C++ объекту а
можно следующим образом присвоить значение (если операторная функция для
оператора ++ определена так, как показано в листинге 9.14):
++а - Ь;
Смысла в этом немного, ведь, несмотря на увеличение, объект а все равно станет
равным Ь, но, по крайней мере, стандарт это разрешает. Можно также
использовать цепочку операторов ++++а (выполняемых, согласно правилам C++, справа
налево), в которой один оператор ++ передает ссылку на объект другому
оператору ++, и в результате время увеличивается на две секунды.
Построив операторную функцию для префиксного оператора, перейдем к
постфиксному оператору, который должен увеличивать время после использования
объекта. Если говорить об объектах класса mycl ock, то после выполнения
следующих инструкций часы b должны показывать 10:59:30, а часы а — 10:59:31:
clockaA0.59.30). b:
b = а++;
Но как компилятор поймет, что оператор постфиксный? Очевидно, нужен
какой-то знак, отличающий его от префиксного, и создатели C++ решили
использовать для этого фиктивный целочисленный параметр. Если в определении
операторной функции имеется запись (int) (в круглых скобках), функция
реализует постфиксный оператор. Определение такой операторной функции для класса
myclock показано в листинге 9.16.
164
Глава 9. Операторные функции
Листинг 9.16. Постфиксный оператор ++
myclock operator++(int) {
myclock tmp = *this;
tickO;
return tmp;
}
Здесь сначала создается временный объект tmp, где запоминается текущее
состояние объекта (this — указатель на текущий объект, оператор * превращает
указатель в сам объект, а присваивание tmp=*thi s приводит к тому, что объект tmp
«запоминает» состояние текущего объекта). Далее «стрелки» объекта *this переводятся
на секунду вперед, и временный объект возвращается во внешний мир. Обратите
внимание: возвращается объект, а не ссылка на него. Это значит, что
постфиксному оператору нельзя присвоить значение, он должен стоять справа от оператора
присваивания. Программа, показанная в листинге 9.17, использует постфиксный
оператор для увеличения времени на секунду. В ней используется новый
заголовочный файл myclock4.hpp, содержащий определение двух операторов ++ —
префиксного и постфиксного.
Листинг 9.17. Испытание постфиксного оператора ++
#iinclude <iostream.h>
#iinclude "myclock4.hpp"
using namespace std:
int mainO {
myclock aA0. 59. 29):
cout « a++ « endl; //10:59:29
cout « a « endl: //10:59:30
}
Эта программа выводит на экран сначала A0:59:29), а затем A0:59:30).
Оказывается, операторные функции для всех постфиксных операторов
устроены примерно так же, как и для оператора ++ класса myclock (см. листинг 9.16).
Например, для целочисленной переменной i, стоящей справа в равенстве a=i++,
компилятор создает временную переменную t, запоминает в ней значение i, затем
увеличивает i на единицу и, наконец, «подсовывает» t переменной а. Создание
временной переменной в операторной функции для постфиксного оператора
называется «побочным эффектом» и объясняет, почему нельзя записать a++=i.
Дело в том, что вместо а создается временная переменная t, в результате
переменной а присвоить значение i не удастся. Поэтому в стандарте языка сказано,
что постфиксный оператор может стоять только справа от знака равенства. По
той же причине нельзя записать (а++)++, ведь первый оператор вернет не
увеличенное значение а, а временный объект, увеличивать который оператором ++
бессмысленно1.
1 Это значит, что, встретив запись а++++, компилятор независимо от типа объекта должен выдать
сообщение об ошибке. В реальности так бывает не всегда. Некоторые компиляторы создадут программу
с такой инструкцией, только вот работать она будет неверно. Никакого увеличения на две секунды не
получается.
Две скобки [][]
165
Две скобки [][]
Оператор () для доступа к элементу двухмерного массива, определенный в
начале этой главы, хорош всем, кроме одного: он не похож на привычные квадратные
скобки [][] и больше напоминает вызов функции. Несмотря на это, его часто
используют, потому что он прост и эффективен.
Но все же многим программистам хочется использовать квадратные скобки,
потому что они естественней, привычней и не создают путаницы. Беда только в том, что
оператора [][] в природе не существует. Есть только последовательность
операторов [][][], выполняемых слева направо. Поэтому для доступа к массиву с помощью
квадратных скобок приходится определять его как некий объект, содержащий
массив вспомогательных объектов (например, массив массивов). И первый оператор []
будет возвращать ссылку на вспомогательный объект, а второй оператор [] должен
быть определен уже для вспомогательного объекта, хранящего элементы массива.
Проще всего реализовать эти идеи, если определить динамически создаваемый
двухмерный массив как вектор векторов. Тогда первый оператор [] должен
возвратить ссылку на вектор, а второй оператор [] просто не понадобится определять,
потому что у вектора он уже есть.
Листинг 9.18. Использование квадратных скобок [][] в динамическом массиве
#include <iostream>
#include <vector>
using namespace std;
class dynamic_array {
public:
dynamic_array(){};
dynamic_array(int rows, int cols) {
for(int i = 0; i < rows; ++i) {
data_.push_back(vector<int>(cols));
}.
}
vector<int> & operator^] (int i) {
return data_[i]:
}
private:
vector<vector<int> > data_:
}:
int mainO {
dynamic_array aC. 3):
a[l][2] = 2:
int x = a[l][2]:
cout « x « endl; 112
}
Программа из листинга 9.18 использует в качестве двухмерного массива
целочисленных переменных int объект типа dynamic_array, представляющий собой вектор
векторов1:
vector<vector<int> > data ;
1 Обратите внимание на разделенные пробелом закрывающие скобки > >. Не будь пробела,
компилятор принял бы их за оператор вывода » и выдал сообщение об ошибке.
166
Глава 9. Операторные функции
Конструктор класса dynamicarray получает число строк и столбцов в двухмерной
структуре, затем создает одну за другой строки массива vector<int>(cols) и
заталкивает их еще в один вектор:
for(int i - 0; i<rows; ++i) {
data_.push_back(vector<int>(col s)):
}
В результате получается вектор, состоящий из rows векторов, содержащих col s
переменных int. Оператор [], определенный для этого вектора векторов, возвращает
ссылку на вектор из целочисленных переменных int:
vector<int> & operator^](int i) {
return data_[i];
}
Когда в программе встречается выражение а[1][2] =2;, компилятор вызывает
сначала оператор [], определенный в классе dynamicarray и возвращающий ссылку на
вектор, хранящий ncols переменных типа int, то есть на первую строку1. Второй
оператор [], стоящий правее, применяется уже к вектору, хранящему
целочисленные переменные. Этот оператор известен в классе vector и вызывается
автоматически, без нашего участия.
Задача 9.4. Сравните скорость обращения к объектам типа dynamicarray (см.
листинг 9.18) и matrix (см. листинг 9.1). Для этого создайте большие двухмерные
массивы обоих типов и сравните время заполнения их числами.
Постоянные операторы и функции
Казалось бы, оператор [], определенный в предыдущем разделе, хорош всем.
Возвращая ссылку на объект, он способен и читать, и менять любой элемент вектора.
Но излишние способности иногда вредят. Посмотрим, например, как справится
оператор [], определенный для очень простого класса array (листинг 9.19) с
переданным функции постоянным объектом.
Листинг 9.19. Попытка применить оператор [] к постоянному объекту
#include <iostream>
using namespace std:
class array {
public:
array(int size) {
ini_ = new char[size];
}
-arrayО{delete [] ini_;}
//конструктор копии, оператор присваивания и т. д...
char & operator[](const int p) {
return *(ini_ + p); //получить р-й элемент
}
private:
1 He забывайте, что строки и столбцы нумеруются начиная с нуля.
Постоянные операторы и функции
167
Листинг 9.19 (продолжение)
char *ini_;
}:
void ShowArr(const array & a) {
cout « a[3] « endl;
}
int mainO {
array aA0);
a[3] = 'a';
//ShowArr(a); Ошибка!!
}
Класс array, определенный в листинге 9.17, имитирует обычный массив. В нем
есть конструктор, выделяющий память для size переменных char и деструктор,
освобождающий эту память. Такому классу положен, конечно, и конструктор
копирования и оператор присваивания, но они нас сейчас не интересуют. Нам
интересен оператор []:
char & operator^](const int p){
return *(ini_ + p);
}
Он получает указатель на элемент массива, прибавляя к указателю на начало
массива ini_ номер элемента р1, затем превращает оператором * указатель в сам
элемент и наконец возвращает ссылку на этот элемент. Этот оператор используется
внутри функции ShowArrO, чтобы показать на экране третий элемент массива.
Функция ShowArrO принимает ссылку на постоянный массив, то есть она не
вправе менять этот массив. Она и не пытается это делать, но компилятор тем не менее
выдает сообщение об ошибке, потому что определенный нами оператор способен
менять массив в принципе. Чтобы успокоить компилятор, нужно определить два
оператора [] — один такой, как в листинге 9.19, второму же нужно запретить
менять данные объекта, и возвращать он должен либо ссылку на константу, либо
просто значение:
char operator[](const int p) const {
return *(ini_+p);
}
В этом последнем операторе слово const стоит в непривычном месте — после
списка параметров в круглых скобках:
char operator[](const int p) const {...}
Задача 9.5. Добавьте оператор [], работающий с постоянными массивами, в
листинг 9.19. Посмотрите, как ведет себя компилятор при отсутствии оператора
operator[]() const{}.
В соответствии с правилами разбора объявлений, содержащих слово const (см.
раздел «Константы» главы 7), так определяется постоянный оператор [], возвращаю-
1 Этот номер не меняется оператором, поэтому параметр р объявлен как const int p.
168
Глава 9. Операторные функции
щий значение char. Когда в определении класса помещены два оператора [],
компилятор в каждом конкретном случае будет выбирать правильный вариант. Если
ему будет указано, что объект класса array можно менять, он использует оператор,
определенный в лиЬтинге 9.19. Если же явно указано, что объект не может быть
изменен (как в функции ShowArrO из листинга 9.19), компилятор использует
второй вариант оператора [] со словом const.
Оператор, как мы знаем, — это специальная собственная функция. Поэтому слово
const можно написать справа от списка параметров любой собственной функции
и его смысл при этом будет тем же самым: постоянная собственная функция не
может менять объекты своего класса.
Как мы уже знаем (см. раздел «Указатель This»), собственная функция на всех
одна и потому «обслуживает» все объекты своего класса. Перед вызовом она
получает указатель thi s, по которому находит данные конкретного объекта. В случае
когда вызывается обычная функция — это указатель типа х * const, то есть
постоянный указатель на... То есть можно менять объекты, на которые указывает this,
но не сам указатель. Постоянная же функция, объявленная как functionO const{},
получает постоянный указатель на константу const x * const, то есть указатель,
пригодный только для чтения данных.
После знакомства с постоянными собственными функциями можно расставить
слова const всюду, где они необходимы. Например, внутри оператора
присваивания из листинга 9.5
void matrix::operator=(matrix &mc){
if(this !- &mc){
nrows_ - mc.rgetO:
ncols_ = mc.cgetO;
mEi^ncols+jj^mcCi.j);
}
параметр mc не меняется и потому должен быть объявлен как постоянный:
void matrix::operator=(const matrix &mc) {
}
Но стоит совершить это благое дело — и компилятор возмутится, потому что
обнаружит в конструкторе функции rgetO, cgetO и оператор О для доступа к
элементам матрицы, о которых не известно, что они не могут изменять параметр тс.
Чтобы успокоить компилятор, нужно объявить эти функции как постоянные, не
меняющие данные объекта. Для этого слово const помещается справа от списка
параметров функции:
int rgetOconst {
return nrows_:
}
Тайные преобразования
169
Кроме того, придется написать вторую версию оператора () для постоянных
объектов. Выглядеть она может так1:
double operator()(int r.int с) const {
return mini_[r*ncols_ + с]:
}
После этого подозрительность компилятора обратится уже на функции rgetO,
cgetO и оператор О — не меняют ли они элементы данных объекта. Значит, одно
слово const способно вызвать лавинообразные изменения в программе, поэтому
лучше добавлять его по мере создания функций. Если по смыслу задачи какой-то
параметр постоянен, сообщите об этом компилятору. Если функция только читает
данные — снабдите ее словом const.
Задача 9.6. Перепишите класс matrix с учетом постоянности собственных
функций и принимаемых ими параметров.
Тайные преобразования
В самом начале книги мы уже познакомились с преобразованиями объектов, о
которых компилятор нас не спрашивает. Встретив инструкцию, в которой делается
попытка сложить переменные разных типов (например, int и double), компилятор
преобразует более «узкий» тип к более широкому (в нашем случае int — к double)
и затем уже произведет сложение.
Подобные преобразования можно обеспечить для объектов любого класса, если
создать специальный оператор, преобразующий объект данного класса к типу
<тип>:
operator <тип>() const {...}
В качестве примера создадим простой класс fraction, описывающий дроби, то есть
объекты, у которых есть числитель и знаменатель без общих множителей,
например 1/3, 3/5, 7/8. Дробь— это особый объект, отличный от переменных double
или целых чисел. Но часто дроби используются в арифметических выражениях,
и потому оператор приведения дроби к типу doubleO кажется вполне разумным
(листинг 9.20).
Листинг 9.20. Неявное преобразование дроби к типу double
finclude <iostream>
using namespace std;
class fraction {
public:
fraction(int n. int d) : numerator_(n). denominator_(d){}
operator doubleO const {
return static_cast<double>(numerator_) / denominator^;
}
1 Обратите еще раз внимание на то, что оператор возвращает значение double, а не ссылку. Это дает
гарантию, что с помощью такого оператора не удастся присвоить значение элементу объекта matrix.
170
Глава 9. Операторные функции
private:
int numerator^;
int denominator_:
}:
int mainO {
int a = 1:
fraction fA.3):
cout « a + f « endl: //1.33333
cout « f « endl; //0.33333
}
Сама дробь описана в листинге классом fraction, у которого в зоне private
расположены две целочисленные переменные numerator_ (числитель) и denominator
(знаменатель). Конструктор класса стандартен, поэтому обратим внимание на
оператор приведения к типу double:
operator doublet) const {
return static_cast<double>(numerator_) / denominator;
}
Как видим, он не имеет даже имени. В нем указан только тип возвращаемого
значения (double), а слово const после круглых скобок говорит о том, что оператор не
меняет объекты, принадлежащие классу fraction. Внутреннее устройство
оператора понятно: в нем числитель дроби явно приводится к типу double оператором
staticcast, а компилятор уже неявно «подтянет» к этому типу знаменатель дроби
denominator, поэтому оператор вернет уже десятичную дробь в переменной типа
double.
Оператор приведения для класса fraction работает так же незаметно, как и
оператор приведения переменной int к типу double. Видя попытку сложить
целочисленную переменную и дробь:
int a = 1;
fraction fA.3):
cout « а + f « endl;
компилятор приводит дробь f к типу double, а затем «подтягивает» к типу double
целочисленную переменную а. В результате на экране появляется число 1,33333 —
приблизительное значение суммы 1 + 1/3.
Гораздо сложнее поведение компилятора при выводе переменной f на экран:
cout « f « endl;
Прежде всего компилятор попытается найти оператор « для класса fraction. He
найдя его, он станет искать какой-нибудь оператор преобразования. Увидев, что
объект f можно преобразовать к известному объекту cin типа double, компилятор,
не спрашивая нас, выполнит преобразование, и на экране появится десятичная
дробь 0,33333.
Но хорошо ли это? Ведь переменная double может хранить лишь
приблизительное значение дроби. Сам же объект типа fraction должен отображаться на экране
иначе. Здесь роль оператора приведения double () была скорее негативной, потому
Тайные преобразования
171
что он воспользовался нашей небрежностью (не снабдили класс fraction
оператором «) и превратил дробь в значение double.
Катастрофы, конечно, не произошло, но кто знает, где и как может сработать
оператор приведения? Компилятор способен действовать по своему усмотрению,
молча, а в результате оператор приведения будет вызываться в самых
неожиданных местах. Вот почему его лучше не использовать вовсе, а если преобразование
типов все-таки необходимо, написать собственную функцию ToDoubl e(). Сложение
дроби и целочисленной переменной будет теперь выглядеть не так красиво:
cout « а + f.ToDoubleO « endl:
Но зато «все тайное теперь станет явным», и программисту не нужно будет гадать
о том, что делает компилятор.
Глава 10
Наследование
Составные объекты
В предыдущих главах мы познакомились с двухмерными массивами, описанными
классом matrix. Из-за необходимости выделять память во время исполнения
программы нам пришлось создавать конструктор, деструктор, конструктор
копирования и оператор присваивания. Кроме того, у класса matrix появился оператор
доступа О. Чтобы вся эта работа не пропала зря, нужно научиться применять класс
matrix к решению разнообразных задач.
Рассмотрим в качестве примера систему из двух уравнений:
Ее решение записывается с помощью определителей трех матриц (см. выше),
каждый из которых вычисляется по следующей формуле:
det = я@,0) х яA,1) - аA,0) х «@,1), где a (ij) — элемент матрицы (i — номер
строки J — номер столбца1).
Очевидно, класс matrix поможет решать системы уравнений, если снабдить его
собственной функцией, вычисляющей определитель. Но делать это в общем
случае для любых квадратных (то есть имеющих равное число строк и столбцов)
матриц довольно трудно, а вычислять определитель матрицы 2x2 прямо в классе
matrix, описывающем массивы произвольного размера, нелогично.
Так неужели придется создавать новый класс matrix2_2, в котором, кроме уже
сделанных конструкторов и операторов, будет еще одна собственная функция,
вычисляющая определитель квадратной матрицы 2x2?
Напомню, что нумерация строк и столбцов начинается с нуля.
Составные объекты
173
К счастью, делать этого не нужно, потому что в C++ есть возможность включить
в новый класс данные и собственные функции старого. Эта возможность
называется наследованием. Попробуем же «унаследовать» все хорошие черты старого
класса в новом, описывающем массивы размером 2x2.
Чтобы показать, что класс matrix2_2 происходит от класса matrix, используется
ключевое слово public, предваренное двоеточием:
class matrix2_2 : public matrix {
public:
private:
double det_: //значение определителя
}:
Класс matrix2_2 наследует от класса matrix все переменные. То есть в объект класса
matrix2_2 перейдут из класса matrix переменные int nrows_, int ncols_, double *mini_
и еще добавится «своя» переменная double det_. Иными словами, размер объекта
matrix2_2, определяемый оператором sizeof О, будет больше, чем у объекта класса
matrix, как раз на величину переменной det_.
В отличие от переменных конструктор и деструктор не наследуются1 — прежде
всего потому, что довольно редко конструктор базового класса (так мы будем
называть класс matrix) подходит классу производному (так мы будем называть класс
matrix2_2). Например, матрица 2x2 содержит всего четыре элемента, и будет
разумно не только выделить для них память (как это было в классе matrix), но
и установить их начальные значения. Поскольку matrix2_2 наследует переменные
из matrix, было бы логично вызывать сначала конструктор matrix, а затем
конструктор matrix2_2. Для этого в C++ имена конструкторов производного и базового
классов разделяются двоеточием:
matrix2 20: matrixB.2){
За исключением конструктора и деструктора все остальные собственные функции
базового класса наследуются классом производным. Значит, для вычисления
определителя матрицы 2x2 можно использовать уже готовый оператор доступа (),
определенный в классе matrix. Получившийся класс matrix2_2 показан в листинге
10.1.
Листинг 10.1. Описание квадратной матрицы 2x2 (файл matrix2_2.hpp)
class matrix2_2 : public matrix {
public:
matrix2_2 (double aOO. double aOl.
double alO. double all) : matrixB.2) {
(*this)@.0)=a00:
(*this)@.1)-a01:
(*thisKL0)=al0; Продолжение*
Также не наследуются переопределенный оператор присваивания (=) и «друзья».
174
Глава 10. Наследование
Листинг 10.1 (продолжение)
(*this)(l.l)-all:
}
matrix2_2() : matrixB.2){};
double det() {
det_ - (*this)@.0)*(*this)(l.l)
(*this)@.1)*(*this)A.0);
return det_;
}
private:
double det_:
}:
В этом классе реализованы два конструктора: первый устанавливает начальные
значения элементов матрицы, второй же просто выделяет для них память. Эти
конструкторы как бы состоят из двух частей: сначала вызывается конструктор
базового класса, затем — конструктор производного. Естественно, уничтожение
объекта идет в обратном порядке: сначала вызывается деструктор производного
класса, затем — деструктор базового1. Поэтому собственный деструктор классу
matrix2_2 и не нужен, ведь память, выделенная конструктором класса matrix, будет
освобождена его же деструктором.
Перед тем как перейти к решению системы уравнений, взглянем еще раз на
устройство класса matrix2_2. В нем используется многое из базового класса matrix.
Прежде всего — это оператор доступа О. Для манипуляций с элементами матрицы
приходится использовать указатель на текущий объект (thi s), так как имя объекта
нам не известно.
Теперь все готово к решению системы уравнений. Соответствующая программа
очень проста (листинг 10.2).
Листинг 10.2. Применение производного класса для решения системы двух уравнений
#include <iostream>
#include "matrix3.hpp"
#include "matrix2_2.hpp"
using namespace std;
// 3x+ 5y = 21
// Юх-у = 17
int mainO {
matrix2_2 aC. 5. 10. -1):
matrix2_2 xaB1. 5. 17. -1):
matrix2_2 yaC. 21. 10. 17);
double x. y;
x = xa.detO / a.detO:
у - ya.detO / a.detO;
cout « x « endl: // x=2
cout « у « endl; // y=3
}
В ней создаются три показанные ранее матрицы, вычисляются их определители
и выводятся на экран значения неизвестных. Включаемый файл matrix3.hpp пред-
1 Нужно только понимать, что конструктор базового класса нужно вызывать явно (как это сделано
у нас в листинге 10.1), а деструкторы вызываются автоматически. Впрочем, см. конец раздела «Идеи
и вещи».
Право наследования
175
ставляет собой окончательный вариант класса matrix со всеми конструкторами
и оператором присваивания (=), а в файле matrix2_2.hpp хранится описание
квадратной матрицы 2x2 (см. листинг 10.1).
Право наследования
Решая систему из двух уравнений, мы не задумывались о том, какие права доступа
к переменным, унаследованным от базового класса, будут у класса производного.
Ясно, что «валить все в кучу» нельзя, и то, что было тайным в базовом классе, не
может стать явным в производном. Вполне понятно, что переменные из
области public базового класса останутся открытыми и в производном. Но для многих
будет неожиданным узнать, что переменные из области private базового класса
в производном классе окажутся недоступными. Добраться до них смогут только
собственные функции базового (а теперь уже и производного) класса. Пояснит
сказанное пример программы, показанной в листинге 10.3.
Листинг 10.3. Доступность переменных в базовом и производном классах
finclude <iostream>
using namespace std;
class Base {
public:
int bpub:
private:
int bpriv_:
}:
class Der : public Base {
public:
void getbpub(){cout « bpub « endl;}
void getdpriv(){cout « dpriv_ « endl:}
void fD(Base&. Der&):
private:
int dpriv_:
}:
void Der::fD(Base& b. Der& d){
b.bpub = 5: //
d.bpub = 7: //
// b.bpriv_ = 0: //Из класса Der - нельзя
d.dpriv_ = 11: //
// d.bpriv_ = 0: //Нельзя!!!
// bpriv_ = 0: //Тем более нельзя
}
int mainO {
Base b: Der dl.d2:
cout « sizeof(Base)« endl: //
cout « sizeof(Der) « endl: //
dl.fD(b.d2):
d2.getbpub(): 111
d2.getdpriv(): //11
}
176
Глава 10. Наследование
В этом примере создаются два «игрушечных» класса — базовый класс Base с
двумя переменными bpub (открытая) и bpriv_ (закрытая), а также производный класс
Der с закрытой переменной dpriv_. Поскольку Der — производный класс, он
наследует переменные bpub и bpriv_ базового класса. Всего в классе Der окажутся три
переменных: bpub, bpri v_ и dpri v_. Переменная bpub, понятное дело, будет доступна
и в базовом, и в производном классах, переменная dpriv_, помещенная в область
private производного класса, будет доступна из производного класса, но не из
базового! А вот переменная bpriv_ из области private базового класса в производном
классе будет вообще недоступна. То есть в собственной функции производного
класса fd(Base& b, Der& d) запись d.bpriv_=0 некорректна, и тем более некорректна
запись bpriv_=0! Хоть bpriv_— переменная, принадлежащая классу Der, но в
базовом классе она объявлена в области private, а по мнению разработчиков C++
производный класс не может изменить статус закрытой переменной даже после
того, как она становится его частью. Поэтому производному классу приходится
общаться с переменной bpriv_ так, как будто она чужая, используя
унаследованные собственные функции базового класса.
Задача 10.1. Чему равны sizeof (Base) и sizeof (Der)?
Итак, производный класс страдает от того, что не имеет прямого доступа к тем
своим переменным, которые объявлены закрытыми в базовом классе. Чтобы
частично восстановить «справедливость», создатели C++ придумали ключевое слово
protected (защищенный), делающее переменные доступными как базовому, так
и производному классам, причем для всего остального мира они остаются
закрытыми (private). Программа, показанная в листинге 10.4, содержит чуть
измененные базовый (Base) и производный (Der) классы.
Листинг 10.4. Доступность объектов из области protected
#inc*lude <iostream>
using namespace std:
class Base {
protected:
int bprot_;
private:
int bpriv_:
}:
class Der : public Base {
public:
void getbprot(){cout « bprot_ « endl:}
void getdpriv(){cout « dpriv_ « endl:}
void fD(Base&. Der&);
private:
int dpriv_;
}:
void Der::fD(Base& b. Der& d){
// b.bprot_ = 0: //bprot_ - protected!!!
// b.bpriv_ = 0: //bpriv_ - private!
d.bprot_ =9: //
d.dpriv_ = 11: //
bprot_ - 13: //меняет *this
// d.bpriv_=0; //Нельзя!!
Изменчивость и отбор
177
// bpriv_=0; //Тем более нельзя!!!
}
int mainO {
Base b; Der dl.d2;
// b.dpriv_=0; //нельзя
dl.fD(b.d2);
d2.getbprot(); //bprot_ в d2 = 9
d2.getdpriv(); //dpriv_ в d2 - 11
dl.getbprotO: //bprot_ в dl - 13
}
В этой программе производный класс Der наследует две переменных: bprot_
(protected) и bpri v_ (pri vate). По-прежнему из производного класса нет доступа к
защищенным и закрытым элементам базового класса, то есть инструкции b.bprot_=0
и b.bpriv_=0 внутри функции fDO, принадлежащей производному классу,
некорректны. Но теперь производный класс имеет доступ к собственной переменной
bprot_, объявленной в базовом классе в области protected, и внутри функции fDO
можно записать инструкцию:
d.bprot_ =9:
Естественно, производному классу доступны «свои» закрытые переменные (d. dpri v_
=0;), но недоступны переменные, объявленные закрытыми в базовом классе
(некорректна инструкция d.bpriv_=0;). Заметим, что инструкция bprot_=13 относится
к текущему объекту производного класса. Раз эта функция вызывается у нас как
dl.fD(b,d2), то текущим будет объект dl, и переменная dl.bprot_ станет равной 13.
А одноименная переменная объекта d2 d2.bprot_ будет равна 9.
Изменчивость и отбор
До сих пор мы изучали только наследование данных, считая, что
собственные функции наследуются легко и свободно. Например, в разделе «Составные
объекты» класс matrix2_2 без труда использовал оператор доступа О базового
класса.
Но вышло так потому, что оператор О работает с теми же данными, что и в
базовом классе. Все оказывается иначе, когда унаследованной собственной функции
требуются новые данные из производного класса.
Для примера рассмотрим класс point, описывающий точку на плоскости. У
этого класса есть собственная функция showO, показывающая на экране значения
двух координат. Предположим теперь, что нам понадобилось описание цветной
точки на плоскости. Соответствующий класс cpoint разумно сделать
производным от класса обычной точки, добавив еще одну переменную, хранящую
значение цвета. Но тогда функция showO базового класса не сможет вывести на экран
цвет точки, потому что ничего о нем не знает. Придется в классе cpoint создать
новую функцию, выводящую на экран не только координаты, но и цвет точки.
Программа, где созданы два класса: point (точка) и cpoint (цветная точка),
показана в листинге 10.5.
178
Глава 10. Наследование
Листинг 10.5. Создание своего варианта собственной функции в производном классе
#include <iostream>
#include <string>
using namespace std:
class point {
public:
point(double xc. double yc): x_(xc). y_(yc){}
void showO {
cout « "("« x_ «","« y_ « ")"« endl;
} .
protected:
double x_:
double y__;
}:
class cpoint : public point {
public:
cpoint (double xc. double yc. string s):
point(xc,yc).col_(s){}
void showO {
cout « " (" « x_ « "." « y_ « ")" « " ";
cout « col_ « endl:
}
protected:
string col_:
}:
int main(){
point pB.3):
cpoint cpD.5,"red");
p.showO: // B.3)
cp.showO: // D.5) red
}
Функции, выводящие на экран характеристики объектов, названы в обоих классах
одинаково — showO, но путаницы не возникает, потому что инструкция cp.showO
вызывает функцию производного класса, а инструкция p.showO —
соответствующую функцию базового класса point.
Замечу, что в одинаковых названиях похожих функций производного и базового
классов есть большой смысл, ведь объекты этих классов довольно близки, и
можно представить их «смесь», где для каждого из объектов вызывается «своя»
функция showO.
Но где хранить такую смесь? Первым приходит на ум обычный массив, но ведь
там хранятся одинаковые объекты, а наши классы point и cpoint хоть и похожи, но
неодинаковы по размеру. Причем, строка col, добавленная в класс cpoint, может,
в зависимости от цвета, занимать разное число символов.
Можно, конечно, выделить каждому объекту столько памяти, чтобы он
обязательно поместился в массив, но такое решение некрасиво, потому что в памяти,
занимаемой объектами, образуются пустоты. Гораздо изящней поместить в массив
не сами объекты, а указатели на них. Ведь указатель — это адрес начала
объекта, а размер адреса не зависит от размера объекта, он одинаков как для обычных
(point), так и для цветных (cpoint) точек.
Изменчивость и отбор
179
Остается решить, массив каких указателей должен быть создан. Очевидно, это не
могут быть указатели на производные классы, ведь указатель на объект — не
просто адрес, это еще и знание всех составных частей объекта, а значит, и его размера1.
Если р — указатель на объект класса point, то р->х — это горизонтальная
координата точки, а р->у — вертикальная координата. Если же в указателе на объект
производного класса хранить адрес объекта базового класса, у программиста появится
доступ к несуществующим данным (в нашем примере — к цвету точки), что очень
опасно.
Но с другой стороны, не очень понятно, как использовать указатель на объект
базового класса. Ведь ему неизвестны данные и собственные функции класса
производного. Программа, иллюстрирующая сказанное (листинг 10.6), содержит те же
определения классов point и cpoint, что и программа из листинга 10.5.
Листинг 10.6. Использование указателей для вызова собственных функций
#include <iostream>
#include <string>
using namespace std:
class point{
}V
class cpoint:public point {
}V
int main(){
point pB.3):
cpoint cpD,5."red");
point *pp = &p;
pp->show(): //B.3)
pp=&cp;
pp->show(); //D.5)
}
В этой программе создаются объекты базового и производного классов point
и cpoint. Далее указателю на базовый класс присваивается сначала адрес объекта
базового класса р — (рр=&р), а затем адрес объекта производного класса — (рр=&ср).
И хоть указатель правильно ориентируется в адресах объектов, он остается
указателем базового класса и используется функцией show(), известной только базовому
классу. Значение цвета он не выводит.
Как же сделать так, чтобы по адресу объекта чудесным образом определялся его
тип и вызывалась нужная функция? Оказывается, в C++ для это достаточно
использовать одно лишь слово virtual, поставленное перед именем функции
базового класса. В листинге 10.7 показана программа, которая вызывает функцию showO
в соответствии с типом объекта. Если в указателе хранится адрес объекта базового
1 Вспомним, что при увеличении на единицу указатель передвигается к следующему объекту.
Следовательно, указателю известен размер объекта, на который он указывает.
180
Глава 10. Наследование
класса, вызовется функция showO, выводящая на экран только координаты точки.
Если же указатель содержит адрес объекта производного класса, вызовется
функция showO, выводящая на экран не только координаты, но и цвет точки.
Листинг 10.7. Использование виртуальных функций
#include <iostream>
#iinclude <string>
using namespace std;
class point {
public:
point(double xc. double yc): x_(xc). y_(yc){}
virtual void showO {
cout « " (" « x_ « "," « y_ « ")" « endl;
}
protected:
double x_:
double y_:
}:
class cpoint:public point {
public:
cpoint (double.xc. double yc. string s):
point(xc.yc).col_(s){}
void show(){
point::showO:
cout « « «« col_ « endl:
}
protected:
string col_:
}:
int mainO {
point pB.3):
cpoint cpD.5.»red»):
point *pp = &p:
pp->show(): cout « endl: //B.3)
pp = &cp:
pp->show(): cout « endl: //D.5) red
}
Заметим, что в этом варианте программы производный класс использует в своей
функции showO функцию showO базового класса. Чтобы явно указать
компилятору, какую функцию вызывать, применяется оператор расширения области
видимости — poi nt::show().
Теперь мы, наконец, готовы решить задачу, ради которой писался этот раздел. Для
одновременной обработки «разношерстных» объектов, принадлежащих базовому
и производным классам, нужно соблюсти перечисленные ниже условия.
1. Создать в базовом и производных классах одноименные функции (в нашем
примере — это showO), которые и займутся обработкой.
2. Перед объявлением функции в базовом классе поставить ключевое слово
virtual.
3. Создать массив указателей на объекты базового класса и поместить туда
указатели на наши объекты.
Изменчивость и отбор
181
Пример программы, обрабатывающей различные варианты написания точек (для
базового и производных классов), показан в листинге 10.8.
Листинг 10.8. Запуск виртуальных функций с помощью
массива указателей на базовый класс
#include <iostream>
finclude <string>
using namespace std;
class point {
virtual void show(){}
}V
class cpoint : public point {
void show(){}
}V
class ccpoint : public cpoint {
public:
ccpoint(double xc. double yc. string s. int w):
cpoint (xc.yc.s),width(w){};
void showO {
cpoint: :show(); cout « " "« width:
}
protected:
int width:
}:
int mainO {
point *ppts[3]; //массив указателей
ppts[0] « new pointB.3);
ppts[l] = new cpointD.5."red"):
ppts[2] = new ccpointF,7."blue".2):
for(int i - 0: i < 3; i++){
ppts[i]->show():
cout « endl:
}
}
В этой программе классы point и cpoint такие же, как и в листинге 10.7. От
предыдущей эту программу отличает еще один класс ccpoint, производный от cpoint.
В объектах этого класса добавлен еще один признак точки — ширина wi dth_. Новый
класс порожден классом cpoint, то есть классу point он приходится как бы внуком.
И несмотря на это, одного слова virtual в самом старшбм классе point
достаточно, чтобы программа по указателю на объект точно определила, какую функцию
showO вызывать.
Действительно, в функции mainO (см. листинг 10.8) создается массив из трех
указателей на объект класса point — *ppts[3]. Затем создаются объекты point, cpoint
и ccpoint, причем указатели на них засылаются в массив. Так, указатель на ccpoint
создается инструкцией:
ppts[2] = new ccpointF.7."blue".2 );
182
Глава 10. Наследование
Далее указатели перебираются в цикле, и для каждого объекта, на который они
указывают, вызывается функция showO. Как будет выполняться инструкция
ppts[i] ->show(), зависит от того, есть ли слово vi rtual перед объявлением функции
showO в базовом классе. Если оно есть, вызывается функция showO,
соответствующая объекту, на который направлен указатель, если нет — функция show() базового
класса. В нашем примере на «нулевом обороте» цикла (i=0) функция showO класса
point покажет на экране B,3), на первом обороте та же функция, но определенная
в классе cpoint, покажет D,5) red и, наконец, на втором, последнем обороте будет
вызвана функция showO класса ccpoint, которая выведет на экран F,7) blue 2.
Задача 10.2. Определите размер объектов классов point и cpoint в двух случаях,
когда, во-первых, функция showO класса point определена с ключевым словом
virtual и, во-вторых, без него. Почему и зачем слово virtual увеличивает размер
объекта?
В этом длинном разделе нам осталось рассказать о механизме вызова функций
в соответствии с типом объекта. Ключевое слово virtual не творит чудес. Просто,
встретив его, компилятор начинает иначе строить объекты базового и всех
производных классов (в том числе и производных от производных). Оказывается,
слово virtual добавляет в каждый объект закрытую переменную, расположенную на
одинаковом расстоянии от его начала, что позволяет легко найти ее в процессе
исполнения программы. Эта переменная хранит тип объекта, и, прочитав его,
программа просто вызывает функцию, которая этому типу соответствует.
Идеи и вещи
Идеи (высшая из них — идея блага) —
вечные и неизменные умопостигаемые
прообразы вещей, всего преходящего и
изменчивого бытия; вещи — подобия и отражения
идей.
Большой энциклопедический словарь,
статья «Платон»
Согласно философии Платона предметы нашего мира — суть несовершенные
отражения идей из иного, идеального мира. Существует идея яблока, собаки,
кошки, велосипеда и т. д. Не правда ли, классы языка C++ и созданные по их образу
и подобию объекты напоминают платоновские идеи и их несовершенные
воплощения? Разница в том, что объекты C++, о которых мы до сих пор знали, — скорее
точные, а не приблизительные копии идей (классов). Но в C++, как вы, наверное,
уже заметили, есть все, в том числе и абстрактные платоновские идеи.
Представим себе различных животных: кошек, собак, свиней и т. д. Каждое
животное издает определенные звуки: собака лает, кошка мяукает, свинья хрюкает.
И в соответствующих объектах должны быть функции, имитирующие эти звуки.
Но чтобы автоматически вызывать нужную функцию, как это мы делали в преды-
Идеи и вещи
183
дущем разделе, необходимо не только одинаково назвать эти функции в разных
классах (скажем voice), но и сделать все эти классы производными от чего-то
высшего, например от класса animal (животное). В этом классе тоже должна быть
функция voice, перед объявлением которой стоит слово virtual. Но как кричит
«животное»? Каким образом определить такую функцию?
Проще всего оставить такую функцию пустой:
class animal {
public:
virtual void voice(){};
}:
Но это не очень красиво, ведь объекты, которые ничего не делают, никому не
нужны. Поэтому в C++ решено в таких случаях быть честными до конца и прямо
указывать, что данный класс — фиктивный, созданный только для формального
построения «настоящих» производных классов и вызова собственных функций,
соответствующих природе объекта.
Делается это с помощью особой записи функции:
class animal {
public:
virtual void voiceO = 0;
}:
Функция, в объявлении которой вместо фигурных скобок стоят символы =0,
называется чисто виртуальной, а класс, содержащий хотя бы одну такую функцию,
называют абстрактным базовым классом, потому что такие классы не
порождают объектов и служат как бы «заглушкой» на самой высокой иерархической
ступени.
Программа, показанная в листинге 10.9, использует абстрактный базовый класс
animal для создания двух обычных классов dog и cat. Заметим, что в классах,
производных от абстрактного, если они, конечно, хотят порождать объекты, нужно
определить функции с тем же именем, что у чисто виртуальной функции базового
класса. Если этого не сделать, класс унаследует функцию базового класса и
автоматически станет абстрактным. А это приведет к тому, что при создании объекта
этого класса компилятор выдаст сообщение об ошибке. Выходит, абстрактные
базовые классы не так уж абстрактны, раз они помогают контролировать
правильность программы.
Листинг 10.9. Пример абстрактного базового класса
#include <iostream>
using namespace std;
class animal {
public:
virtual void voiceO = 0:
}:
class dog : public animal {
public:
void voiceO {
cout « "Ab!" « endl:
Продолжение ё>
184
Глава 10. Наследование
Листинг 10.9 (продолжение)
}
}:
class cat : public animal {
void voiceO {
cout « "Мяу!" « endl:
}
}:
int mainO {
animal *a[2];
a[0]=new dog;
a[l]=new cat:
a[0]->voice(); //Ав!
a[l]->voice(); //Мяу!
}
Вызвать нужную функцию производного класса можно не только с помощью
указателя, но и с помощью ссылки на базовый класс. В программе из листинга 10.10
функция voiceO в базовом классе animal сделана просто виртуальной, чтобы сам
класс перестал быть абстрактным и позволял создавать объекты.
Листинг 10.10. Запуск нужных функций через ссылку на базовый класс
#include <iostream>
using namespace std:
class animal {
public:
virtual void voice(){cout « "animal" « endl;}
}:
class dog : public animal {
public:
void voiceO {
cout « "Ав!" « endl:
}
}:
class cat : public animal {
void voiceO {
cout « "Мяу!" « endl;
}
}:
void AnimalSound(animal &a){
a.voiceO;
}
int mainO {
animal A;
dog D;
cat C;
animal &ad = D;
animal &ac = C;
ad.voiceO; //Ав!
ас.voiceO; //Мяу!
//dog &dg = А; Нельзя!
AnimalSound(ad); //Ав!
Animal Sound(ас); //Мяу!
}
Идеи и вещи
185
После определения классов в программе создаются три объекта: А (базового) и D,
С (производных классов). Затем ссылки на базовый класс соединяют с объектами
производных классов
animal &ad = D;
animal &ac = С:
Такая ссылка, подобно указателю, обеспечит доступ к объектам базового
класса и к виртуальным функциям базового и производного классов. Инструкция
ad.voiceO: покажет на экране «Ав!», потому что функция voiceO — виртуальна,
а сама ссылка соединена с объектом класса dog. Точно так же инструкция ас. voi се () ;
покажет на экране «Мяу!», потому что ас ссылается на объект класса cat. Заметим,
что ссылку на производный класс нельзя связать с объектом базового класса:
//dog &dg = А; Нельзя!
потому что программа получит в этом случае доступ к элементам производного
класса, которых нет в классе базовом.
Остаток листинга 10.10 показывает, как объекты производных классов можно
передать функциям, принимающим ссылку на базовый класс. Единственный
параметр функции AnimalSound(animal &a) — ссылка на класс animal. Но компилятор
не будет возражать, если функция получит ссылку на класс производный. В этом
случае инструкция a.voiceO; вызовет функцию voiceO, соответствующую классу
переданного объекта. В нашем случае программа еще раз выведет на экран «Ав!»,
а затем — «Мяу!».
А вот если заставить функцию Animal Sound() принимать не ссылку, а просто объект
класса animal
void Animal Sound(animal a) {
a.voiceO;
}
волшебство пропадет и нужная функция voiceO не вызовется. При передаче
функции AnimalSound(animal а) объектов класса cat и dog будет вызываться функция
voiceO базового класса animal, и на экране появится только скучное, безликое
слово «animal».
При передаче по значению нас подстерегает еще одна опасность — так называемое
«обрезание»1. Суть его в том, что функция, принимающая по значению объекты
базового класса, при передаче ей объектов производных классов теряет
информацию о том, что «добавлено» в производные классы. Она увидит только то, что есть
в базовом классе2. Кроме того, передача по значению предполагает копирование
объекта и будет очень медленной, если объект велик и сложен. Значит, передавать
по значению лучше только очень простые объекты вроде целочисленных
переменных, а для всех других использовать передачу по ссылке или через указатель.
1 По-английски этот эффект называется «slicing».
2 Такая же потеря произойдет, если просто приравнять объекту базового класса объект класса
производного.
186
Глава 10. Наследование
Виртуальный деструктор
Занимаясь вызовом «правильной» функции, мы опять забыли о создании и
уничтожении объектов. Представим себе, что объект производного класса создается
оператором new, причем его адрес оказывается в указателе на базовый класс:
class base {
virtual f();
}:
class der : public base {
}:
base *p;
p = new der:
delete p; //вызывается деструктор base!
Как мы знаем, деструкторы производного и базового классов вызываются
автоматически: сначала деструктор производного класса, затем базового. Но так
происходит только при явном объявлении объектов:
der derobject:
Если же адрес объекта производного класса, создаваемого оператором new,
присваивается указателю на базовый класс, то при уничтожении такого объекта
оператором delete вызовется только деструктор базового класса, что опасно, когда в
производном классе выделяется дополнительная память, о которой этот деструктор
ничего не знает. Вывод прост: деструктор базового класса всегда должен быть
виртуальным. Тогда инструкция delete p вызовет сначала деструктор производного,
а затем уже деструктор базового класса, каждый из них освободит ту память, за
которую отвечает, и все в результате будет хорошо.
Задача 10.3. В листинге 10.8 память под объекты базового и производного
классов выделяется, но не освобождается. Исправьте листинг 10.8 так, чтобы все
деструкторы вызывались и правильно освобождали память, занятую объектами.
Задача 10.4. Пусть р — указатель на производный класс. Должен ли деструктор
базового класса быть виртуальным, чтобы правильно освобождалась память
после удаления объекта инструкцией del ete p?
Пример: снова часы
Знакомство с наследованием и виртуальными функциями позволяет нам
по-другому взглянуть на объект, по-разному ведущий себя в зависимости от внутреннего
состояния. Класс myclock, созданный в главе 7, в разделе «Часы с „кнопочками"»,
описывал «настоящий» объект, чье внутреннее состояние было скрыто от
посторонних глаз. Этот объект управлялся несколькими «кнопками» — почти как
настоящие электронные часы. Но у того объекта был врожденный и неизлечимый
порок: для добавления новых функций приходилось вставлять все новые
условия case в инструкции switch, причем вставки приходилось делать в нескольких
Пример: снова часы
187
разных местах. Когда у часов было немного функций, все обходилось. Но легко
понять, что класс myclock из главы 7 при добавлении новых состояний станет
громоздким, неуправляемым и трудным в отладке. Поэтому попробуем сделать
другие, по-настоящему «аддитивные» часы, каждое новое состояние которых
описывается отдельным классом.
Основная идея новых часов состоит в том, чтобы сделать все их состояния
производными от некоего базового класса State. В этом классе должны быть объекты,
хранящие время (часы, минуты, секунды), и некая чисто виртуальная функция,
на место которой в зависимости от состояния часов будет подставляться функция
из производного класса. Эскиз соответствующей программы показан в
листинге 10.11.
Листинг 10.11. Набросок нового класса myclock
#include <iostream>
class State {
public:
virtual void handleO = 0;
protected:
static int h_.m_.s_:
}:
class Reset: public State {
public:
virtual void handleO {
h_=0: m_=0; s_=0:
}
}:
int State::h_ = 0:
int State::m_ = 0;
int State::s_ = 0:
class myclock {
private:
State *state:
public:
myclockO: state(O) {}
void requestO {
if(state) {
state -> handleO:
}
}
void setState( State *state ) {
this -> state = state:
}
}:
int mainO {
State *reset = new Reset:
myclock watch:
watch.setState(reset):
watch. requestO:
}"
188
Глава 10. Наследование
В «сердце» таких часов находится объект State, хранящий их состояние: часы,
минуты и секунды. Эти переменные объявлены в классе State как static, потому что
время у часов, а значит, и у всех объектов класса State и его подклассов — одно.
Обратите внимание на то, что начальные значения присваиваются статическим
переменным h_, m_, s_ вне класса State. Чтобы показать, что переменные
принадлежат этому классу, используется оператор области действия ::
int State::h_ = 0;
Делается это потому, что статические переменные, как мы знаем, принадлежат
классу, а не конкретным объектам. Для всех объектов данного класса они
одинаковы. Поэтому их начальные значения нельзя задать конструктором, вызываемым
для каждого объекта в отдельности. Чтобы подчеркнуть «глобальность»
статических переменных, их не только задают, но и создают вне класса. Инструкция static
int h_,m_,s_; только указывает компилятору, что будут использоваться такие
статические переменные. А запись вне класса int State: :h_ = 0; действительно создает
переменную, выделяет для нее память и присваивает начальное значение — ноль.
Из всех возможных классов, производных от State, в листинге 10.11 показан
только класс Reset, устанавливающий нулевое время.
Сами часы описываются классом mycl ock, куда включен указатель на объект State:
State *state;
Состояние часов меняет собственная функция setStateO, получающая указатель
на State и присваивающая его указателю state конкретного объекта класса
mycl ock:
void setState( State *state ) {
this -> state = state;
}
На этот конкретный объект указывает this.
Нажимает «кнопки» на часах функция requestO:
void requestO {
if(state) {
state -> handleO:
}
}
Поскольку handleO — чисто виртуальная функция класса State, поведение
функции requestO зависит от того, на что указывает state. Если state указывает на
объект производного класса Reset, то вызывается функция handleO из класса Reset,
которая приравнивает нулю часы, минуты и секунды. Если state хранит указатель
на другой класс, производный от State, то функция requestO вызовет другую
версию функции handleO. Таким образом, управление часами идет в два этапа:
сначала создается объект класса, производило от State. Указатель на него посылается
в указатель на объект класса State. Затем функция setStateO меняет состояние
объекта класса mycl ock, а функция requestO вызывает функцию handleO из
соответствующего состоянию объекта, производного от State класса.
Пример: снова часы
189
myclock watch; //watch - наши часы
//указателю на базовый класс State присваивается
//указатель на производный класс Reset
State *reset = new Reset;
watch.setStateCreset); //состояние; сброс
watch.requestO: //сам сброс часов
В листинге 10.12 показана версия часов, выполняющая то же, что и часы из
раздела «Часы с „кнопочками"» главы 7 (см. листинг 7.3).
Листинг 10.12. Полностью аддитивный вариант часов
#indude <iostream>
class State {
public:
virtual void handleO = 0;
protected:
static int h_. m_. s_;
}:
int State::h_ - 0;
int State::m_ - 0;
int State::s_ - 0;
class Reset : public State {
public:
virtual void handle(){ h_ - 0; m_ - 0: s_ - 0;}
}:
class Tick : public State {
public:
virtual void handleO {
if(++s_ ==60) {
s_ - 0;
if(++m_ — 60) {
m_ = 0;
h_ = (h_ + 1) % 24;
}
}
}
}:
class IncHour : public State {
public:
virtual void handleO {
h_ = (h_ + 1) % 24;
}
}:
class IncMinute : public State {
public:
virtual void handleO {
m_ = (m_ + 1) % 60:
}
}:
class ShowTime : public State {
public:
virtual void handleO {
std::cout « h_;
std::cout « " ":
Std::cout « m_;
Std::cout « " ";
Продолжение &
190
Глава 10. Наследование
Листинг 10.12 (продолжение)
std::cout « s_;
std::cout « '\r':
}
}:
class myclock {
private:
State *state;
public:
myclockO: state(O) {}
void request0 {
if( state ) {
state -> handleO:
}
}
State* getStateO {
return state:
}
void setStateC State* state ) {
this -> state - state:
}
}:
int mainO {
State *reset - new Reset:
State *showtime - new ShowTime;
State *tick - new Tick:
State *inchour - new IncHour:
State *incminute - new IncMinute:
myclock watch:
watch.setStateC reset ):
watch. requestO:
watch.setStateC inchour ):
forCint i-1; i <» 6: i++)
watch.requestO; //установить часы
watch.setState( incminute ):
forCint i-1; i <- 45; i++)
watch.requestO; //установить минуты
while(l) {
watch.setStateCtick);
watch. requestO;
watch.setState(showtime);
watch. requestO:
}
}
Задача 10.5. Найдите 10 отличий в листингах 7.3 и 10.12.
Из листинга 10.12 видно, что каждое новое состояние часов описывается классом,
производным от State. В наших часах таких классов 5: Reset (сброс часов), Tick
(перевести стрелки на секунду вперед), IncHour (перевести часы), IncMinute
(перевести минуты), ShowTime (показать времяI. Каждый класс задает новое состоя-
1 Объекты этих*классов создаются операторами new (например, State *reset = new Reset;), но не уни-
тожаются операторами delete, потому что программа крутится в цикле while(l){} и прерывается
искусственно — комбинацией клавиш Ctrl + Break. В данном случае это не страшно, потому что память
все равно будет освобождена операционной системой после прерывания работы программы. Такой же
недостаток есть у часов из листинга 7.3.
Пример: снова часы
191
ние часов и действия, которые нужно выполнить в этом состоянии. Чтобы задать
новое состояние часов, достаточно создать новый класс, производный от State.
То есть новые функции в такие часы действительно добавляются и оказываются
«заперты» в пределах одного класса.
Напоследок сравним еще раз листинги 7.3 и 10.12. Часы, показанные в них,
невозможно отличить, если смотреть только показываемое на экране время. Но какая
огромная разница в исходных текстах! В часах из листинга 10.12 просматривается
новый, еще не известный нам язык C++. Этот язык создан благодаря многим
людям, которые пытались осмыслить и применить на практике то, чему положил
начало Бьярн Строуструп. В языке C++ есть, оказывается, множество тайн, которые
не могут открыться одному человеку. Сотни программистов открывали эти тайны,
изобретали особые классы, пригодные в тех или иных обстоятельствах,
придумывали контейнерные классы из стандартной библиотеки. Иногда кажется, что C++
настолько сложен, что его надо изучать 20 лет, уединившись в каком-нибудь
тибетском монастыре. Но это чувство обманчиво. Чтобы овладеть C++, нужно как
можно больше программировать на нем. И чем раньше вы начнете, тем лучше.
Глава 11
Макросы и шаблоны
Макросы
Любая программа на C++, прежде чем ее допустят к компилятору, обрабатывается
препроцессором, который похож на текстовый редактор, управляемый
специальными командами.
Каждая команда препроцессора начинается со значка #, и мы уже хорошо знакомы
с двумя из них: #include <файл> (вставить содержимое файла в текст программы)
и #define ИМЯ1 ИМЯ2 (всюду в тексте программы заменить «ИМЯ1» на «ИМЯ2»).
Определение имени с помощью команды #define называется в языке C++
макросом и с его помощью можно не только присваивать имена константам, но и
создавать подобия условных инструкций.
Такие макросы часто применяются, чтобы не позволить директиве #include<>
несколько раз включить один и тот же файл в исходный текст программы.
Представим себе, что в файле, включаемом с помощью #include<>, есть свои
директивы #include <>, а в файлах, включаемых этой последней директивой — свои.
Чтобы не позволить препроцессору включить несколько раз один и тот же файл,
используются специальные директивы препроцессора #ifndef...#endif. Посмотрим,
например, как устроен файл <iostream> нашего компилятора дсс.
Файл <iostream> встречается очень часто, поэтому опасность его многократного
включения особенно велика. Чтобы этого не случилось, содержимое
заголовочного файла обрамляется такими директивами:
#ifndef _CPP_IOSTREAM
#define _CPP_IOSTREAM
// внутренности файла iostream
#endif
Первая директива #ifndef _CPP_IOSTREAM проверяет, известно ли уже
препроцессору имя CPPIOSTREAM. Если нет, директива #define CPPIOSTREAM добавляет имя
_ CPPIOSTREAM в список уже известных препроцессору и дальше содержимое
заголовочного файла включается в исходный текст программы. Если #include <iostream>
Inline
193
встретится препроцессору еще раз, то, открыв файл iostream, он опять встретит
директиву #i f ndef _CPP_IOSTREAM, но на этот раз имя CPPIOSTREAM уже будет известно,
и все, что помещается внутри условной директивы #ifndef...#endif, препроцессор
просто не заметит.
Как видим, стандартные заголовочные файлы защищены от многократного
включения. То же самое нужно сделать и с собственными заголовочными файлами.
Для этого каждому такому файлу нужно придумать уникальное имя и затем
использовать директивы #i f ndef... #def i ne...#endi f.
Inline
Мы долго плыли в декорациях моря,
Но вот они — фанера и клей.
БГ. Тень
Кроме управления исходным текстом программ с помощью директив #i f ndef ...#endi f,
макросы способны еще создавать подобия функций. Попробуем, например,
написать макрос, возводящий произвольное число в квадрат. Выглядеть он может так:
#define SQR(x) x*x
Ключевой элемент этого макроса — скобки. Не будь их, макрос:
#define SQRx x*x
просто заменял бы в тексте программы символы SQRx на х*х. Но открывающая
скобка, непосредственно следующая за именем SQR, говорит препроцессору, что
перед ним макрос с параметрами, заключенными в круглые скобки. В нашем случае
параметр один, это х, и встретив в тексте программы символы SQR(a),
препроцессор поставит вместо них уже а*а, а не х*х.
Программа, показанная в листинге 11.1, вычисляет «дважды два» и выводит на
экран четверку. Мы видим в ней присваивание s=SQR(i). Но компилятор на его месте
увидит s=i*i, потому что получит текст, прошедший через препроцессор.
Листинг 11.1. Простейший макрос, возводящий число в квадрат
#include <iostream>
#define SQR(x) x*x
using namespace std:
int mainO {
int i=2. s:
s - SQR(i);
cout « s « endl; // 4
}
Казалось бы, макрос с параметрами полностью заменяет функцию, ведь строчка
s=SQR(i)HH4eM по виду не отличается от вызова функции и присваивания
возвращенного значения переменной s. Но стоит немножко по-другому «вызвать»
макрос, и «вот они — фанера и клей».
194
Глава 11. Макросы и шаблоны
Программа, показанная в листинге 11.2, должна, по идее, выводить на экран 0.25,
то есть единицу, поделенную на 4.
Листинг 11.2. Ошибка макроса
#include <iostream>
#define SQR(x) x*x
using namespace std:
int main(){
double i-2.0. s:
s-1 / SQR(i);
cout « s « endl; //1 !!
}
Но препроцессор понимает макрос буквально: каждый формальный параметр
будет заменен фактическим, и компилятор на месте s=l/SQR(i) увидит s=l/i*i.
А дальше он поделит единицу на i, а результат деления умножит на i, ведь
приоритет умножения и деления одинаков, а выполняются они слева направо. В
результате 0.5 (результат деления) умножится на 2, и получится единица!
Чтобы получить правильный результат, достаточно окружить скобками
произведение переменных: 5=1/A*1), а для этого можно просто переопределить макрос,
поместив х*х в скобки:
#define SQR(x) (x*x)
Но представим теперь, что в квадрат возводится сумма двух чисел:
s=SQRB+2);
Как обычно, препроцессор не станет умничать, а тупо заменит формальные
параметры макроса фактическими. И после его работы компилятор увидит:
s=B+2*2+2):
Умножение, как мы знаем, обладает большим приоритетом, поэтому в результате
получим 8:
s=2 + B*2) +2=2+4+2=8;
а не 16, как нам бы хотелось. Выход из этого затруднения прост: нужно окружить
оба параметра скобками, чтобы они сначала вычислялись, а только потом
умножались друг на друга:
#define SQR(x) ((x)*(x))
Такой макрос правильно вычислит s=SQRB+2), потому что препроцессор
преобразует SQRC2+2) в (B+2)*B+2)). Но представим теперь, что в программе встретились
строчки:
1=2;
S=SQR(i++);
Если SQRO — функция, все будет хорошо, и после ее вызова s будет равна
четырем, a i — трем. Но если SQRO — макрос, то 1 и s могут оказаться какими угодно,
потому что препроцессор превратит SQR(I) в ((i++)*A++)) — выражение, значение
Inline 195
которого не определено. И из этого последнего затруднения есть, похоже, один
выход — просто не использовать оператор ++ и ему подобные в таких макросах.
А еще лучше заменить макрос особым видом функций, чьи объявления
предваряются словом inline. В листинге 11.3 показано, как применить для возведения
в квадрат inline-функцию.
Листинг 11.3. Замена макроса inline-функцией
#inc1ude <iostream>
inline int SQR(int);
using namespace std:
int main(){
int i=2. s:
s=SQR(i++):
cout « s « endl; //4
cout « i « endl; //3
}
inline int SQR(int i) {
return i*i;
}
Функция, объявленная как in! ine, ведет себя более предсказуемо, чем макрос. Она
принимает аргументы, как обычная функция, и если нам вздумается написать
I - 2;
SQR(i++);
то функция возведет два в квадрат и затем увеличит i, так что от нее не стоит ждать
каких-то сюрпризов. Во всем же остальном inline-функция очень похожа на макрос.
Так же как макрос, она не вызывается, то есть компилятор, встретив где-нибудь
в исходном тексте вызов функции, объявленной как inline, не засылает аргументы
в особую область памяти и не переходит к первой инструкции функции, а берет все
инструкции, из которых функция состоит, и вставляет их в то место программы,
где функция должна вызываться. То есть inline-функция, в отличие от обычной,
существующей в одном экземпляре, размножается. У inline-функции не может
быть поэтому собственного набора локальных переменных и, следовательно, она
не может быть рекурсивной (см. раздел «Рекурсия, или „Раз, два, три"» главы 4).
Раз inline-функция не вызывается, то и возврата из нее быть не может. Возврат
происходит автоматически, когда выполняется последняя инструкция функции.
А это значит, что выполнение inline-функции требует меньшего времени. Это
будет особенно заметно для функций, которые часто вызываются и почти ничего не
делают: основное время работы таких функций занимает вызов и возврат. Именно
их стоит объявлять как inl ine.
Заметим, что функции, заданные внутри класса, компилятор автоматически
старается сделать inline-функциями. Далеко не для каждой функции это
возможно, поэтому периодически вы будете слышать жалобы компилятора: «функция
такая-то не может быть inline по такой-то причине». Никакой беды в этом нет.
Чтобы слушать поменьше жалоб, следует задавать функции вне класса, используя
слово inline только там, где это действительно необходимо. Для правильного
задания inline-функции требуется сначала выяснить, где программа тратит больше
196
Глава 11. Макросы и шаблоны
всего времени1. Только ту функцию, вызов которой занимает значительное время,
стоит пытаться объявить как inline.
Может показаться, что у inline-функций есть и недостаток по сравнению с
макросами: их параметры должны быть определенного типа, в то время как макрос часто
может работать с любыми объектами. Например, наш макрос SQRO способен возвести
в квадрат любое число. Но этот недостаток легко устраняется с помощью шаблонов
функций, о которых мы говорили в разделе «Функции-тезки» главы 4.
Задача 11.1. Создайте шаблон inline-функций, возводящей в квадрат любые
объекты, для которых определен оператор *.
Шаблоны классов
Легко догадаться, что шаблоны применимы в C++ не только к функциям. Ведь
универсальными, пригодными для работы с объектами разной природы должны быть и
классы. Такие классы мы не раз уже использовали в наших программах. Вспомним,
например, контейнер vector<>, в котором можно хранить все, что угодно.
Храня объекты в контейнерах, мы до сих пор пользовались только готовыми
шаблонами классов. И вот теперь готовы создавать такие шаблоны самостоятельно.
Попробуем с помощью шаблона описать массив, способный хранить разные
объекты. Предусмотрим, кроме того, возможность менять размеры массива, а также
проверять верность индекса в операторе []. Пример класса array показан в
листинге 11.4.
Листинг 11.4. Шаблон класса аггауо
template <typename T>
class array {
public:
arrayO : lenJO). a_(NULL){}
-arrayO {delete [] a_:}
void push_back(T e) {
expandO;
a_[len_ - 1] = e;
}
T & operator[](int n) {
check(n);
return a_[n]:
}
int length О {returndenj;}
private:
T* a_; //указатель на нулевой объект
int len_: //число элементов
void checkdnt n);
void expandO:
}:
1 Такие исследования называются профилированием, и занимаются ими специальные програм-
мы-профайлеры.
Шаблоны классов
197
Шаблон класса, судя по этому листингу, создается так же, как и для функции:
перед определением класса ставится признак шаблона tempi ate <typename Т>, где Т
обозначает некий тип объекта.
Основой будущего массива служит указатель Т *а_, которому при создании
объекта конструктор присваивает значение NULL — так в C++ принято обозначать
заведомо несуществующий адрес1.
Сначала число элементов массива 1еп_ равно нулю, а новые элементы, как и
в'контейнерных классах, таких как vector, добавляются функцией pushbackO. Кроме
того, интерфейс класса содержит еще функцию TengthO, позволяющую получить
текущий размер массива, а также оператор доступа к произвольному элементу [].
Обратите внимание: оператор [] возвращает ссылку на элемент массива, чтобы
можно было присвоить ему новое значение, то есть поставить оператор [] слева от
знака равенства ([]=).
Кроме интерфейсных в области private объявлены две служебные функции:
checkO и expandO, помогающие управлять классом. Первая проверяет,
укладывается ли индекс в границы массива, вторая добавляет к нему новый элемент. Эти
функции не видны пользователям класса, поэтому их можно как угодно менять
без ущерба интерфейсу. В листинге 11.4 эти функции только объявлены, их
возможная реализация показана в листинге 11.5.
Листинг 11.5. Две функции класса аггауо
tempiate<typename T>
void аггау<Т> :: checkdnt n) {
if((n < 0) || (n >- lenj) {
cout « "Неверный индекс"« endl;
exit(l);
)
}
template <typename T>
void array<T> :: expandO {
T* temp » new T[++len_J;
if(len_ > 1){
forCint i = 0; i < len_; i++)
temp[i] = a_[i];
delete [] a_;
}
a_ = temp;
f
Нет ничего удивительного, что в шаблоне класса задаются шаблоны функций.
Каждая функция начинается словами tempi ate <typename Т>, далее оператор ::
показывает, что функция принадлежит шаблону класса array:
void аггау<Т> :: expandO
1 Значение NULL на самом деле равно нулю, и вместо a(NULL) в конструкторе можно писать а@), но
NULL наглядней — сразу видно, что перед нами указатель.
198
Глава 11. Макросы и шаблоны
Обратите внимание, что шаблон класса обозначается его именем и стоящим далее
в треугольных скобках параметром:
аггау<Т>
Нам осталось понять устройство собственных функций. В функции checkO,
предназначенной для прерывания работы программы, когда индекс массива выходит
за допустимые пределы, используется функция exit(l), возвращающая
операционной системе код ошибки 1. Функция expandO выделяет область памяти, куда
помещается на один элемент больше, затем переписывает туда все элементы
массива, далее освобождает занимаемую им память и, наконец, засылает адрес нового
массива в указатель а_.
После создания шаблона можно попробовать его «в деле» с объектами разной
природы. Программа, показанная в листинге 11.6, использует шаблон класса array,
помещенный в файл array.hpp (в него вошли листинги 11.4 и 11.5).
Листинг 11.6. Пример использования шаблона класса array
finclude <iostream>
using namespace std;
#include "array.hpp" // листинги 11.4. 11.5
int main(){
array<int> ia:
array<char* > sa;
ia.push_backA0);
ia.push_backB0);
for (int i - 0; i < ia.lengthO; i++){
cout « ia[i] « endl;
}
ia[0] - 5:
cout « ia[0] « endl; //5
sa.push_back("MaMa!"):
sa.push_back("fl боюсь!");
for (int i = 0; i < sa.lengthO; i++){
cout « sa[i] « endl;
}
}
Шаблон используется в программе для создания массива целочисленных
переменных (array<int> ia;) и массива указателей на char (array<char *> sa;). Далее
в оба массива функция push_back() «запихивает» по два значения, и если
добавление целочисленных элементов пояснений не требует, то о массиве указателей
стоит сказать несколько слов.
Инструкция sa.push_back("MaMa!") может показаться непонятной и опасной,
потому что не видно, когда для строки «Мама!» выделяется память. Но дело в том, что
компилятор C++, встретив следующую инструкцию, автоматически выделяет
память для пяти символов «Мама!» и завершающего шестого символа (' \0'), а затем
также автоматически засылает в переменную а адрес буквы «М»:
Char *а«"Мама!";
Так что в массиве sa окажутся нужные указатели, и на экран будут выведены
ожидаемые строки «Мама!» и «Я боюсь!».
Шаблоны классов
199
Задача 11.2. Напишите конструктор копирования для класса array.
Задача 11.3. Функция expandO класса array увеличивает размер массива на
единицу. Между тем, частые вызовы функции push_back() занимают много времени,
ведь каждый раз нужно перемещать массив на новое место. Чтобы программа
работала быстрее, создайте шаблон класса array, где функция expandO удлиняет
массив сразу на несколько элементов.
Глава 12
Ввод-вывод
Потоки ввода-вывода
Всякая программа (а мы их знаем уже несколько десятков) должна добывать
откуда-то данные и посылать их во внешний мир. Нам уже знакомы операторы »
и «, объекты cin и cout, файлы. Настало время получить более систематическое
и детальное представление о вводе-выводе в C++.
Прежде всего нужно сказать о потоках ввода-вывода (streamsI —
последовательностях байтов (символов), создаваемых самыми разными источниками. Всякие
ввод и вывод данных связаны с потоками. Когда, например, числа или строки
вводятся с клавиатуры, то каждое нажатие клавиши посылает в поток один символ.
За поступающими символами следит объект cin, как бы притаившийся в засаде.
До определенного момента он безучастен, но стоит появиться в потоке
символу перевода строки, посылаемому нажатием клавиши Enter, как объект начинает
действовать. Причем действия его зависят от того, что стоит справа от
оператора ». Посмотрим, например, как работает программа из листинга 12.1, в которой
объект cin читает с клавиатуры целые числа.
Листинг 12.1. Ввод чисел с клавиатуры
linclude <iostream>
using namespace std:
int mainO {
int x = 1;
while (x != 0){
cout « "Введите число" « endl:
cin » x;
cout « "Вы ввели: "« х « endl;
}
}
He путать с потоками выполнения (threads).
Потоки ввода-вывода
201
Объект cin следит за входным потоком, куда с клавиатуры поступают символы.
Как только появляется символ перевода строки, объект пытается найти среди
введенных символов те, которые можно превратить в число. Первым делом он
отбрасывает все введенные пробелы, символы табуляции и прочую «дребедень». Далее
он проверяет все идущие подряд символы 1,..., 9, 0 до тех пор, пока не встретится
пробел (символ табуляции и т. д.), после чего последовательность символов
превращается в целое число и записывается в переменную х.
Пусть, например, с клавиатуры ввели 5 пробелов, затем два символа 1 и 2 и
нажали клавишу Enter. Тогда объект cin пошлет в переменную х число 12. Подчеркнем:
символы 1 и 2 кодируются числами 49 и 50 соответственно, но объект cin
преобразует их в число 12, потому что так велит ему инструкция cin » x. Если бы вместо
целочисленной переменной х стояла строка или другой объект, объект cin вел бы
себя иначе.
А теперь посмотрим, что будет, если ввести вместо цифры какую-нибудь букву,
например q. Эксперимент показывает, что программа «срывается», бесконечно
сообщая, что введена единица, то есть значение, присвоенное х еще до входа
в цикл.
Ясно, что никакого ввода на самом деле не происходит, а произошла ошибка, с
которой программа не в состоянии справиться. Чтобы помочь ей, нужно знать, что
объект cin умеет следить за состоянием потоков с помощью собственной
функции cin.fail О. Ее значение равно true — после удачного преобразования и fal se —
в противном случае. Очевидно, ввод буквы q вместо числа привел поток в
нерабочее состояние, о чем и должна сообщить функция cin. fail О. Получив известие об
этом, программа должна вывести поток из состояния fai 1 (это делает функция ci n.
clear О) и затем продолжить ввод. Исправленная версия программы ввода чисел
показана в листинге 12.2.
Листинг 12.2. Проверка правильности ввода
#include <iostream>
#indude <fstream>
using namespace std:
int main(){
int x:
char chl;
do {
cout « "Введи число\п";
cin » x;
if (cin.failO) {
cin.clearO;
cin» chl;
cout « "Это не число" « endl;
}
else {
cout « "Прочитано число ";
cout « x « endl;
}
} while(x != 0);
}
202
Глава 12. Ввод-вывод
Заметим, что функция ci n. clear О не очищает поток от всех введенных символов.
Она лишь выводит его из нерабочего состояния. Инструкция cin » chl забирает из
потока один символ. Если до него в потоке есть еще символы, не равные цифрам,
поток опять попадет в состояние fail, снова будет выведен из него — и так до тех
пор, пока «вредных» символов не останется. Если, например, вместо числа ввести
десять символов q, то программа десять раз выведет на экран фразу «Это не
число», прежде чем будет готова к приему «правильных» цифр.
Естественно, «лишние» символы «выплеснутся» из потока и в случае, когда
вводятся не целые числа, а, скажем, последовательности символов или
строки. Программа, показанная в листинге 12.3, предназначена для ввода двух строк
с клавиатуры.
Листинг 12.3. Ввод строк с клавиатуры
#include <iostream>
#include <string>
using namespace std:
int mainO {
string si. s2;
//Вводим "Come together"
cin » si;
cin » s2;
cout « si « endl: //Come
cout « s2 « end!; //Together
}
Если ввести Come, нажать клавишу Enter, а затем ввести together и снова нажать
клавишу Enter, то, как и ожидается, в строке si окажется Come, а в строке s2 — together.
Но если сразу набрать всю фразу Come together и нажать клавишу Enter — результат
будет тем же самым. Дело в том, что, встретив пробел после Come, первый
оператор » прекратит работу, оставив в строке si слово Come. А второй оператор » уже
не нуждается в вводе с клавиатуры, потому что в потоке остались символы,
которые «вывалятся» в строку s2 без всякого нашего участия!
Функции ввода-вывода
Операторы « и>> позволяют решить только самые основные задачи ввода-вывода.
Более деликатные задачи решаются с помощью собственных функций классов
istream и ostream, которым принадлежат объекты cin и cout соответственно.
Мы уже знаем, что операторы » и « пропускают «ненужные» символы —
пробелы, табуляции и т. д. Но, допустим, что перед нами стоит задача узнать, какими
числами кодируются эти символы. Ясно, что оператор » для этого не годится.
Тут, видимо, нужно читать каждый символ отдельно, для чего в классе i stream есть
специальная функция cin.get(chI, где ch — переменная типа char. Но беда в том,
что символы поступают с клавиатуры по меньшей мере парами, потому что при
Мы уже применяли ее в главе 3 для чтения символов из файла.
Манипуляторы
203
любом вводе нажимается клавиша Enter. Значит, придется воспользоваться
функцией cin.getline(s,n), которой можно указать число читаемых символов.
Функция getline(char *s, int n) предназначена для чтения последовательности
символов, завершаемых символом '\п' (он вводится при нажатии клавиши Enter).
После ввода символы оказываются в массиве s, причем символ перевода строки
отбрасывается и заменяется нулевым символом * \0'. Параметр п задает
количество читаемых символов. Если п, например, равен трем, с клавиатуры считаются два
символа, а на месте третьего в массиве s окажется символ * \0'.
Вспомнив о нашей задаче — показать на экране число, которым кодируется тот или
иной символ, легко понять, что параметр п должен быть равен двум. Программа,
решающая эту задачу, показана в листинге 12.4.
Листинг 12.4. Программа, показывающая, какими числами кодируются символы
#include <iostream>
using namespace std;
int mainO {
char ch[2];
whiledcin.eofO) {
cin.getline(ch. 2);
cout « static_cast<int>(ch[0]) « endl:
}
}
Крошечная эта программа содержит, тем не менее, много нового. Начнем с
порядка размещения символов в массиве s, состоящем из двух переменных char. Если
ввести с клавиатуры символ и нажать клавишу Enter, то в потоке окажутся два
символа — тот, что нам нужен, и символ перевода строки ' \п'. Символ ' \п'
функция getl ine( )отбросит, записав вместо него символ ' \0'. Значит, нужный нам
символ окажется на нулевом месте (s[0]), а завершающий нуль — на первом. Чтобы
показать число, которым кодируется символ s[0], его нужно превратить в
целочисленную переменную. Иначе cout воспримет его как переменную char и
повторит введенный символ на экране.
Нам осталось понять, как программа завершит работу. Легко догадаться, что это
произойдет, когда функция cin.eofO возвратит значение true, то есть когда в
поток попадет символ конца ввода. Этот символ нельзя получить нажатием одной
клавиши. Чтобы он попал в поток, следует одновременно нажать клавиши Ctrl и Z,
то есть Ctrl+Z.
Манипуляторы
У объектов, управляющих потоками ввода-вывода, есть не только собственные
функции, но и так называемые манипуляторы. Один из них — endl — хорошо нам
знаком. Манипуляторы часто используются вместо собственных функций, потому
что они лучше и естественней выглядят. Решим для примера простую задачу:
вывести на экран сто целых чисел так, чтобы в одной строке их было 10. Естественно,
204
Глава 12. Ввод-вывод
числа на экране должны стоять стройными рядами, образуя что-то вроде таблицы.
Эту простую задачу решает программа, показанная в листинге 12.5.
Листинг 12.5. Вывод на экран таблицы чисел
finclude <iostream>
#include <iomanip>
using namespace std;
int mainO {
int m[100]:
for(int i - 0; i < 100; i++) {
Файлы
205
торая принимает ссылку на поток и возвращает ссылку на поток. Эта функция —
и есть манипулятор.
Для класса ostream определение оператора «, позволяющего использовать
манипуляторы, выглядит так:
ostream& operator«(ostream & (*func)(ostream &str)) {
return ((*func)(*this));
}
Чтобы создать собственный манипулятор, достаточно написать подходящую
функцию funcO. Простая программа, создающая манипулятор NewLine, заменяющий
endl, показана в листинге 12.6. Функция EndlineO, определенная в этой
программе, принимает ссылку на поток str, вставляет туда символ конца строки:
str.put('\n');
и затем выбрасывает все символы потока на экран:
str.flushO:
Листинг 12.6. Создание собственного манипулятора
#include <iostream>
using namespace std;
ostream & NewLine(ostream &str) {
str.put('\n'):
str.flushO;
return str;
}
int mainO {
cout « "Перевод" « NewLine « "строки";
}
В результате программа выведет на одной строке слово «Перевод», а на
следующей — слово «строки».
Файлы
Как уже говорилось, ввод-вывод данных основан на потоках ввода-вывода —
последовательностях символов. Такой последовательностью можно считать и файл,
поэтому к файлам применимы те же функции, операторы и манипуляторы, что и
к объектам cin и cout. Формально это происходит потому, что вся система ввода-
вывода в C++ построена иерархически. На вершине стоит класс ios, от которого
происходят классы istream и ostream, порождающие такие объекты, как cin и cout.
В свою очередь, файлы в C++ описываются классами if stream, of stream и f stream,
производными от i stream и ofstrefm. Вот почему они так похожи на обычные
потоки.
Посмотрим, например, как выводятся в файл целые числа и как они читаются из
файла. Программа, которая это проделывает, показана в листинге 12.7.
206
Глава 12. Ввод-вывод
Листинг 12.7. Запись в файл и чтение из файла операторами « »
#include <iostream>
#include <fstream>
#inc1ude <iomanip>
using namespace std;
int mainO {
int a[100];
of stream outCdigs"):
fordnt i = 0: i < 100: i++)
out « setwC) « i;
out.closeO;
ifstream inCdigs"):
fordnt i = 0; i < 100; i++)
in » a[i]:
for(int i - 0; i < 100: i++)
cout « a[i] « endl:
in.closeO;
}
Сначала в этой программе создается объект out класса of stream. Его конструктору
указывается имя файла digs. Класс of stream описывает файлы, созданные только
для записи, что нам и нужно.
Запись в файл ста последовательных чисел выполняет цикл:
for(int i - 0; i < 100: i++)
out « setwC) « i:
В этом цикле объект out ничем, по сути, не отличается от cout, хотя первый
описывает файл, а второй — экран монитора.
Манипулятор setwC) здесь необходим, чтобы создать пробелы между
последовательными числами. Пробелы показывают оператору », где кончается одно число
и начинается другое.
После вывода чисел файл digs закрывается функцией out.closeO и тут же
открывается вновь. На этот раз с ним связан объект in класса ifstream, то есть файл digs
теперь можно только читать, что и делается в цикле:
fordnt i - 0; i < 100; i++)
in » a[i];
После ввода в массиве а[] оказываются те же числа, что и в файле. В этом нас
убеждает последний цикл, выводящий числа из массива на экран.
Задача 12.2. Скомпилируйте и запустите программу, показанную в листинге 12.7.
С помощью текстового редактора посмотрите, что записано в файле digs.
То, что файлы происходят от потоков, вовсе не значит, что в них нет ничего нового.
Родители часто недоумевают, откуда у Них такие дети и в кого. Как мы только что
видели, у файлов много общего с родительскими классами, но много и различий,
связанных с их иной природой. Файлы «устойчивее» потоков. Их можно хранить,
что-то менять в любой части файла, добавлять данные в конец файла и т. д.
Возможные операции с файлом определяются при его открытии. Делается это с
помощью специальных констант, определенных в классе ios. До сих пор мы порож-
Файлы
207
дали файлы из классов if stream (для чтения) и of stream (для записи). Попробуем
для разнообразия объявлять файлы как объекты класса fstream, порождающего
файлы, пригодные как для чтения, так и для записи.
Итак, следующее объявление с помощью константы ios: :in открывает файл name
для чтения:
fstream name("digs", ios::in):
Можно указать сразу несколько констант, разделенных знаком |. Так, показанное
ниже объявление открывает файл name как для чтения, так и для записи:
fstream name("digs", ios::in | ios::out):
Здесь записью ведает константа ios: :out, а чтением — ios::in. В этом случае
программа может работать только со «старым», уже существующим файлом. Запись
в файл требует осторожности, потому что можно уничтожить уже существующие
данные. Именно так действует одинокая константа ios:: out. Чтобы сохранить
файл, можно использовать константу ios: :арр, разрешающую запись только в
конец файла (режим добавления). Программа, показанная в листинге 12.8, не в силах
испортить файл words, потому что всегда записывает символы в самый его конец.
Листинг 12.8. Защита файла от уничтожения
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std:
int mainO {
fstream out("words". ios::out | ios::app);
out « "Это место занято":
out.closeO:
}
Теперь после знакомства с открытием файлов можно попробовать их копировать
с помощью собственной версии команды сору операционной системы. Дублировать
файлы крайне просто, если воспользоваться их сходством с потоками. Нужно,
очевидно, читать символ из одного файла и записывать его в другой. Все это легко
проделать в цикле:
while(in.get(ch))
out.put(ch);
Здесь in — файл-источник, a out — файл-копия. Этот цикл кажется крайне простым
и поэтому, быть может, требует длинных пояснений. Удивляет, прежде всего,
условие выполнения цикла whileO, ведь in.get(ch), как и всякая операция ввода,
возвращает ссылку на объект (в нашем случае — класса i fstream или fstream). Делается
это для того, чтобы можно было соединить в одну цепочку несколько операций,
например cin.get(ch) » chl. Но условие выполнения цикла whileO — это, как мы
знаем, булево выражение, принимающее только два значения true и fal se.
Разрешается это противоречие просто: как только компилятор видит, что выражение,
в котором есть объекты, принадлежащие системе ввода-вывода, по своему смыслу
208
Глава 12. Ввод-вывод
должно принимать только два значения true и fal se, он превращает их в булево
значение1. Так, например, функция in.getO, попав в круглые скобки после whileO,
превращается компилятором в true, если чтение было успешным, и fal se — в противном
случае2. Обычно значение fal se получается, когда достигнут конец файла. В этом
случае цикл whileO прервется, не дойдя до функции put(), потому что последний
символ уже прочитан и записан в файл out при предыдущем проходе цикла.
Теперь попробуем написать программу копирования файлов. Исходя из того, что
до сих пор нам известно, выглядеть она будет так, как в листинге 12.9.
Листинг 12.9. Посимвольное копирование файлов
#include <fstream>
using namespace std;
int main(int argc. char **argv) {
char ch;
fstream in(argv[l]. ios::in);
fstream out(argv[2]. ios::out):
while(in.get(ch))
out.put(ch);
}
Если скомпилировать эту программу и проверить на реальных файлах, окажется,
что работает она весьма странно. Исходные тексты на C++ копируются верно, а от
исполняемых файлов (с расширением .ехе) остается только обрывок начала.
Все дело в том, что файлы по умолчанию открываются в так называемом
текстовом режиме. Это значит, что некоторые служебные символы при чтении
меняются, а другие (например, символ SUB, кодируемый числом 26) вообще означают
конец ввода. Символа SUB не бывает в текстовых файлах, но он часто встречается
в исполняемых. Вот почему их копирование не удается.
Чтобы выйти из затруднения, в C++ предусмотрена специальная константа ios::
binary, которая устанавливает бинарный режим чтения и записи файлов. В нем
копирование байтов происходит «безоглядно» до самого конца файла. Правильная
программа копирования показана в листинге 12.10.
Листинг 12.10. Посимвольное копирование бинарных файлов
#include <fstream>
using namespace std;
int main(int argc. char **argv){
char ch:
fstream in(argv[l]. ios::in | ios::binary);
fstream'out(argv[2].ios::out j ios::binary);
while(in.get(ch))
out.put(ch):
return 0:
}
1 См. раздел «Тайные преобразования» главы 9.
2 В разделе «Питание программ» главы 3 мы уже сталкивались с подобным выражением while(cin »
buf), но не имея достаточных знаний, просто говорили, что оно равно true или false в зависимости от
введенного символа.
Непоследовательные файлы
209
Задача 12.3. Перепишите программу из листинга 12.10 так, чтобы она проверяла,
достаточно ли параметров в командной строке, существуют ли копируемый файл
и файл-копия. Если выходной файл уже существует, нужно предупреждать о том,
что он будет уничтожен.
Программа из листинга 12.10 работает правильно, но обладает одним
недостатком. Она очень медленная. Копировать по байту за раз — большая
расточительность. Поэтому создадим напоследок еще одну команду копирования,
основанную на функциях readO и writeO, способных читать сразу несколько байтов
(листинг 12.11).
Листинг 12.11. Копирование файлов с использованием буфера
#include <fstream>
const int BSIZE - 100;
using namespace std;
int main(int argc. char **argv) {
char buf[BSIZE]:
int n:
ifstream in(argv[l].ios::in | ios::binary);
ofstream out(argv[2]. ios::out | ios::binary);
while(l) {
in.readCbuf. BSIZE);
n = in.gcountO:
if(n — BSIZE)
out.write(buf.BSIZE);
else {
out.write(buf.n);
in.closeO;
out.closeO;
break:
}
}
}
В программе из листинга 12.11 за функцией чтения сразу следует функция
gcountO, проверяющая, сколько байтов на самом деле прочитано. Если это число
равно размеру буфера, байты переписываются в выходной файл. Меньшее число
байтов означает, что достигнут конец файла и нужно дописать остаток в выходной
файл, закрыть оба файла и завершить программу.
Задача 12.4. Сравните скорости работы программ из листингов 12.10 и 12.11 при
копировании длинного (гораздо больше размеров буфера) файла.
Непоследовательные файлы
До сих пор мы «перемещались» по файлам, как по дорогам с односторонним
движением — только вперед. Прочитав, нулевой символ, можно было прочитать
только первый и никакой другой. То же самое относилось и к записи. Все это
напоминает одноразовую кассету без возможности перемотки. Посмотрел фильм — и
выбросил.
210
Глава 12. Ввод-вывод
Ясно, что «так жить нельзя», и в файлах должна быть возможность перемещения
назад и вперед в любое разумное место. К счастью, такая возможность присуща
файлам изначально, только мы о ней еще не знаем.
Оказывается, с каждым файлом связан флаг, указывающий на позицию, с
которой пойдет чтение или запись. В момент открытия файла флаг стоит в его начале.
После того как прочитан один символ, флаг перемещается на байт вперед, и это
означает, что можно читать, начиная со второго символа, или записывать с этого
места другие символы.
Этот флаг передвигается не только в результате операций чтения/записи.
Оказывается, есть две функции seekgO и seekpO, которые позволяют перемещать флаг
в пределах файла. Первая функция работает с объектами класса if stream— это
файлы, открытые для чтения (буква «g», стоящая в конце имени функции,
означает «get» — получать). Вторая используется объектами класса ofstream, то есть
файлами, открытыми для записи (буква «р», стоящая в конце имени функции,
означает «put» — класть). С объектами класса fstream могут работать обе
функции. Различные перемещения внутри файла демонстрирует программа из
листинга 12.12.
Листинг 12.12. Прогулки по файлу
#include <fstream>
#include <iostream>
using namespace std:
int mainO {
char ch;
fstream f;
f.open("test".ios::out); //открыть для записи
f « 123456789"; //записать символы
f.closeO; //закрыть
f.open("test".ios::in | ios::out):
f.seekgE. ios::beg);
f.get(ch);
cout « ch « endl; //5
f.seekg(-5. ios::end);
f.put('A');
f.seekg(-l. ios::cur);
f.get(ch);
cout « ch « endl; //A
f.closeO;
}
В листинге 12.12 сначала создается «пустой» объект f класса fstream: fstream f;.
Далее с этим объектом связывается файл test. Собственная функция орепО
управляется константой ios: :out, которая велит создать файл (если его еще нет) или
уничтожить содержимое старого файла. Затем в файл f записываются символы
«0123456789» и файл закрывается функцией close(). Но тут же открывается вновь.
На этот раз функцией open О управляют две константы ios:: in и ios: :out. Указав
их, мы открываем уже существующий файл для чтения и записи.
После второго открытия функция f .seekgE,ios: :beg) перемещает флаг на пятую
позицию от начала файла (о том, что перемещение идет относительно начала,
Непоследовательные файлы
211
говорит константа ios: :beg). На этой позиции стоит символ 5, который и
оказывается в переменной ch после чтения функцией f .get(ch). Следующая функция
f.seekg(-5,ios::end) перемещает флаг на пять позиций от конца файла. На это
указывает константа ios: :end. При перемещениях флага от конца файла нулевой
считается позиция сразу за последним символом. Так что флаг снова указывает
на символ 5, и функция f .put( 'A') записывает на место пятерки букву «А». После
записи флаг передвигается на шаг вдоль файла, указывая на шестую позицию, где
стоит символ 6. Чтобы прочесть только что записанный символ, необходимо
вернуться на шаг назад, что и делает функция f .seekg(-l,ios: :cur) (константа ios::
cur говорит о перемещении относительно текущей позиции). После этого флаг
указывает на символ «А», который и появится на экране.
Следя за перемещением флага, нужно понимать, что он сдвигается не обязательно
на одну позицию. Если читаются сразу четыре байта, флаг перемещается на
четыре позиции к концу файла. Рассмотрим интересный пример такого
перемещения — бинарный вывод чисел.
До сих пор мы занимались выводом символов. Если, скажем, в целочисленной
переменной i записано число 1234567, то инструкция out « i выведет в файл out
семь символов: 1,2,3,..., 7. Но если записать в файл содержимое компьютерной
памяти, которую занимает переменная i, там окажется всего четыре байта1, —
столько отводит наш компилятор целочисленной переменной. Если теперь переписать
эти байты из файла в память, занимаемую другой целочисленной переменной, то
в ней окажется то же число. Сказанное иллюстрирует листинг 12.13.
Листинг 12.13. Запись и чтение чисел
#include <fstream>
#include <iostream>
using namespace std:
int mainO {
int i = 1234567. j - 0:
fstream f;
f.openCtest". ios: :out);
f.closeO;
f.openCtest". ios::in | ios::out | ios: :binary);
f.write(reinterpret_cast<char*>(&i). sizeof(i)):
f.seekg(Dsizeof(int). ios::cur);
f.read(reinterpret j:ast<char*>(&j). sizeof(int)):
cout « j « endl:
f.closeO:
}
Открытие файла f в комментариях не нуждается, чего нельзя сказать об
инструкции записи f .writeO. Чтобы в ней разобраться, вспомним прототип функции:
write(char *.int);
1 Бинарный вывод экономит место в файле, если число превышает 9999 (подумайте, почему).
212
Глава 12. Ввод-вывод
Первый параметр — адрес начала последовательности байтов, второй — число этих
байтов. В нашем случае число равно размеру переменной int, то есть sizeof(int).
Но вот адреса начала последовательности из четырех байтов, где хранится
переменная int, у нас нет. Казалось бы, этот адрес равен &i, но &i — указатель на int,
а не на char. Чтобы получить из &i требуемый адрес, необходимо явное
приведение типов. Однако оператор static_cast<char *> в этом случае не подходит,
потому что преобразование указателя на int в указатель на'сИаг компилятор считает
опасным и просто отказывается это делать. Вот для таких рискованных
трансформаций и предназначен оператор reinterpret_cast<>(). Он— настоящий
волшебник, способный превратить указатель в целое число, а людоеда в мышь. Поэтому
пользоваться им следует с осторожностью.
Но в нашем случае никакого риска нет, и операция записи работает правильно.
В этом можно убедиться, прочитав число из файла и посмотрев его на экране. Для
этого нужно, прежде всего, вернуть указатель файла на sizeof(int) позиций назад,
что и делает инструкция:
f.seekg(- si zeof (i nt).i os::cur);
Далее файл читается инструкцией f .readО, причем для большей убедительности
содержимое файла переписывается не в исходную, а в другую переменную j. Ее
адрес вычисляется так же, как и при записи файла. Ну и, наконец, инструкция
cout « j « endl выводит на экран число 1234567 — то самое, что мы и ожидали
увидеть.
Любопытно посмотреть на содержимое получившегося файла. Для этого в
оболочке FAR файл test сначала выбирается, затем нажимается кнопка (или
клавиша) F3 (просмотр) и далее — F4 (показ в шестнадцатеричных кодах). Наше число
выглядит так:
87 D6 12 00
Чтобы узнать в нем число 1234567, нужно догадаться, что память компьютера
хранит числа в обратном порядке, то есть старший байт оказывается последним.
Поэтому для правильного восприятия число нужно «вывернуть наизнанку»: 00 12
D6 87. Подставив его в калькулятор и переведя в десятичное, получим ожидаемое
1234567.
Глава 13
Работа над ошибками
Assert и Throw
Программа, свободная от ошибок, есть
абстрактное теоретическое понятие.
Д. Ван Тассел. Стиль, разработка,
эффективность, отладка
и испытание программ
До сих пор мы почти не обращали внимания на возможные ошибки в
программе. Между тем, в программировании, как нигде, справедливо изречение «человеку
свойственно ошибаться». Если бы хирурги ошибались так же часто, как
программисты, операционные превратились бы в морги.
То, что безошибочных программ не бывает, знают все, кто работает на компьютере.
Быть может, это происходит потому, что борьба с ошибками — крайне нудное
занятие, требующее нечеловеческого педантизма. Между программой, работающей
только на одном компьютере, и программой, способной работать на всех
компьютерах, лежит пропасть.
Как же бороться с ошибками? Первое средство нам уже известно — это проверка
значений, возвращаемых собственными функциями, такими как fail О или eof О.
Второе средство под названием assert О очень удобно при отладке программ,
потому что сигнализирует о выходе параметров за допустимые границы. Внутри
круглых скобок помещается булево выражение, которое для успешного
выполнения программы должно быть истинным.
Рассмотрим в качестве примера программу (листинг 13.1), где проверяется время,
заданное для объекта класса myclock (см. о нем в разделе «Богатые и бедные
классы» главы 8).
Листинг 13.1. Выявление ошибок с помощью assertQ
#include <iostream>
#include <assert.h>
finclude "myclock5.hpp"
Продолжение &
214
Глава 13. Работа над ошибками
Листинг 13.1 (продолжение)
using namespace std:
void insptime (myclock &a) {
int s « a.getsO:
assert@ < s && s < 60);
}
int mainO {
myclock aA0.59.65);
insptime(a);
for(int i = 0; i < 1000; i++)
cout « ++a;
}
После создания объекта а, принадлежащего классу myclock, в программе из
листинга 13.1 вызывается функция insptimeO, проверяющая правильность задания
времени. По идее, эта функция должна быть собственной в классе myclock, тогда
бы ей легче было добраться до значений времени. Но у нас она для наглядности
сделана независимой.
В этой функции проверяются только секунды. Следующее условие означает, что
значение секунд, выходящее за указанные пределы @.59), прервет работу
программы1:
assert@ < s && s < 60):
Программа в этом случае аварийно завершится, показав на экране сообщение:
Assertion failed: s<60. file clockl.cpp. line 8
Чтобы программа могла продолжить работу, в C++ существует другой, гораздо
более сложный механизм обнаружения и исправления ошибок, связанный с
созданием «на лету» специальных объектов и передачей их функциям, которые
анализируют ошибку и принимают решения о дальнейшей судьбе программы. В
листинге 13.2 показана программа, использующая этот механизм для исправления
неправильно заданного времени.
Листинг 13.2. Создание и перехват объекта-ошибки
#include <stdexcept>
#include <iostream>
#include <excpt.h>
#include "myclock5.hpp"
using namespace std;
void insptime (myclock &a) {
int s = a.getsO;
if(s > 60){
a.putsE9);
throw range_error("Секунды исправлены");
}
}
int main(){
myclock aA0.59.65):
try {
1 Чтобы функция assertQ заработала, необходимо включить заголовочный файл assert.h.
Путь ошибки
215
insptime(a);
}
catch(runtime_error& г) {
cout « г.what О « endl;
}
fordnt i = 0: i<1000; i++)
cout « ++a;
}
В этой программе функция insptime О, прежде чем сообщить об ошибке,
исправляет неправильно заданные секунды. Сообщение об ошибке — это, по сути,
создание объекта стандартного класса гапдееггог:
throw range_error("Секунды исправлены"):
При создании объекта стандартного класса гапдееггог вызывается конструктор,
принимающий сообщение об ошибке.
Но одного создания объекта недостаточно для правильной работы программы.
«Подозрительную» область, где ошибка может встретиться, нужно пометить
конструкцией try{}. В нашем случае помечается вызов функции insptimeO.
И наконец, третий элемент системы — функция catchO, стоящая
непосредственно за блоком try{}. В нашем случае catch принимает ссылку на объект класса
runtime_error, а не класса гапде_еггог, создаваемого инструкцией throw range_
еггогО. Но гапде_еггог— класс, производный от runtime_error, поэтому
компилятор воспринимает его без возражений. То, что функция catchO принимает
ссылку на объект базового класса, позволяет ей отслеживать разные ошибки.
О том, какие классы порождает класс runtime_error, можно узнать в
заголовочном файле stdexcept.h.
Действует вся эта система очень просто: функция insptimeO исправляет
неверно заданные секунды, затем на экране появляется предупреждение «Секунды
исправлены», и часы начинают отсчет времени. Если нужно, выполнение программы
можно прервать, поместив внутрь блока catchO соответствующую инструкцию,
например exit A).
Путь ошибки
Объект, посылаемый с помощью throw, подобен всплывающей бомбе: если дать ей
достичь поверхности, она взорвется, и программа аварийно завершится. Но пока
бомба всплывает, многое еще можно поправить, и программа на C++ делает для
этого все возможное.
Как только включился особый режим, вызванный throw, выполнение
инструкций, следующих за throw, прекращается, и если в функции, где возникла ошибка,
нет блока try{}catch{}, происходит возврат в вызывающую функцию. Если и там
нет соответствующего catch{}, происходит возврат еще на уровень вверх. И так
ошибка поднимается до тех пор, пока не будет перехвачена. Если же поймать ее не
удастся, программа завершится. Во время охоты за ошибкой программа еще наде-
216
Глава 13. Работа над ошибками
ется, что катастрофы удастся избежать и поэтому, несмотря на то, что выполнение
обычных инструкций до перехвата ошибки прекращается, все деструкторы
объектов, подлежащих уничтожению при выходе из функций, исправно вызываются,
что иллюстрирует программа из листинга 13.3.
Листинг 13.3. Ошибка не мешает уничтожению объектов
#include <iostream>
using namespace std;
void fl(void);
void f2(void);
class xl {
public:
xl() {cout « "xl создан" « endl:}
~xl(){cout « "xl уничтожен" « endl;}
}:
class x2 {
public:
x2(){cout « "x2 создан" « endl:}
~x2(){cout « "x2 уничтожен" «endl:}
}:
void fl() {
cout « "вход в fl" « endl:
xl XI:
f2();
cout « "выход из fl" « endl: //не выводится
} // деструктор XI
void f2() {
cout « "вход в f2" « endl;
x2 X2;
throw stringC"Исключение в f2"):
cout « "выход из f2" « endl; //не выводится
} // деструктор Х2
int mainO {
try {
fl():
}
catch(string &message) {
cout « message « endl;
}
}
В ней ошибка искусственно вызывается внутри функции f2(), которая
вызывается функцией НО, а та в свою очередь вызывается функцией mainO. Поскольку
работа функции при возникновении ошибки прерывается, мы не увидим на
экране сообщения «выход из f2», стоящего после инструкции throw (message);. По той
же причине не появится на экране сообщение «выход из fl», которое выводит на
экран инструкция
cout « "выход из fl" « endl;
стоящая после вызова f2(). Но сообщения, выводимые конструкторами и
деструкторами объектов XI и Х2, мы на экране увидим. Результат работы программы
окажется таким:
Путь ошибки
217
вход в fl
xl создан
вход в t*2
х2 создан
х2 уничтожен
xl уничтожен
Исключение в f2
Автоматический вызов деструкторов при возникновении ошибки в программе
показывает, что throw создает аварийный объект не для того, чтобы завершить
программу, а чтобы продолжить. Поэтому нужно пытаться что-то сделать с ошибкой,
а не просто показать сообщение о ней в блоке catch{}. В программе из
листинга 13.2 нам удалось исправить ошибку там, где она возникла, сообщить об этом
и затем продолжить выполнение программы.
Но часто бывает, что внутри функции нельзя получить сведения, необходимые
для исправления ошибки. Тогда ошибку можно «поймать» в блоке «catch» и
перенаправить на более высокий уровень программы. Делает это инструкция throw
без аргументов. В программе из листинга 13.3 ошибку можно попробовать
перехватить и в функции fl():
void fl(){
cout « "вход в fl" « endl;
try {
xl XI:
f2();
}
catch (string &alarm) {
cout « "Я занят, зайдите на следующей неделе" « endl;
throw;
}
cout « "выход из fl" « endl:
}
Здесь блок catch() перехватывает ошибку, выводи; на экран сообщение и затем
пустой инструкцией throw: перенаправляет ее вверх, где она будет поймана еще
раз в функции mainO.
Поскольку объекты, создаваемые инструкцией throw, могут быть самыми разными,
поймать их в одном блоке catch О оказывается невозможно. Поэтому приходится
ставить на пути ошибок несколько заслонов:
try {
г
catch(){
)
catch(){
}
catch(...){
}
Порядок ловушек catch О, куда попадают ошибки, нужно продумывать заранее,
потому что ошибка может быть поймана только один раз. Как только нашлась
218
Глава 13. Работа над ошибками
подходящая функция catchO, ошибка обрабатывается, а все другие «ловушки»
пропускаются. Вот почему сначала нужно ловить частные ошибки (например,
объекты производных классов), а затем — более общие. Такой порядок
обработки позволяет правильней реагировать на ошибку. Последней в цепи «ловушек»
обычно стоит функция catch(...){}, где список параметров заменен многоточием.
Такая функция принимает все ошибки и служит «заглушкой» на их пути. Ошибку,
принятую этой функцией, вряд ли можно будет правильно обработать, но мы, по
крайней мере, будем знать о ней.
Провалы памяти
Если у вас нету тети,
то вам ее не потерять.
И если вы не живете,
то вам и не, то вам и не,
то вам и не умирать.
Из фильма «Ирония
судьбы», слова А. Аронова
До сих пор мы сами посылали сигналы бедствия с помощью инструкции throw. Но '
часто эти сигналы возникают сами по себе, потому что вызов throw может
произойти в стандартной функции или операторе. Когда, например, оператор new не в
состоянии выделить память, он создает с помощью throw объект bad_a11oc, который
нужно перехватить в блоке catch(){}.
Листинг 13.4. Перехват объекта bad_alloc
#include <iostream>
#include <exception>
using namespace std;
#include "matrix2a.hpp"
const int NOFCOLS = 10000;
const int NOFROWS - 20000;
int mainO {
try {
int i.j:
matrix a(NOFROWS.NOFCOLS);
aB.2)-10:
double &m - aB.2);
cout « m « endl: // 10
m - 5;
cout « aB.2) « endl;
}
catch(exception & ex) {
cout « ex.what О « endl:
exit(l);
}
}
Провалы памяти
219
В программе из листинга 13.4 делается попытка создать объект класса matrix,
хранящий 20 000x10 000 переменных типа double. Если учесть, что каждая
переменная double занимает 8 байт, то получится, что объект matrix требует примерно 1,5
гигабайта памяти. Число не фантастическое, ноу моего компьютера памяти почти
в 10 раз меньше. Значит, в конструкторе объекта а класса matrix1 возникнет сигнал
бедствия, который перехватывается обычным способом:
matrixCint г = 2. int с = 2) {
nrows_=r:
ncols_=c:
try {
mini_=new double[r*c]:
} catch(exception & ex){
cout « ex.what О « endl:
throw;
}
}
Блок catch (){} может принимать ссылку на объект badalloc, но у нас он
принимает объект базового класса exception. Описание класса exception (вместе со своими
подклассами) находится во включаемом файле exception. Все классы,
производные от exeption, используют виртуальную функцию what(), которая показывает на
экране имя реального объекта, рожденного throw. В нашем случае функция what О
дважды покажет на экране2:
St9bad_alloc
St9bad_alloc
потому что объект bad_al loc в конструкторе класса matrix посылается дальше
простой инструкцией throw и еще раз перехватывается в функции mainO, которая тут
же прекратит работу, перейдет к блоку catchO и, показав на экране имя ошибки,
завершится.
Пример, который мы только что разобрали, интересен еще и тем, что создаваемый
объект matrix оказывается после случившегося бедствия (невозможности
выделить память) в «полуживом» состоянии: часть переменных уже создана, часть —
нет. Поэтому непонятно, что будет с ним после инструкции throw, вызванной
внутри конструктора.
Если бы объект был полностью создан, то он был бы уничтожен деструктором
перед выходом из функции mainO. Но раз конструктор не выполнился полностью
(мы не достигли закрывающей фигурной скобки }), то считается, что «если вы не
живете, то вам и не умирать». Деструктор такого объекта не вызывается.
Но это не значит, что при выходе из функции какой-то участок памяти останется
занятым. Для всех объектов, чьи конструкторы успешно отработали, будут
вызваны соответствующие деструкторы и в результате все захваченные у компьютера
ресурсы будут вовремя освобождены.
1 Описание этого класса хранит файл matrix2a.hpp.
2 Такие сообщения показывает программа, скомпилированная с помощью gcc. Если использовать
другой компилятор, сообщение может быть иным.
220
Глава 13. Работа над ошибками
Правда, все это относится к создаваемым объектам. Если же с помощью оператора
new присваивается начальное значение указателю на объект, то станет возможна
потеря памяти, если ошибка возникнет между выделением памяти и ее
освобождением.
Пусть, например, в некой функции выделяется память для массива
целочисленных переменных, и адрес нулевого элемента массива посылается в указатель pi.
Такая память должна быть освобождена вручную, поэтому перед выходом из
функции должна стоять инструкция del ete:
int *pi = new int[N];
delete [] pi:
А теперь представим себе, что между выделением и освобождением памяти была
выполнена инструкция throw, прерывающая выполнение программы. В этом
случае программа перескочит через инструкцию delete [] pi; и сразу запустит
деструкторы уже созданных объектов. В результате массив исчезнет из нашего поля
зрения, но память, занимаемая им, не освободится.
Против этого есть, казалось бы, уже испытанное средство: достаточно «поймать»
ошибку в блоке try{}catch(), освободить память и передать объект-ошибку
«дальше по цепочке» с помощью инструкции throw без аргументов:
int *pi = new int[N];
try {
...//выполнить все. что нужно, и не бояться ошибок
}
catch(...) {
delete [] pi;
throw; //передать ошибку дальше
}
delete [] pi:
Беда только в том, что программа, усеянная блоками try{}catch(){}, станет при
этом абсолютно нечитаемой. Поэтому гораздо красивее выглядит такая идея: если
«настоящие» объекты C++, снабженные конструктором и деструктором,
автоматически создаются и уничтожаются, то сделаем указатель таким объектом,
поместив его в оболочку class{};. Листинг 13.5 показывает, как пользоваться
указателем, помещенным внутрь объекта C++ (такие указатели называют «умными»).
Листинг 13.5. Пример «умного» указателя
#include<iostream>
using namespace std;
tempiate<typename T>
class ArrPtr {
public:
ArrPtr (int size) {
p_ - new T[size];
}
-ArrPtr () {delete [] p_;}
T & operator^) const { return *p_: }
T * operator+(int offs) const {
Провалы памяти
221
return (p_ + offs):
}
Т & operator[](int offs){return *(p_ + offs);}
private:
T * p_:
}:
int mainO {
ArrPtr<double> ipA0);
*(ip + 4) - 5.0;
ip[5] - 34.0;
cout « ip[4] « endl; // 5
cout « ip[5] « endl; // 34
}
Основой «умного» указателя служит обычный указатель р_, помещенный в
область private класса ArrPtr. Чтобы можно было «указывать» на элементы любых
массивов, использован шаблон, в котором Т обозначает тип элемента массива (char,
int, double) или какой-то другой.
Чтобы объектом класса ArrPtr можно было пользоваться как обычным
указателем, нужно, очевидно, определить операторы, которые применимы к указателям.
Прежде всего, это оператор раскрытия ссылки *, оператор произвольного доступа
к элементу массива [] и оператор прибавления к указателю целого числа +. Для
полного сходства с указателем потребуется определить операции сложения и
вычитания указателей, а также было бы хорошо как-то защититься от неверного
индекса в операторе [].
Инструкции в функции mainO показывают, что объект ip класса ArrPtr не
отличается от указателя на нулевой элемент массива переменных double. Но в
отличие от «настоящего» указателя, этот объект снабжен деструктором и потому будет
правильно уничтожен, если в функции, где он объявлен, вдруг возникнет сигнал
бедствия throw.
Заключение
Учитель истории упрекал ученика за то, что
тот не знает ни одной даты... Напротив, —
отвечал ученик, — я знаю все даты и могу
их прямо сейчас перечислить. Я только не
знаю, что в эти даты происходило.
Анекдот
Перефразируя гениальный ответ ученика, можно сказать, что мы теперь знаем все
основные понятия объектно-ориентированного программирования (ООП), нам
только не известны их названия.
Настало время систематизировать основные идеи ООП1, что, безусловно,
поможет не только лучше понять C++, но и освоить другие объектно-ориентированные
языки, такие как Smalltalk, Java и Eiffel.
Начнем с объектно-ориентированного подхода — системы идей, касающихся
общих методов познания, применимых не только в программировании, но в любой
научной дисциплине — химии, ботанике, социологии.
Объектно-ориентированный подход зиждется на трех китах: классификации, специализации, полиморфизме.
Классификация — есть способность выделить группы объектов с одинаковыми
свойствами и операциями. Люди способны классифицировать почти
инстинктивно. Мы легко отличаем стул от стола, раковину от ванны, спам от полезных
сообщений и даже ящик стола от корзины для мусора.
Специализация — есть способность продолжить классификацию дальше, то есть
из класса животных выделить насекомых, млекопитающих и т. д., из
млекопитающих выделить человекообразных, а из них — вид homo sapience.
Полиморфизм — есть способность разных объектов по-разному реагировать на
одинаковые действия. Например, загрузка файла в текстовый редактор
отличается от загрузки файла в редактор графический, потому что форматы текстовых
и графических файлов различны. Но действия при этом выполняются
одинаковые: в меню Файл выбирается пункт Открыть.
I При подготовке этого раздела использовалась статья Ashley M. Aitken «Object Orientation Revealed!».
II Ргос. 10 th Australasian Conference on Information Systems, 1999.
Заключение
223
После определения основных понятий объектно-ориентированного подхода
перейдем к объектно-ориентированному программированию. Нужно сказать, что
основные идеи объектного подхода по-разному реализуются в разных языках. Мы,
конечно, будем опираться на C++.
Начнем со средств, позволяющих реализовать классификацию. Очевидно,
классификация невозможна, если язык программирования не способен создать модель
объекта. Так вот, механизм создания таких моделей называется инкапсуляцией.
Инкапсуляция — это способность языка создать модель объекта, в которой
соединены его свойства и поведение. Такой моделью в языке C++ служит прототип
объекта, задаваемый словом class. Причем свойства объекта связаны с его
данными, а поведение с собственными функциями.
Второе свойство языка, необходимое для выделения объектов, называется
сокрытием данных. Вспомним про области private в классах, куда помещаются
элементы данных, а также функции, выполняющие внутренние для объекта операции.
Не будь возможности сокрытия данных, полноценная реализация объектов, а
значит, и их классификация, стали бы невозможными. Без сокрытия данных
объекты теряют обособленность, становятся как бы прозрачными и общедоступными.
Сокрытие данных поддерживает целостность объекта.
Сокрытие данных приводит к тому, что объекту нужно передавать сообщения, на
которые тот должен реагировать. Действительно, должен же быть какой-то доступ
к скрытым данным, иначе они окажутся просто невидимыми. Сообщения,
принимаемые объектом, определяют его поведение, которое не должно меняться при
изменениях в скрытых данных и функциях.
Таковы средства языка, обеспечивающие классификацию. Они не так
очевидны, если сравнить их с наследованием (см. главу 10), безусловно созданным для
поддержания специализации. Можно, конечно, с помощью механизма
наследования смешать бульдога с носорогом, но смысл наследования все-таки в уточнении,
когда методы базового класса естественно применяются классом производным.
Именно таков оператор доступа О, использованный в разделе «Составные
объекты» главы 10.
И наконец, упомянем полиморфизм, богато представленный в C++. Прежде
всего вспомним о переопределении функций. Оказывается, в C++ можно заготовить
несколько одноименных функций и потом не беспокоиться о выборе нужной,
потому что компилятор сам определит, какую вызвать, по ее аргументам. Такого же
эффекта можно добиться использованием шаблонов. При таком подходе
полиморфное поведение достигается тем, что компилятор сам создает функцию, если,
конечно, находит подходящий шаблон (см. раздел «Функции-тезки» в главе 4).
Создать полиморфные объекты помогают операторные функции, о которых
рассказывается в главе 9. Определив в разных классах операторную функцию для
оператора +, можно писать с=а+Ь, не задумываясь о том, что такое а и b — матрицы,
комплексные числа, массивы или что-то еще.
Полиморфизм обеспечивают также шаблоны классов (см. раздел «Шаблоны
классов» в главе 11). Вспомним контейнерные типы, в которых многие действия вы-
224
Заключение
полняются одинаково для разных типов объектов. Например, сортировка в
контейнере выполняется одинаково, если для всех разнородных объектов правильно
реализован оператор сравнения.
С помощью шаблонов, а также операторных и одноименных функций реализуется
так называемый статический полиморфизм, называемый еще статическим
связыванием, потому что класс или функция создаются во время компиляции, до
запуска программы.
В противоположность статическому динамическое связывание происходит во
время исполнения программы. Пример можно найти в разделе «Изменчивость и
отбор» главы 10. Там, как вы помните, в массив указателей на базовый класс
записывались указатели как на базовый, так и на производный классы. Если с помощью
таких указателей вызывать собственные функции, одинаково называемые как
в базовом, так и в производном классах, то вызывается функция,
соответствующая адресу объекта. Для объекта производного класса вызывается своя функция,
для объекта базового класса — своя.
Динамическое связывание считается фирменной особенностью
объектно-ориентированного программирования. Во многих языках оно действует по
умолчанию. Но в C++ для его запуска нужно пометить функцию базового класса словом
virtual.
Как видите, основные понятия ООП, хотя и выглядят несколько вычурно, по сути
своей просты. Их знание понадобится вам при чтении книг по C++, а также
поможет освоить другие объектно-ориентированные языки.
Приложение А
Приоритеты и порядок
выполнения операторов
В следующей таблице перечислены операторы C++, расставленные в порядке
убывания приоритета.
Оператор Порядок выполнения
:: (глобальный) Слева направо
:: (для класса)
() Слева направо
[]
->
++ (постфиксный)
-- (постфиксный)
dynamic_cast<mwH>(e)
static_cast<mww>(e)
reinterpret_cast<mMH>(e)
const_cast<mwrc>(e)
++ (префиксный) Справа налево
-- (префиксный)
!
& (получение адреса)
sizeof
+ (унарный)
- (унарный)
*(указатель)
delete
new (тип) е
.* и ->* Слева направо
Продолжение &
226
Приложение А. Приоритеты и порядок выполнения операторов
Порядок выполнения
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Слева направо
Справа налево
Справа налево
Слева направо
Слева направо
Оператор
*. /и*
+ и -
« и »
<, <«, «> И ~
= и ! =
&
I
&&
11
?:
S/ += .=/ *=/ /, ^ »= «= &S/ ^, и |,
Throw (e)
, (запятая)
Приложение Б
Устройство целочисленных
переменных
В этом приложении мы продолжим изучение внутреннего устройства
целочисленных переменных, начатое в главе 1. Полученные знания помогут нам
объяснить неожиданные результаты некоторых арифметических действий.
Итак, попробуем понять, каким образом хранятся целые числа. Для простоты
предположим, что в переменной всего 4 бита. Таких маленьких переменных на самом
деле не бывает, но принцип хранения чисел одинаков и не зависит от числа бит.
В первой главе мы узнали, что четырехбитовая переменная может находиться в 16
различных состояниях, и сейчас попробуем перебрать все эти состояния (каждое
их которых представляется в виде двоичного числа), начав с нуля и получая
каждое последующее число прибавлением единицы к предыдущему.
Итак, сначала в переменной записан ноль:
0 0 0 0// число ноль
Прибавив к нему единицу, получим единицу в двоичном коде:
0 0 0 1
Чтобы прибавить следующую единицу, нужно знать, как складываются двоичные
числа. Как и десятичные, двоичные числа складываются поразрядно, начиная с
младшего разряда. Причем правила сложения в пределах одного разряда, крайне просты:
0 + 0 = 0
0 + 1 = 1
1 + 0=1
1 + 1=0A в уме).
228
Приложение Б. Устройство целочисленных переменных
Значит,
0001 + 0001 = 0010
Складываем единицы в младших разрядах, ноль пишем, один в уме. Затем
складываем нули в следующих разрядах, получаем ноль плюс запомненная единица.
Итог: единица во втором разряде.
Результат последовательного прибавления единиц показан на рис. Б.1.
Рис. Б.1. Так хранятся целые без знака
Двоичным кодам, показанным на рисунке, сопоставлены целые неотрицательные
числа от 0 до 15. То есть на рис. Б.1 показана беззнаковая переменная unsigned.
Числа идут по кругу не случайно. Если прибавить к 15 (в двоичном коде это
1111) единицу, получится совсем не 16, а ноль! Действительно, двоичное число
16 A0000) нельзя уместить в 4-х битах. Когда к числу 1111 прибавляется 1, все
биты «валятся», оставляя за собой одни нули. То же самое будет происходить
с любыми целочисленными переменными, только числа (в зависимости от
размера переменной) будут другими. Если переменная типа unsigned char, то ее
предельное значение равно 255. Для переменной unsigned char справедливо равенство:
255+1 = 0. А дальше все идет по кругу: 0, 1, 2, 3,..., 255, 0,1...
Теперь нам должно быть понятно, почему не работает программа, показанная
в листинге Б.1.
Листинг Б.1. Вечный цикл
#iinclude <iostream>
using namespace std:
mainO {
unsigned char i:
ford - 0: i <= 255: i++)
cout « i « endl;
}
Приложение Б. Устройство целочисленных переменных
229
По замыслу эта программа должна вывести на экран все числа от нуля до 255 и
затем прекратить работу. На самом же деле она будет работать вечно, выводя на
экран все числа от 0 до 255 вновь и вновь.
Весь фокус в том, что переменная i объявлена как unsigned char, а мы знаем, что
диапазон ее значений — от нуля до 255. Казалось бы, условие i<=255 показывает,
что i меняется в допустимых пределах. Но давайте посмотрим, что будет с
циклом, когда i достигнет 255. Условие i <= 255 в этом случае выполнится, объект cout
покажет на экране число 255, а дальше i увеличится на единицу и снова будет
проверено условие i<=255. Чему будет равно i в этот момент, мы уже знаем: 255 + 1
означает для переменной unsigned char ноль! Условие i <= 255 будет выполнено,
и цикл будет прокручен снова 256 раз, затем i снова станет равна нулю и так до
бесконечности.
С помощью чисел, показанных на рис. Б.1, можно записать любые
последовательности бит. Например, 16-битовое число, в котором все биты равны единице,
можно записать так:
15 15 15 15
Здесь каждое число 15 обозначает 4 идущих подряд единичных бита: 1111. При
такой записи приходится ставить пробелы после каждой цифры, потому что
некоторые комбинации бит требуют одной цифры (например, число 3 обозначает
биты ООП), а некоторые — двух (число 10, например, обозначает биты 1010). Для
удобства каждая цифра, большая девяти, заменяется буквой. Например,
латинская В соответствует битам 1011 или (в десятичном представлении) числу 11.
При таких обозначениях число, в котором все 16 бит равны 1, можно записать как
FFFF — пробелы теперь не нужны, потому что каждой комбинации из 4 бит
соответствует один символ (см. рис. Б.1).
Такая запись чисел называется шестпадцатеричной, она очень компактна и часто
используется, когда программиста интересует не значение числа, а расположение
его бит. В языке C++ шестнадцатеричные числа записываются специальными
константами, которые начинаются символами Ох :. Программа, показанная в
листинге Б.2, напечатает число 255 — именно такое десятичное число соответствует
восьми идущим подряд единичным битам.
Листинг Б.2. Использование шестнадцатеричного кода
#include <iostream>
using namespace std;
main(){
int i:
i = OxFF;
cout « i « endl: // 255
}
Поняв устройство переменных без знака, посмотрим, как кодируются переменные
со знаком. Соответствующая круговая диаграмма показана на рис. Б.2.
230
Приложение Б. Устройство целочисленных переменных
Рис. Б.2. Числа со знаком
Четыре бита, как ни крути, могут находиться лишь в 16 разных состояниях. Но
теперь этим состояниям соответствуют другие числа. Любопытно, что за семеркой
сразу следует -8 A000). Далее идут -7 A001), -6 A010), -5 A011), и все
кончается нулем. Такой способ кодирования поначалу может показаться странным, но на
самом деле он очень удобен, потому что позволяет заменить вычитание числа на
прибавление такого же числа, но с противоположным знаком. Например, 5 - 4 =
= 5 + (-4) = 0101 +1100 = 0001 = 1. На самом деле, при сложении чисел 5 @101)
и -4 A100) получается 17, то есть в двоичной записи 10001. Но в нашей
переменной всего 4 бита, и старшая единица вытесняется за ее пределы, остается только
единица в младшем разряде. Вот и получается, что 5 + (-4) = 1.
Легко заметить, что отрицательное число получается, если из 16 вычесть такое
же по абсолютной величине, но положительное число. Чтобы, например, получить
код для числа -7, нужно из шестнадцати вычесть семь:
16 - 7 - 9 - 1001
Можно еще сказать, что отрицательное число получается дополнением до
шестнадцати соответствующего положительного. Например, -7 — это дополнение
числа 7 до 16, то есть 9 (в двоичном представлении — 1001).
Теперь понятно, что сумма положительного числа и равного ему по абсолютной
величине отрицательного (например, 6 + (-6)) всегда равна 16. Это число в
двоичной записи равно 10000 и занимает 5 бит. Но в нашей переменной четыре бита,
и старший разряд «сваливается» с ее левого конца, оставляя четыре нуля, что
и требуется.
Способ кодирования отрицательных четырехбитовых чисел легко обобщается на
любое число бит. Если переменная 8-битовая, отрицательные числа будут
получаться дополнением положительных до 28 = 256, шестнадцатибитовые
переменные будут кодироваться дополнением до 216 = 65536, а тридцатидвухбитовые —
дополнением до 232.
Приложение Б, Устройство целочисленных переменных
231
Посмотрим, например, какие числа при таком способе кодирования могут
храниться в знаковой переменной int, занимающей 4 байта или 32 бита. Максимальное
положительное число будет, очевидно, равно:
0111 1111 1111 1111 1111 1111 1111 1111
или в десятичном представлении 2147483647. Максимальное отрицательное
число, которое умещается в 32 битах, будет по аналогии с 4-битовыми числами
равно:
1000 0000 0000 0000 0000 0000 0000 0000
Как и в случае четырех бит, оно по абсолютной величине на единицу больше
максимального положительного, то есть это -2147483648. Правило дополнения
действует, конечно, и в случае 32 бит. Так, например, сумма 2147483647 и - 2147483647
равна сумме двоичных чисел
0111 1111 1111 1111 1111 1111 1111 1111 B147483647)
+
1000 0000 0000 0000 0000 0000 0000 0001 (-2147483647)
10000 0000 0000 0000 0000 0000 0000 0000 = 232
Приложение В
Стандартная библиотека
Стандартные контейнеры
Есть два типа контейнеров: последовательные и ассоциативные. В
последовательных контейнерах место включаемого объекта не зависит от его величины. В
ассоциативных контейнерах объекты всегда упорядочены, поэтому место, куда
включается объект, зависит от его величины.
Последовательные контейнеры
Строка
#include<stnng>
string
Этот контейнер представляет собой последовательность символов char. Быстрый
доступ к любому элементу.
Вектор
#include<vector>
vector
Напоминает массив, но может «на ходу» менять размер. Объекты хранятся в
контейнере vector плотно, один за другим. Поэтому добавление объектов в конец
контейнера можно сделать быстро. И так же быстро удалить их оттуда. Добавление
в любое другое место (удаление из любого другого места) идет медленно (время
пропорционально размеру контейнера). Быстрый доступ к любому элементу.
Двусторонняя очередь
#include<deque>
deque
Единственное отличие от вектора в том, что позволяет быстро добавлять
элементы как в начало, так и в конец. Так же быстро удаляет первый и последний
элементы.
Адаптеры
233
Список
#include<list>
list
В этом контейнере каждый элемент связан только с ближайшими соседями —
с предыдущим и последующим. Чтобы добраться до элемента списка, нужно
пройти все предыдущие элементы. Элементы списка не хранятся в памяти плотно, без
зазоров, как элементы вектора. Список обеспечивает быструю вставку (удаление)
элемента, независимо от их положений.
Ассоциативные контейнеры
set и multiset
#include<set>
set
multiset
В контейнерах set и multiset элементы «на лету» сортируются по определенному
критерию. Так что составляющие set и multiset объекты всегда расставлены по
порядку. Отличаются они друг от друга тем, что multiset может хранить одинаковые
объекты, a set — нет.
тар и multimap
#include<map>
тар
multimap
Контейнеры map и multimap хранят пары объектов, которые автоматически
сортируются по первому объекту пары (ключу), тар не может хранить объекты с
одинаковыми ключами, multimap — может.
Для контейнеров set, multiset, map, multimap время вставки, удаления и поиска
пропорционально логарифму числа элементов.
Адаптеры
Адаптеры — это «надстройки» над стандартными контейнерами. Адаптеры не
имеют своих итераторов и потому не могут использоваться алгоритмами.
Очередь
#include<queue>
queue
Элементы, добавленные в очередь первыми, первыми и извлекаются.
priority_queue
234
Приложение В. Стандартная библиотека
Элементы, добавляемые в priority_queue, постоянно сортируются (по умолчанию
используется оператор <). Поэтому извлекается самый «большой» на данный
момент объект.
Стек
#include<stack>
stack
Первыми из стека извлекаются объекты, вошедшие в него последними.
Типы объектов
Собственные функции и алгоритмы, связанные с контейнерами, используют
следующие типы объектов:
¦ iterator — итератор;
¦ poi nter — указатель;
¦ reference — ссылка;
¦ sizetype — целое без знака;
¦ di fference_type — целое число со знаком для представления разности
итераторов;
¦ val ue_type — тип объекта, хранимого к контейнере.
Для контейнера, поддерживающего двунаправленные1 итераторы:
¦ reverse_iterator — обратный итератор. Такой итератор направляется
оператором ++ в обратном направлении — от последних элементов контейнера к
первым;
¦ const_reverse_iterator — обратный итератор, только для чтения.
Для ассоциативных контейнеров:
¦ key_type — тип первого элемента пары в контейнерах тар и multimap;
¦ val ue_compare — функция, сравнивающая два объекта val uetype.
Конструкторы и деструкторы
Для всех контейнеров:
¦ contained) — создает пустой контейнер;
1 Двунаправленный итератор используется для движения по контейнеру в любом направлении. Такой
итератор можно увеличивать или уменьшать. Но бывают итераторы, с помощью которых можно
двигаться по контейнеру только в одном направлении.
Собственные функции
235
¦ container(const container &that) — создает контейнер, копируя все элементы
контейнера that.
Для последовательных контейнеров:
¦ container(size_type n) — создает вектор из п элементов, каждый из которых
создается конструктором по умолчанию;
¦ container(size_type n, const value_type &x) — создает контейнер, в котором п
копий объекта х;
¦ container (iterator begin, iterator end) — создает контейнер из элементов,
задаваемых итераторами [begin, endI.
Для ассоциативных контейнеров:
¦ container (key_compare compare) — создает пустой контейнер, использующий
key_compare для сравнения элементов;
¦ container (iterator begin, iterator end,key_compare compare) — создает
контейнер из элементов, задаваемых итераторами [begin, end) с функцией сравнения
compare.
Собственные функции
Собственные функции, общие
для всех контейнеров
¦ i terator begi n () — возвращает итератор, указывающий на нулевой элемент
контейнера;
¦ iterator end() — возвращает итератор, указывающий на элемент, стоящий
непосредственно за последним элементом контейнера;
¦ void clearO — уничтожает все элементы контейнера;
¦ boo! emptyO const — возвращает true, если контейнер пуст;
¦ erase(iterator p) — уничтожает элемент, на который указывает р. В случае
последовательных контейнеров возвращает итератор, указывающий на элемент,
стоящий непосредственно за удаленным или end(). В случае ассоциативных
контейнеров возвращает void;
¦ erase (iterator first, iterator last) — уничтожает элементы в интервале,
задаваемом итераторами [first, last). В случае последовательных контейнеров
возвращает позицию следующего за удаленными элементами. В случае
ассоциативных контейнеров возвращает void;
1 Круглая скобка [...) означает открытый интервал, то есть элемент, на который указывает end, не
включается в контейнер.
236
Приложение В, Стандартная библиотека
¦ sizetype max_size() const — возвращает максимальное число элементов,
которое способен хранить контейнер;
¦ container &operator=(const container &that) — уничтожает все элементы
текущего контейнера и затем копирует все элементы контейнера that;
¦ reverse_iterator rbeginO — возвращает обратный итератор, указывающий на
последний элемент контейнера;
¦ reverse_iterator rend () — возвращает обратный итератор, указывающий на
элемент, предшествующий нулевому элементу контейнера;
¦ size_type sizeO const — возвращает число элементов в контейнере;
¦ void swap(const container &that) — если cl и с2 — два контейнерных типа, то
вызов cl. swap(c2) меняет местами содержимое контейнеров, то есть содержимое cl
оказывается в с2, а содержимое с2 — в cl
Другие собственные функции
В квадратных скобках указаны контейнеры, для которых эти функции
определены.
[string, deque, vector]
reference at(size_type n) — возвращает ссылку на элемент под номером п.
[deque, list, vector]
reference back О — возвращает ссылку на последний элемент контейнера.
[deque, list, vector]
reference front О — возвращает ссылку на нулевой элемент контейнера.
[string, deque, vector]
reference operator [] (size_type n) — возвращает ссылку на элемент п.
[map]
reference operator [] (const key_type& key) — возвращает ссылку на значение,
соответствующее ключу key. Если такого ключа нет, создается новый элемент.
[deque, list, vector]
void pop_back() — уничтожает последний элемент контейнера.
[deque, list]
void pop_front() — уничтожает нулевой элемент контейнера.
[string, deque, list, vector]
void push_back(const valuejtype &x) — вставляет на последнее место в контейнере
элемент х.
Собственные функции
237
[deque, list]
void push_front(const value_type &x) — вставляет элемент х на нулевое место в
контейнере.
[list]
¦ void sort О — сортирует список, пользуясь для сравнения элементов
оператором <;
¦ void sort(op) — сортирует список, пользуясь для сравнения элементов
функцией ор().
Собственные функции
последовательных контейнеров
¦ void assign(iterator first, iterator last) — заменяет содержимое контейнера
элементами, на которые указывают итераторы в интервале [first,last);
¦ void assign(size_type n, value_type value) — вместо старых элементов
контейнера ставится п копий val ue;
¦ iterator insert(iterator p, const valuejtype &x) — вставитьхнепосредственно
перед элементом, на который указывает р. Возвращает итератор, указывающий
нах;
¦ void insert (iterator p, sizetype n, const valuejtype &x) — вставить п копий х
перед элементом, на который указывает р;
¦ void insert(iterator p, iterator first, iterator last) — вставляет элементы,
на которые указывают итераторы в интервале [f i rst, 1 ast) перед элементом, на
который указывает р.
Собственные функции
ассоциативных контейнеров
¦ sizejtype count(const key_type &k) const — возвращает число элементов с
ключом1, равным к;
¦ pair<iterator, iterator^ equal_range(const key_type k)const — возвращает
первую и последнюю позиции элементов с ключом к;
¦ iterator lower_bound(const key_type k) const — возвращает позицию первого
элемента, у которого ключ >- к;
¦ iterator upper_bound(const key_type k) const — возвращает позицию первого
элемента, у которого ключ > к;
¦ sizetype erase(const keytype &k) — удалить все элементы с ключом к,
возвращает число удаленных элементов;
1 Для контейнеров set и multiset ключ — это сам элемент контейнера.
238
Приложение В. Стандартная библиотека
¦ iterator find (const key_type& k) — возвращает итератор, указывающий на один
из элементов с ключом к или end() — в случае, когда элемент не найден;
¦ insert (const valuejtype &x) — вставляет в контейнер элемент х, возвращает
итератор, указывающий на только что вставленный элемент; если контейнер
поддерживает только уникальные ключи, возвращает пару pair<iterator, boo!>,
в которой iterator указывает на элемент, равный х, а переменная boo! равна
true, если элемент вставлен, и false — если элемент уже есть в контейнере;
¦ iterator insert (iterator p, const value_type &x) — вставляет в контейнер
объект х, — р содержит «догадку» о месте, куда этот элемент нужно вставить.
¦ insert(iterator first,iterator last) — берет объекты в интервале [first, last)
и вставляет их в контейнер;
¦ key_compare key_comp() const — возвращает функцию, сравнивающую ключи.
Алгоритмы
Алгоритмы, не меняющие контейнеры
¦ difference_type count(iterator first, iterator last, const valuejtype &value) —
в интервале, определяемом итераторами [first, last), находит число
элементов, равных value;
¦ difference_type countjif(iterator first, iterator last, bool pred(value_type)) —
в интервале, определяемом итераторами [first,last), возвращает число
элементов, для которых функция predO возвращает true;
¦ указанные ниже алгоритмы находят минимальный или максимальный элемент
в интервале, заданном итераторами [first,last). В первом варианте алгоритма
используется оператор <, во втором — функция ор(), принимающая два объекта
и возвращающая true, когда первый «меньше» второго:
¦ iterator min_element(iterator first, iterator last);
¦ iterator min_element(iterator first, iterator last; bool opO);
¦ iterator max_element (iterator first, iterator last);
¦ iterator max_element(iterator first, iterator last, bool opO);
¦ iterator find(iterator first, iterator last, valuejtype const &value) —ищет value
в интервале, заданном итераторами [first, last), в случае удачи возвращает
первый итератор, указывающий на элемент, равный value, в противном
случае — возвращает 1 ast;
¦ iterator find_if(iterator first, iterator last, bool predO) — в интервале,
заданном итераторами [first, last), ищет первый элемент, для которого функция
predO равна true. В случае удачи возвращает указатель на этот элемент, в
противном случае — 1 ast;
Алгоритмы
239
iterator search_n(iterator first, iterator last, sizejtype count, value jtype const
&value) и iterator searchji (iterator first, iterator last, sizejtype count, value_
type const &value, bool opO) — в интервале [first, last) ищут count идущих
подряд элементов value. Если поиск удачен, возвращается итератор, указывающий
на начало последовательности. Если нет — возвращают 1 ast. В первой версии
алгоритма для сравнения используется оператор —, во второй — элементы
считаются равными, если op(elem, value) равна true;
iterator search(iterator firstl, iterator lastl, iterator first2, iterator last2)
и iterator search(iterator firstl, iterator lastl, iterator first2, iterator last2,
bool opO) — поиск последовательности элементов, заданной итераторами
[first2, last2) в интервале [firstl, lastl). Алгоритм возвращает итератор,
указывающий на начало первой такой последовательности. Если совпадение не
находится, возвращает lastl. В первом варианте алгоритма для определения
равенства используется оператор —, во втором — элементы считаются
равными, когда функция op(eleml,elem2) возвращает true;
iterator find_end(iterator firstl, iterator lastl, iterator first2, iterator last2)
и iterator find_end(iterator firstl, iterator lastl, iterator first2, iterator
last2, bool opO) — поиск последовательности элементов, заданной
итераторами [first2, last2) в интервале [firstl, lastl). Алгоритм возвращает итератор,
указывающий на начало последней такой последовательности. Если
совпадение не находится, возвращает lastl. В первом варианте алгоритма для
определения равенства используется оператор =, во втором — элементы считаются
равными, когда функция op(eleml,elem2) возвращает true;
iterator findjfirst_of(iterator firstl, iterator lastl, iterator first2, iterator
last2) и iterator find_first_of (iterator firstl. iterator lastl, iterator first2,
iterator last2, bool opO) — возвращают итератор, указывающий на первый
элемент в интервале [firstl, lastl), найденный в интервале [first2, last2). Если
таковой не находится, возвращают lastl. В первом варианте алгоритма для
определения равенства используется оператор ==, во втором элементы считаются
равными, когда функция op(eleml,elem2) возвращает true;
iterator adjacent_find(iterator first, iterator last) и iterator adjacent_find_
if (iterator first, iterator last, bool opO) — возвращают итератор,
указывающий на первый элемент в интервале [first, last), равный следующему за
ним элементу. В алгоритме adjacentfind равенство определяется
оператором =, в алгоритме ad jacentfindif соседние элементы считаются равными,
когда ор(элемент, следующий элемент) возвращает true;
bool equal (iterator first, iterator last, iterator cmp) и bool equal (iterator first,
iterator last, iterator cmp, bool opO) — возвращают true, если элементы в
интервале [first, last) попарно равны элементам в интервале, на начало которого
указывает стр. В противном случае возвращают false. В первом варианте
алгоритма для сравнения используется оператор =, во втором — функция ор();
pair<iterator, iterator> mismatch(iterator 'first, iterator last, iterator cmp)
npair<iterator,iterator> mismatch (iterator first, iterator last, iterator cmp,
240
Приложение В. Стандартная библиотека
bool opO) — возвращают пару итераторов, указывающих на первые
несовпадающие элементы в интервале [first, last) и в интервале, на начало которого
указывает итератор стр. Первый возвращенный итератор принадлежит
интервалу [first, last), второй — интервалу [стр,...). Первый вариант алгоритма
использует дли сравнения оператор =, второй — функцию ор(). Если элементы
в обоих интервалах попарно равны, возвращаются конечные1 итераторы обоих
сравниваемых интервалов;
¦ bool lexicographical_compare(iterator firstl, iterator lastl, iterator first2,
iterator last2) и bool lexicographical_compare(iterator firstl, iterator lastl,
iterator first2, iterator last2, bool opO) — лексикографически сравнивают два
интервала: [firstl, lastl) и [firstl, lastl). Под лексикографическим
понимается такое сравнение, когда результат сравнения последовательностей решает
сравнение первых неравных элементов. Более короткая последовательность,
чьи элементы такие же, как в другой, считается «меньше». В первом варианте
алгоритма используется оператор <, во втором — функция ор().
Модифицирующие алгоритмы
¦ function for_each(iterator first, iterator last, function f) — применяет
объект-функцию f к каждому элементу контейнера в интервале, заданном
итераторами [first, last), возвращает объект-функцию после того, как он был
применен ко всем элементам;
¦ i terator copy (i terator f i rst, i terator 1 ast, i terator resul t) — копирует элементы
из области, заданной итераторами [first, last), в область [result; resul t+( last -
first)). Алгоритм copy переписывает уже существующие элементы. Для
вставки новых элементов не годится. Возвращает итератор, указывающий на
элемент, стоящий сразу за последним скопированным. Итератор result не может
быть в интервале [first, last);
¦ iterator copy_backward(iterator first, iterator last, iterator result) —
копирует элементы из области, заданной итераторами [first, last) в область
[result-(last-first), result). Отличается от сору О тем, что копирует элементы в
обратную сторону. Здесь result не начало, а конец интервала. Итератор result
не должен попасть в интервал [first, last);
¦ iterator transform(iterator first, iterator last, iterator result, valuejtype
f()) — применяет к объектам в интервале, заданном итераторами [first, last),
функцию f () и переписывает преобразованные объекты в область, на начало
которой указывает result. Поскольку этот алгоритм заменяет объекты
преобразованными, result может быть равен first. Возвращает итератор,
указывающий на элемент, стоящий сразу за последним преобразованным;
¦ iterator transform(iterator firstl, iterator lastl, iterator first2, iterator
result, valuejtype f()) — в отличие от предыдущего в этом алгоритме f() прини-
То есть last и соответствующий ему завершающий итератор второго интервала.
Алгоритмы
241
мает два аргумента. Один — из интервала [firstl, lastl), второй — из
интервала [first2,...). Преобразованные элементы переписываются в область, на
начало которой указывает result;
iteratormerge(iterator firstl, iterator lastl, iterator first2, iterator last2,
iterator dest) и iterator merge (iterator firstl, iterator lastl, iterator first2,
iterator last2, iterator dest, bool opO) — алгоритмы mergeO «сливают»
содержимое двух отсортированных контейнеров, заданных итераторами [firstl,lastl),
[first2, last2) в один контейнер, начало которого обозначено итератором dest.
В первой версии алгоритма для сравнения элементов используется оператор <,
во второй — функция ор();
iterator swap_ranges(iterator firstl, iterator lastl, iterator first2) —
обменивает элементы в интервале [firstl, lastl) и соответствующие элементы,
начинающиеся с fi rst2. В результате элементы обоих интервалов меняются
местами. Возвращает итератор, непосредственно следующий за последним
элементом во втором интервале;
void fill (iterator first, iterator last, valuejtype const & value) — присваивает
значение value всем элементам в интервале [first, last);
iterator fill_n(iterator first, sizejt n, valuejtype const &value) — присваивает
значение value всем элементам в интервале [first,first+n);
void generate(iterator first, iterator last, generator gen) — каждому элементу в
интервале, заданном итераторами [first,last), присваивает значение,
возвращенное функцией gen. Она не имеет параметров и при каждом вызове может
возвращать разные значения. Пример — генератор случайных чисел randO;
iterator generate_n(iterator first, sizet n, generator gen) — каждому элементу
в интервале, заданном итераторами [first,first+n), присваивает значение,
возвращенное функцией gen. Возвращает итератор first+n;
void replace(iterator first, iterator last, valuejtype const &old_value, valuejtype
const &new_value) — в интервале, заданном итераторами [first, last), заменяет
элементы со значением old_value на элементы со значением new_value;
void replaceif (iterator first, iterator last, bool op(), valuejtype const &newVal-
ue) — в интервале, заданном итераторами [first, last), заменяет на newValue все
элементы, для которых op(elem)=newValue;
iterator replace_copy(iterator first, iterator last, iterator result, valuejtype
const &old_value, valuejtype const &new_value) — копирует элементы из
интервала, заданного итераторами [first, last), в область, на начало которой
указывает result. В процессе копирования все элементы со значением old_value
подменяются элементами new_value. Возвращает итератор, указывающий на элемент,
стоящий сразу за последним переписанным;
iterator replace_copy_if(iterator first, iterator last, iterator result, bool
op( val ue_type e). val ue_type const &new_val ue) — то же, что repl асессору, но теперь
элементы, для которых ор(е) равно true, подменяются элементами со
значением new value.
242
Приложение В, Стандартная библиотека
Удаляющие алгоритмы
¦ iterator remove (iterator first, iteratorlast, value_type const &value) — винтерва-
ле, заданном итераторами [first, last), уничтожает все элементы со значением
val ue. Возвращает итератор newj ast такой, что новый интервал [f i rst, newj ast)
не содержит элементов со значением value. RemoveO, не уничтожает элементы
и не меняет размер контейнера sizeO, а только вытесняет элементы со
значением value за пределы интервала [first, newj ast). Чтобы действительно удалить
элементы, к ним можно применить собственную функцию eraseO;
¦ iterator remove_if(iterator first, iterator last, bool op(value_type e) — то же,
что removeO, но удаляются элементы, для которых ор(е) равно true;
¦ iterator remove_copy(iterator first, iterator last, iterator result, valuejtype
const &value) — удаляет элементы со значением value и переписывает их,
начиная с позиции, на которую указывает result. Возвращает итератор,
указывающий на элемент, стоящий сразу за последгош переписанным.
¦ iterator remove_copy_if(iterator first, iterator last, iterator result, bool
op(value_type e)) — удаляет из интервала, заданного итераторами [first, last),
все элементы, для которых ор(е) равно true, и переписывает их в область, на
начало которой указывает result. Возвращает итератор, указывающий на
элемент, стоящий сразу за последним переписанным;
¦ void unique(iterator first, iterator last) и void unique (iterator first,
iterator last, bool opO) — в интервале, заданном итераторами [first, last),
удаляют элементы, равные предыдущему. Если элементы контейнера расставлены
в порядке возрастания (убывания), алгоритм оставит в контейнере только
уникальные элементы. Первая версия алгоритма использует для определения
равенства оператор —, вторая — удаляет все элементы, следующие за el em, если
для них функция op(elemnew, elem) равна true;
¦ iterator unique_copy(iterator first, iterator last, iterator dest) и void unique_
copy(iterator first, iterator last, iterator dest, bool opO) — переписывают
элементы в интервале [first, last), начиная с позиции, на которую
указывает итератор dest. При переписывании удаляет все элементы, равные
предыдущему. Первая версия алгоритма использует для определения равенства
оператор ==, вторая — удаляет все элементы, следующие за elem, если для них
функция op(elemnew, elem) равна true.
Меняющие алгоритмы
¦ void reverse(iterator first, iterator last) и iterator reverse_copy(iterator first,
iterator last, iterator dest) — в интервале, заданном итераторами [first,last),
меняют порядок элементов на противоположный. Вторая версия алгоритма
обращает порядок элементов, когда копирует их в новый интервал, на начало
которого указывает итератор dest. Возвращает итератор, указывающий на
первый непереписанный элемент;
Алгоритмы
243
¦ void rotateCiterator first, iterator newfirst, iterator last) — в интервале,
заданном итераторами [first,last), «вращает» элементы так, что все элементы
переписываются вперед на одну позицию, а элемент, не уместившийся справа,
переписывается в начальную позицию слева. И так происходит до тех пор, пока
newfirst не станет указывать на элемент, на который до операции указывал
first;
¦ void rotate_copy(iterator first, iterator newfirst, iterator last, iterator dest) —
то же, что и rotateO, но теперь измененный контейнер оказывается в области,
на начало которой указывает dest;
¦ void random_shuffle(iterator first, iterator last) и void random_shuffle(iterator
first, iterator last, RandomFunc &op) — случайным образом «тасуют» элементы
в интервале [first,last). Первый алгоритм использует равномерно
распределенные случайные числа, второй позволяет задать собственный генератор
случайных чисел ор();
¦ iterator partition(iterator first, iterator last, bool op(value_typee)) и iterator
stable_partition(iterator first, iterator last, bool op(value_type e)) —
разбивают элементы контейнера, заданные интервалом итераторов [f i rst, 1 ast), на две
части. Сначала идут элементы, для которых ор( элемент)=true, затем — элементы,
для которых op^eMeHT)=false. Возвращает итератор, указывающий на первый
элемент, для которого ор(элемент)=false. Версия алгоритма stable_partition
работает так, что сохраняет относительный порядок элементов,
удовлетворяющих и неудовлетворяющих критерию. Если, скажем, op(el)=op(e2) и элемент el
стоял раньше в оригинальном контейнере, то он будет стоять раньше и в
отсортированном с помощью stable_partition().
Сортировка
¦ void sort (iterator first, iterator last), void sort (iterator first, iterator last, bool
op(value_type el, valuejtype e2)), void stable_sort(iterator first, iterator last)
и void stable_sort(iterator first, iterator last, bool op(value_type el, valuejtype
e2)) — расставляют элементы контейнера в порядке возрастания, используя для
их сравнения либо оператор <, либо функцию ор(). Версия stable_sort отличается
от sort О тем, что не меняет порядок равных элементов. Для сортировки
необходим произвольный доступ к элементам, поэтому алгоритм неприменим к
контейнеру list, у которого, впрочем, есть своя собственная функция sort О;
¦ void partial_sort(iterator first, iterator sortlast, iterator last) и void partial_
sort (iterator first, iterator sortlast, iterator last, bool opO) — эти
алгоритмы сортируют часть элементов, заданных интервалом [first, last), и
прекращают работу, когда интервал [first, sortlast) оказывается отсортированным.
Полезны, когда нужно найти несколько «наибольших» элементов контейнера.
Если sortlast равен last, сортируется весь контейнер. В первом варианте
алгоритма для сравнения элементов используется оператор <, во втором —
функция с двумя параметрами op(el,e2), возвращающая true, когда элемент el
«меньше» е2;
244
Приложение В. Стандартная библиотека
¦ i terator parti al _sort_copy( iterator first, iterator last, iterator destfirst, iterator
destlast) и iterator partial_sort_copy(iterator first, iterator last, iterator
destfirst, iterator destlast, bool op) — элементы из интервала [first, last)
копируются отсортированными в интервал [destfirst, destlast). Число скопированных
и отсортированных элементов равно числу элементов в меньшем интервале.
Алгоритм возвращает итератор, указывающий на элемент, стоящий
непосредственно за последним скопированным. В первом варианте алгоритма для сравнения
элементов используется оператор <, во втором — функция с двумя параметрами
op(el.e2), возвращающая true, когда элемент el «меньше» е2;
¦ void nth_element(iterator first, iterator nth, iterator last) и void nth_
element (iterator first, iterator nth, iterator last, bool opO) — оба алгоритма
частично сортируют элементы, заданные интервалом итераторов [first, last).
После выполнения алгоритма все элементы разбиваются на две части. На
границу указывает итератор nth. Любой элемент, стоящий до границы,
оказывается больше любого, что стоит после. При этом порядок элементов в обеих частях
не определен. В первой версии алгоритма для сортировки используется
оператор <, во второй — функция с двумя параметрами op(el,e2), возвращающая
true, когда элемент el «меньше» е2;
¦ iterator partitionCiterator first, iterator last, bool op) и iterator stable_parti-
tion (iterator first, iterator last, bool op) — переписывают в начало интервала,
заданного итераторами [first, last), те элементы, для которых op(elem)
возвращает true. Алгоритм stablepartitionO отличается тем, что сохраняет
первоначальный порядок равных элементов;
¦ make_heap(), push_heap(), popheapO, sort_heap() - алгоритмы, работающие со
структурой данных под названием heap. Прочитать о ней можно, например,
в книге Бентли «Жемчужины программирования», 2-е изд. Питер, 2002.
Алгоритмы для отсортированных контейнеров
¦ bool binary_search(iterator first, iterator last, value_type&value) и bool binary_
search(iterator first, iterator last, valuejtype &value, bool op) — производят
бинарный поиск элемента value в интервале, задаваемом итераторами [first,
last). Элементы контейнера должны быть отсортированы. Первый вариант
использует оператор < для сравнения элементов. Второй вариант — использует
функцию сравнения р, принимающую два элемента контейнера и
возвращающую true, если первый «меньше» второго. Возвращает true, если элемент
найден, и fal se — если нет;
¦ bool includes(iterator first, iterator last, iterator searchfirst, iterator
searchlast) и bool includes (iterator first, iterator last, iterator searchfirst,
iterator searchlast, bool opO) — выясняют, все ли элементы из
отсортированного интервала [searchfirst. searchlast) присутствуют в отсортированном
интервале [first, last). Если все элементы присутствуют, то возвращается true.
Если все элементы в [searchfirst,searchlast) равны, [first, last) должен
содержать такое же число элементов. Две разновидности алгоритмов отличаются
Алгоритмы
245
критерием сортировки. В первом случае это оператор <, во втором — функция
ор();
¦ iterator lower_bound( iterator first, iterator last, valuetype const value), iterator
lower_bound(iterator first, iteratorlast, valuetype const value, bool opO), iterator
upper_bound( iterator first, iterator last, valuetype const value) и iterator upper_
bound (iterator first, iterator last, value_type const value, bool opO) — все эти
алгоритмы работают с уже отсортированными контейнерами. Алгоритмы lower_
boundO возвращают итератор, указывающий на первый элемент, который
меньше или равен value. Алгоритмы upper_bound() возвращают итератор,
указывающий на первый элемент, который больше val ue. Если такой итератор не
находится, оба алгоритма возвращают last. В алгоритмах используется либо оператор <,
либо функция ор(), выясняющая, какой элемент «меньше»;
¦ pai r<iterator eqf i rst, iterator eql ast> equal_range (iterator f i rst, iterator 1 ast,
valuejtype const &value) и pair<iterator eqfirst, iterator eqlast> equal_range
(iterator first, iterator last, value_type const & value, bool opO) — алгоритмы
equal_range возвращают пару итераторов, такую, что элементы в интервале [eq-
first, eql ast), равны value. Если такой интервал найти не удается,
возращенные итераторы оказываются равны. Контейнеры, с которыми работает
алгоритм, должны быть предварительно отсортированы. Одна из версий алгоритма
использует для сравнения элементов оператор <, другая — функцию ор();
¦ iterator merge(iterator firstl, iterator lastl, iterator first2, iterator last2,
iterator dest) и iterator merge (iterator firstl, iterator lastl, iterator first2,
iterator last2, iterator dest, bool opO) — «сливают» два отсортированных
контейнера. Результирующий контейнер тоже оказывается отсортированным и
начитается с позиции, на которую указывает dest. Возвращают итератор,
указывающий на элемент, который следует непосредственно за последним
скопированным. В первом алгоритме для сравнения используется оператор <, во
втором — функция op(el, е2), возвращающая true, когда el «меньше» е2;
¦ void inplace_merge(iterator firstl, iterator lastlfirst2, iterator last2) и void
inplacejnerge (iterator firstl, iterator lastlfirst2, iterator last2, bool
opO) — соединяют два рядом стоящих отсортированных интервала [firstl,
lastlfirst2), [lastlfirst2, last2) в один отсортированный интервал [firstl,
last2). Первый вариант алгоритма использует для сравнения оператор <,
второй — функцию ор();
¦ iterator set_union(iterator firstl, iterator lastl, iterator first2, iterator
last2, iterator dest) и iterator set_uni on (iterator firstl, iterator lastl, iterator
first2, iterator last2, iterator dest, bool opO) — алгоритм set_uni on О
«объединяет» два предварительно отсортированных контейнера, заданных
интервалами [firstl, lastl), [first2, last2). В результирующем контейнере оказываются
элементы, имеющиеся либо в первом контейнере, либо во втором, либо в
обоих. Возвращают итератор, указывающий элемент, стоящий непосредственно
за последним скопированным. Относительно одинаковых элементов
существует правило: если в первом контейнере m элементов со значением value, а во
246
Приложение В. Стандартная библиотека
втором — п, то в объединенном контейнере их будет max(m.n). Объединенный
контейнер будет отсортирован либо с помощью оператора <, либо с помощью
функции ор();
¦ iteratorset_intersection(iteratorfirstl, iteratorlastl, iteratorfirst2, iterator
last2, iterator dest) и iterator set_intersection(iterator firstl, iterator lastl,
iterator first2, iterator last2, iterator dest, bool opO) — «сливают» элементы,
заданные двумя отсортированными интервалами [firstl, lastl), [first2, last2).
Начало результирующего контейнера задается итератором dest. Результат
слияния — отсортированные элементы, встречающиеся как в первом, так и во
втором интервале. Если в первом и во втором интервале встретились одинаковые
элементы, то берется содержимое того интервала, где их меньше. В первом
варианте алгоритма для сортировки используется оператор <, во втором —
функция ор(). Алгоритмы возвращают итератор, указывающий на элемент,
следующий непосредственно за последним элементом результата;
¦ iterator set_difference(iterator firstl, iterator lastl, iterator first2, iterator
last2. iterator dest) и iterator set_diference(iterator firstl, iterator lastl.
iterator first2, iterator last2, iterator dest, bool opO) — «сливают» элементы,
заданные двумя отсортированными интервалами [firstl, lastl), [first2, last2).
Начало результирующего контейнера задается итератором dest. Результат
слияния — те отсортированные элементы первого интервала, которых нет во
втором. Если в первом и во втором интервале встретились одинаковые элементы,
то берется разность между числом элементов в первом и во втором
интервале. Если разность отрицательна, берется 0 элементов. В первом варианте
алгоритма для сортировки используется оператор <, во втором — функция ор().
Алгоритмы возвращают итератор, указывающий на элемент, следующий
непосредственно за последним элементом результата;
¦ i terator set_symmetri c_di fference(i terator f i rstl, i terator 1 astl, i terator fi rst2,
iterator last2, iterator dest) и iterator set_symmetric_difference(iterator firstl,
iterator lastl, iterator first2, iterator last2, iterator dest, bool opO) —
«сливают» элементы, заданные двумя отсортированными интервалами [firstl,
lastl), [first2, last2). Начало результирующего контейнера задается
итератором dest. Результат слияния — отсортированные элементы первого и второго
интервала, которые есть либо в первом, либо во втором, но нет в обоих. Оба
алгоритма возвращают итератор, указывающий на элемент, следующий
непосредственно за последним элементом результата
Численные алгоритмы
¦ valuejtype accumulate (iterator first, iterator last, valuejtype ini) и valuejtype
accumulate (iterator first, iterator last, valuejtype i nit. bool opO) —первая
версия алгоритма складывает все элементы контейнера, в интервале [first, last).
Начальное значение суммы равно init. Второй вариант алгоритма использует
обобщенное сложение, заданное функцией ор(). Вместо суммы init+el+e2+e3 во
втором варианте вычисляется op(init,el), op(init,e2), op(init, еЗ) и т. д. Оба
Алгоритмы
247
алгоритма возвращают значение суммы или init, когда интервал, заданный
итераторами [first, last), пуст.
¦ value_type inner_product(iterator 1 firstl, iterator lastl, iterator first2, value_
type i ni tVal ue) — вычисляет i ni tVal ue = i ni tVal ue + el eml * el em2 для всех
элементов в двух интервалах [firstl, lastl) и [first2...);
¦ adjacent_difference(iterator firstl, iterator lastl, iterator first2) —
вычисляются разности между последующим и предыдущим элементами
контейнера, расположенного в интервале [firstl,lastl): al, a2 - al, аЗ - а2, а4 - аЗ и т. д.
Элементы нового контейнера хранятся в интервале [first2...);
¦ iterator partial_sum(iterator firstl, iterator lastl, iterator first2) —
вычисляются частичные суммы элементов контейнера, хранящихся в интервале [f i rstl,
lastl): al, al + a2, al + a2 + аЗ и т. д. Элементы нового контейнера хранятся в
интервале [first2...). Возвращает итератор, указывающий на элемент, стоящий
непосредственно за последним измененным в интервале [first2...).
Литература
1. БентлиДж. Жемчужины программирования. 2-е изд. — СПб.: Питер, 2003. —
272 с.
2. Гамма Эп Хелм Р., Джонсон Р., Влиссидес Дж. Приемы
объектно-ориентированного проектирования. — СПб.: Питер, 2003. — 368 с.
3. Джосьютис Н. C++. Стандартная библиотека. Для профессионалов. — СПб.:
Питер, 2003. - 736 с.
4. Мейерс С. Наиболее эффективное использование C++. 35 новых
рекомендаций по улучшению ваших программ и проектов. — М.: ДМК Пресс,
2000. - 304 с.
5. Мейерс С. Эффективное использование C++. 50 рекомендаций по
улучшению ваших программ и проектов. — М.: ДМК Пресс, 2000 — 240 с.
6. Мейерс С. Эффективное использование STL. Библиотека программиста. —
Питер, 2003. - 400 с.
7. Элджер Дж. C++. Библиотека программиста. — СПб.: Питер, 2000. — 320 с.
Алфавитный указатель
А
Автоматический объект, 131
Алгоритм, 49, 54
accumulate, 51
for_each(), 101
random_shuffle(), 49,101
sort(), 49
Анаграммы, 54
Б
Байт, 26
Бинарный поиск, 93
Бит, 26
В
Выражения, 64
г
Глобальный объект, 130
Д
Деструктор, 137
виртуальный, 183
Динамическое связывание, 221
Директива
#define, 189
#ifndef...#endif, 189
#include, 21
using, 22
з
Запись, 101
и
Инкапсуляция, 220
Инструкция, 15
break, 53,54
do{}while(), 39
for(), 40
Инструкция (продолжение)
if(), 32
if() else, 36
return, 63
switch, 81
while(), 33
Интерфейс, 13
Итератор, 48
к
Класс
ofstream, 158
range_error, 212
runtime_error, 212
абстрактный базовый, 180
базовый, 170
производный, 170
Классификация, 219
Комментарий, 21
Компилятор, 20
Консольное приложение, 25
Константа
ios
арр, 204
beg, 208
binary, 205
cur, 208
end, 208
in, 204
out, 204
Конструктор, 44,79,124
конструктор копирования, 137, 138
Контейнер
deque, 118
map, 93
multimap, 55
vector, 47
250
Алфавитный указатель
Л
Локальный объект, 131
м
Макрос, 189
Манипулятор, 19, 200
endl, 19,201
setw(), 201
Массив, 45
н
Наследование, 220
О
обрезание объекта, 182
Объект
cout, 18
Операнд, 22
Оператор, 16
!, 35
!=, 34
%, 100
&, 31,71
&&, 34
()?, 86
*, 16,48
*-, 17,37
+, 16,36
++, 38
постфиксный, 38
префиксный, 38
+-, 17,37
--, 38
-, 17
->, 105
/, 16
/=,37
<, 32
«, 18,19
<-, 33
-, 16
—, 34
>, 34
>-, 34
», 51
[], 44,45
11,35
delete, 75
new, 73
reinterpret_cast<>, 209
sizeof(), 26, 170
static_cast<THn>, 17
бинарный, 37
области действия, 22
порядок выполнения, 19
Оператор (продолжение)
приоритет, 19
унарный, 37
Операторная функция
!, 158
О, 142
+, 150
++, 159
++ (постфиксный), 160
«, 154
[][]> 162
Оператор приведения, 166
п
Полиморфизм, 219,220
статический, 221
Постоянная функция, 165
Постоянный оператор, 163
Потоки, 197
Препроцессор, 189
Пространство имен, 22,131
std, 22
Профилирование, 193
Процессор, 20
с
Собственная функция
deque
clear(), 120
pop_back(), 119
pop_front(), 119
random_shuffle, 119
fstream
gcount(), 206
get, 205
put, 205
read, 206
write, 206
write(), 208
i fstream
close(), 58
eof(), 53
fail(), 79
get(), 94
getline, 53
open(), 53
seekg(), 207
istream
clear(), 198
fail()f 198
get, 199
getline(), 200
map
insert(), 93
multimap
insert(), 56
Алфавитный указатель
251
Собственная функция {продолжение)
ofstream
seekp(), 207
ostream
flush(), 202
put(), 202
precision, 18
string
append, 43
append(), 30
at(), 44
begin(), 50
end(), 50
erase(), 95
find, 44
insert(), 42
size(), 30
vector
begin(), 48
clear(), 57
end(), 48
push_back(), 47
size(), 48, 92
Сокрытие данных, 220
Специализация, 219
Ссылки, 31
Статический объект, 133
Статическое связывание, 221
Строки, 30,36
т
Тип
char, 26
signed, 29
unsigned, 29
double, 15
мантисса, 27
порядок, 27
ifstream, 53
int, 14
unsigned, 27
string, 30
Тип объекта, 14
у
Указатель, 71
на указатель, 81
на функцию, 87
Ф
Файл, 52
открытие, 52
функтор
См. Функции-объекты, 156
Функции-объекты, 156
Функция
void, 65
Функция {продолжение)
аргументы, 62
параметры, 62
рекурсивная, 68
чисто виртуальная, 180
ш
Шаблон
класса, 193
функции, 64
А
argc, 77
argv, 77
assert(), 210
С
catch(), 212
cdecl, 109
cin, 51
class, 110
E
enum, 98
F
FAR, 23
friend, 153
G
gcc, 23
I
inline, 192
N
Namespace, 22
namespace, 131
P
protected, 173
s
slicing, 182
streams
См. Потоки, 197
struct, 101
T
this, 147
throw, 212
typedef, 103
V
virtual, 176
Крупник Александр Борисович
Самоучитель C++
Главный редактор
Заведующий редакцией
Руководитель проекта
Литературный редактор
Художник
Корректор
Верстка
Е. Строганова
A. Кривцов
В. Шрага
Ю. Суркис
Л. Аду веская
B. Листова
К. Кузьминский
Лицензия ИД № 05784 от 07.09.01.
Подписано в печать 23.08.04. Формат 70X100/16. Усл. п. л. 20,64.
Тираж 5000 экз. Заказ № 305.
ООО «Питер Принт». 194044, Санкт-Петербург, пр. Б. Сампсониевский, д. 29а.
Налоговая льгота — общероссийский классификатор продукции
ОК 005-93, том 2; 953005 — литература учебная.
Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. А. М. Горького
Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций.
197110, Санкт-Петербург, Чкаловский пр., 15.
В1997 году по инициативе генерального директора Издательского дома «Питер»
Валерия Степанова и при поддержке деловых кругов города в Санкт-Петербурге
был основан «Книжный клуб Профессионал». Он собрал под флагом клуба
профессионалов своего дела, которых объединяет постоянная тяга к знаниям и любовь
к книгам. Членами клуба являются лучшие студенты и известные практики из
разных сфер деятельности, которые хотят стать или уже стали профессионалами в той
или иной области.
Как и все развивающиеся проекты, с течением времени книжный клуб вырос
в «Клуб Профессионал». Идею клуба сегодня формируют три основные «клубные»
функции:
• неформальное общение и совместный досуг интересных людей;
• участие в подготовке специалистов высокого класса
(семинары, пакеты книг по специальной литературе);
• формирование и высказывание мнений современного профессионала
(при встречах и на страницах журнала).
КАК ВСТУПИТЬ В КЛУБ?
Для вступления в «Клуб Профессионал» вам необходимо:
• ознакомиться с правилами вступления в «Клуб Профессионал»
на страницах журнала или на сайте www.piter.com;
• выразить свое желание вступить в «Клуб Профессионал»
по электронной почте postboolc@piter.com или по тел. (812) 103-73-74;
• заказать книги на сумму не менее 500 рублей в течение любого времени
или приобрести комплект «Библиотека профессионала».
«БИБЛИОТЕКА ПРОФЕССИОНАЛА»
Мы предлагаем вам получить все необходимые знания, подписавшись на
«Библиотеку профессионала». Она для тех, кто экономит не только время, но и деньги.
Покупая комплект - книжную полку «Библиотека профессионала», вы получаете:
• скидку 15% от розничной цены издания, без учета почтовых расходов;
• при покупке двух или более комплектов - дополнительную скидку 3%;
• членство в «Клубе Профессионал»;
• подарок - журнал «Клуб Профессионал».
Закажите бесплатный журнал
«Клуб Профессионал».
ПИТЕР
ИЗДАТЕЛЬСКИЙ ДОМ
WWW.PITER.COM
Нет времени
ходить по магазинам?
наберите:
www.piter.com
Здесь вы найдете:
Все книги издательства сразу
Новые книги — в момент выхода из типографии
Информацию о книге — отзывы, рецензии, отрывки
Старые книги — в библиотеке и на CD
И наконец, вы нигде не купите
наши книги дешевле!
WWW.PITER.COM
ПРЕДСТАВИТЕЛЬСТВА ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
предлагают эксклюзивный ассортимент компьютерной, медицинской,
психологической, экономической и популярной литературы
РОССИЯ
Москва м. «Калужская», ул. Бутлерова, д. 176, офис 207, 240; тел./факс @95) 777-54-67;
e-mail: sales@piter.msk.ru
Санкт-Петербург м. «Выборгская», Б. Сампсониевский пр., д. 29а;
тел. (812) 103-73-73, факс (812) 103-73-83; e-mail: sales@piter.com
Воронеж ул. 25 января, д. 4; тел. @732) 39-61 -70;
e-mail: piter-vrn@vmail.ru; piterv@comch.ru
Екатеринбург ул. 8 Марта, д. 2676; тел./факс C43) 225-39-94, 225-40-20;
e-mail: piter-ural@r66.ru
Нижний Новгород ул. Премудрова, д. 31а; тел. (8312) 58-50-15, 58-50-25;
e-mail: piter@infonet.nnov.ru
Новосибирск ул. Немировича-Данченко, д. 104, офис 502;
тел/факс C832) 54-13-09,47-92-93,11-27-18, 11-93-18; e-mail: piter-sib@risp.ru
Ростов-на-Дону ул. Калитвинская, д. 17в; тел. (8632) 95-36-31, (8632) 95-36-32;
e-mail: jupiter@rost.ru
Самара ул. Новосадовая, д. 4; тел. (8462K7-06-07; e-mail: piter-volga@sama.ru
УКРАИНА
Харьков ул. Суздальские ряды, д. 12, офис 10-11, т. @57) 712-27-05,712-40-88;
e-mail: piter@tender.kharkov.ua
Киев пр. Красных Казаков, д. 6, корп. 1; тел./факс @44) 490-35-68,490-35-69;
e-mail: office@piter-press.kiev.ua
БЕЛАРУСЬ
Минск ул. Бобруйская д., 21, офис 3; тел./факс C7517) 226-19-53; e-mail: piter@mail.by
AW Ищем зарубежных партнеров или посредников, имеющих выход на зарубежный рынок.
^^ Телефон для связи: (812) 103-73-73.
E-mail: grigorjan@piter.com
?^ Издательский дом «Питер» приглашает к сотрудничеству авторов.
^ Обращайтесь по телефонам: Санкт-Петербург — (812) 327-13-11,
Москва - @95) 777-54-67.
Заказ книг для вузов и библиотек: (812) 103-73-73.
Специальное предложение - e-mail: kozin@piter.com
СПЕЦИАЛИСТАМ
КНИЖНОГО БИЗНЕСА!
Башкортостан
Уфа, «Азия», ул. Зенцова, д. 70 (оптовая продажа),
маг. «Оазис», ул. Чернышевского, д. 88,
тел./факс C472) 50-39-00.
E-mail: aslaufa@ufanet.ru
Дальний Восток
Владивосток, «Приморский торговый дом книги»,
тел./факс D232) 23-82-12.
E-mail: bookbase@mail.primorye.ru
Хабаровск, «Мире»,
тел. D212) 30-54-47, факс 22-73-30.
E-mail: sale_book@bookmirs.khv.ru
Хабаровск, «Книжный мир»,
тел. D212) 32-85-51, факс 32-82-50.
E-mail: postmaster@worldbooks.kht.ru
Европейские регионы России
Архангельск, «Дом книги»,
тел. (8182) 65-41-34, факс 65-41-34.
E-mail: book@atnet.ru
Калининград, «Вестер»,
тел./факс @112) 21 -56-28, 21 -62-07.
E-mail: nshibkova@vester.ru
http://www.vester.ru
Северный Кавказ
Ессентуки, «Россы», ул. Октябрьская, 424,
тел./факс (87934) 6-93-09.
E-mail: rossy@kmw.ru
Сибирь
Иркутск, «ПродаЛитЪ»,
тел. C952) 59-13-70, факс 51-30-70.
E-mail: prodalit@irk.ru
http://www.prodalit.irk.ru
Иркутск, «Антей-книга»,
тел./факс C952) 33-42-47.
E-mail: antey@irk.ru
УВАЖАЕМЫЕ ГОСПОДА!
КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР»
ВЫ МОЖЕТЕ ПРИОБРЕСТИ
ОПТОМ И В РОЗНИЦУ
У НАШИХ РЕГИОНАЛЬНЫХ ПАРТНЕРОВ.
Красноярск, «Книжный мир»,
тел./факс C912) 27-39-71.
E-mail: book-world@public.krasnet.ru
Нижневартовск, «Дом книги»,
тел. C466) 23-27-14, факс 23-59-50.
E-mail: book@nvartovsk.wsnet.ru
Новосибирск, «Топ-книга»,
тел. C832) 36-10-26, факс 36-10-27.
E-mail: office@top-kniga.ru
http://www.top-kniga.ru
Тюмень, «Друг»,
тел./факс C452) 21-34-82.
E-mail: drug@tyumen.ru
Тюмень, «Фолиант»,
тел. C452) 27-36-06, факс 27-36-11.
E-mail: foliant@tyumen.ru
Челябинск, ТД «Эврика», ул. Барбюса, д. 61,
тел./факс C512) 52-49-23.
E-mail:evrika@chel.surnet.ru
Татарстан
Казань, «Таис»,
тел. (8432) 72-34-55, факс 72-27-82.
E-mail: tais@bancorp.ru
Урал
Екатеринбург, магазин № 14,
ул. Челюскинцев, д. 23,
тел./факс C432) 53-24-90.
E-mail: gvardia@mail.ur.ru
Екатеринбург, «Валео-книга»,
ул. Ключевская, д. 5,
тел./факс C432) 42-56-00.
E-mail: valeo@etel.ru
WWW.PITER.COM