Академический Документы
Профессиональный Документы
Культура Документы
32.973.2-018.1я7
УДК 004.43(075)
Э47
Эллайн А.
Э47 C++. От ламера до программера. — СПб.: Питер, 2015. — 480 с.: ил.
ISBN 978-5-496-01189-1
Эта книга предлагает быстрый способ изучить принципы объектно-ориентированного про-
граммирования и освоить практику программирования на языке С++ новейшего стандарта.
Издание может использоваться как учебный курс для начинающих осваивать C++, так и удоб-
ный справочник для тех, кто хочет быстро найти актуальную информацию о том или ином
аспекте языка.
Автор книги Алекс Эллайн — профеcсиональный разработчик на С++, создатель популяр-
нейшего ресурса Cprogramming.com, предлагает собственную уникальную методику обучения
программирования, которая позволит вам в кратчайшие сроки стать экcпертом разработки на
C++.
ББК 32.973.2-018.1я7
УДК 004.43(075)
Права на издание получены по соглашению с Cprogramming.com. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надеж-
ные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гаранти-
ровать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки,
связанные с использованием книги.
Глава 5. Циклы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Глава 6. Функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Операторы else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Else-if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Сравнение строк. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Несколько любопытных булевых операций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Логическое НЕ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Логическое И. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Логическое ИЛИ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Комбинация выражений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Примеры логических выражений. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Глава 5. Циклы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Циклы while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Типичная ошибка. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Циклы for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Инициализация переменной . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Условие цикла. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Обновление переменной . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Циклы do-while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Управление циклами. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Вложенные циклы. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Как выбрать подходящий цикл. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Циклы for. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Циклы while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Циклы do-while. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Глава 6. Функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Синтаксис функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Локальные и глобальные переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Локальные переменные. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Глобальные переменные. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Предупреждение по поводу глобальных переменных. . . . . . . . . . . . . . . . . . 103
Подготовка функций к использованию. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Определения и объявления функций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Пример прототипа функции. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Деление программы на функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Когда код многократно повторяется. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Когда код должен хорошо читаться. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Именование и перегрузка функций. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
Кратко о функциях. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
10 Содержание
1
Эти приложения и многие другие примеры использования языка C++ можно
найти на странице http://www.stroustrup.com/applications.html
2
Эта спецификация была утверждена, когда работа над книгой уже подходила
к концу, поэтому я не включил в книгу материалы нового стандарта. О стан-
дарте C++ 11 вы можете почитать на странице http://www.cprogramming.com/
c++11/what-is-c++0x.html
Благодарности 19
Благодарности
Я хочу поблагодарить Александру Хоффер (Alexandra Hoffer) за вниматель-
ное, терпеливое редактирование этой книги и конкретные советы. Без ее
усилий книга не вышла бы в свет. Выражаю благодарность Андрею Черем-
скому (Andrei Cheremskoy), Миньяню Цзяню (Minyang Jiang) и Йоханнесу
Петеру (Johannes Peter) за ценные отзывы, предложения и поправки.
Г лава 1
Введение и настройка среды разработки
Терминология
Новые термины будут появляться на протяжении всей этой книги, а пока
мы сконцентрируемся на фундаментальных понятиях, которые помогут
начать знакомство с C++.
Программирование
Программирование — это написание инструкций, которые компьютер
способен воспринять и выполнить. Сами эти инструкции называются
исходным кодом. Написание исходного кода — это именно то, чем вам
предстоит заниматься. Мы увидим первые примеры исходного кода уже
через несколько страниц.
Исполняемый файл
Конечным результатом программирования является исполняемый файл.
Исполняемый файл можно запускать: если вы пользуетесь операционной
системой Windows, то знакомы с exe-файлами, которые являются исполня-
емыми. Конкретный пример исполняемого файла — программа Microsoft
Word. У некоторых программ есть дополнительные файлы (графические,
музыкальные и др.), но у каждой программы есть исполняемый файл.
Чтобы создать исполняемый файл, необходим компилятор — программа,
которая преобразует исходный код в исполняемый файл.
Единственное, что можно делать без компилятора — это читать исходный
код. Поскольку это не слишком увлекательное занятие, наш следующий
шаг — научиться пользоваться компилятором.
Windows
Мы настроим инструмент Code::Blocks — бесплатную среду разработки
для языка C++.
Устранение проблем
Если ваша программа не запустится, скорее всего, это вызвано ошибками
компиляции или неправильными настройками IDE.
z z Настройка IDE
Самая распространенная ошибка, которую видят разработчики, когда
программа не работает, выглядит так: ’CB01– Debug” uses an invalid compiler.
Probably the toolchain path within the compiler options is not setup correctly?!
Skipping... (’CB01– Debug” использует некорректный компилятор. Воз-
можно, цепочка инструментов внутри компилятора настроена неверно.
Пропущено...).
z z Ошибки компилятора
Ошибки компилятора происходят, если вы изменили файл main.cpp
и компилятор его не понимает. Чтобы определить, что вы сделали не так,
взгляните на окно Build messages (Сообщения сборки) или Build log (Жур-
нал сборки). Окно Build messages отображает только ошибки компилятора,
а окно Build log содержит сведения и о других проблемах. При наличии
ошибки окно выглядит следующим образом:
Macintosh
В этом разделе речь только о настройке IDE в OS X1. OS X содержит мощную
среду с оболочкой на основе Unix, поэтому вы можете пользоваться многими
из инструментов, рассмотренных в разделе для Linux этой книги. Тем не
менее не исключено, что вы захотите опробовать работу в Xcode от компа-
нии Apple. Независимо от того, захотите вы использовать Xcode или нет, ее
установка обязательна для работы со стандартными инструментами Linux.
Хотя вы не обязаны пользоваться Xcode для разработки программ на C++
для Mac, стоит освоить ее, если хотите заняться разработкой графических
интерфейсов для Mac.
Xcode
Среда разработки Xcode бесплатно распространяется в составе Mac OS X,
но не устанавливается по умолчанию. Можно найти ее на DVD-диске с Mac
OS X или скачать новейшую версию. Если у вас медленное интернет-со-
единение, поищите Xcode на компакт-диске с Mac OS X, поскольку объем
загрузки с документацией очень велик. Обратите внимание, что даже про-
стейшие компиляторы, такие как gcc и g++, которые обычно по умолчанию
Установка Xcode 3
Чтобы скачать Xcode, выполните следующие действия.
• Зарегистрируйтесь как разработчик Apple на сайте http://developer.
apple.com/programs/register/. Регистрация бесплатна. На веб-сайте Apple
может показаться, что необходимо платить, но вышеуказанная ссылка
приведет прямо на страницу бесплатной регистрации, где необходимо
ввести основные личные сведения.
• Перейдите по ссылке https://developer.apple.com/downloads/index.action
и выполните поиск Xcode 3.2.6. Вы увидите один результат; щелкните
сначала на нем, а затем на ссылке для скачивания Xcode 3.2.6 и iOS
SDK 4.3.
Xcode распространяется в виде обычного файла образа диска, который
можно открыть. Откройте файл образа диска и запустите файл Xcode.mpkg.
В процессе установки вас попросят принять условия лицензионного со-
глашения и предложат список компонентов для установки. Достаточно
компонентов по умолчанию. Выберите их и доведите процедуру установки
до конца.
Запуск Xcode
По завершении работы установщика можно найти Xcode в Developer|Appli
cations|Xcode. Запустите приложение Xcode. Xcode поставляется с подроб-
ной документацией; при желании можно ознакомиться с руководством
«Getting Started with Xcode» (Как начать работать с Xcode). Тем не менее
это не обязательно для изучения оставшейся части данного раздела.
Установка Xcode 4
Скачать Xcode 4 можно в магазине приложений для Mac (Mac App Store)
и установить ее. Размер Xcode 4 — около 4,5 Гбайт.
После загрузки Xcode из магазина приложений для Mac на вашем доке
появится значок Install Xcode (Установить Xcode). Дважды щелкните на
нем для запуска установки.
Вас попросят принять условия лицензионного соглашения и предложат
список компонентов для установки. Выберите компоненты по умолчанию
и доведите до конца процедуру установки.
Запуск Xcode
По завершении работы установщика Xcode можно найти в Developer|Appli
cations|Xcode. Запустите приложение Xcode. Xcode поставляется с подроб-
ной документацией; при желании можно ознакомиться с руководством
«Xcode Quick Start Guide» (Как начать работать с Xcode), открыв его по
ссылке Learn about using Xcode (Узнайте об использовании Xcode) на на-
чальном экране. Это не требуется для изучения оставшейся части данного
раздела.
Я уже указал в нем имя продукта HelloWorld и тип C++ в Type (по умолчанию
задан тип C). Еще раз нажмите Next. Вы увидите следующее окно:
Если флажок Create local git repository for this project (Создать локальный
репозиторий git для этого проекта) установлен, можно снять его. Git — это
система контроля исходного кода, которая позволяет хранить несколько
версий одного и того же проекта; ее рассмотрение выходит за рамки этой
книги. Выберите местоположение вашего проекта; я помещу его в папку
Documents. Задав настройки, нажмите Create (Создать). Появится новое окно:
Устранение проблем
Обратите внимание, что в этом разделе используются снимки экранов из
Xcode 3.
Возможно, программа не компилируется из-за ошибок компилятора
(скажем, из-за опечатки в примере или настоящей ошибки в вашей про-
грамме). Если это произойдет, компилятор отобразит одно или несколько
сообщений об ошибках.
Среда Xcode выводит сообщения об ошибках компилятора прямо в тех
строках исходного кода, где они происходят. В приведенном ниже примере
я изменил исходную программу и вместо конструкции std::cout написал c.
Linux
Если вы работаете в Linux, у вас почти наверняка уже установлен ком-
пилятор C++. Обычно пользователи Linux работают с компилятором
g++, который входит в коллекцию компиляторов GNU (GNU Compiler
Collection, GCC).
Linux 41
Пример 1: hello.cpp
#include <iostream>
int main ()
{
std::cout << "Hello, world" << std::endl;
}
и нажмите Enter.
42 Глава 1. Введение и настройка среды разработки
Устранение проблем
Возможно, ваша программа по какой-то причине не компилируется. Как
правило, это происходит из-за ошибок компилятора (скажем, опечатки
при вводе примера программы). В такой ситуации компилятор отображает
одно или несколько сообщений об ошибках компиляции.
Например, если в примере вы ввели x перед cout, компилятор выведет
следующие ошибки:
gcc_ex2.cc: In function 'int main ()':
gcc_ex2.cc:5: error: 'xcout' is not a member of 'std'
Настройка Nano
Чтобы воспользоваться некоторыми возможностями редактора nano,
придется настроить его конфигурационный файл — .nanorc, который, как
большинство конфигурационных файлов Linux, находится в каталоге (~/.
nanorc).
Если файл уже существует, его можно просто отредактировать; если нет —
создать. (Если у вас вообще нет опыта использования текстовых редакторов
Linux, для конфигурирования можно воспользоваться редактором nano.
Информация об основах работы с nano приведена ниже.)
Чтобы правильно настроить nano, воспользуйтесь готовым примером
файла .nanorc. Вы получите удобное выделение синтаксиса и автоотступ,
что существенно упростит редактирование исходного кода.
Использование Nano
Чтобы создать новый файл, можно запустить nano без аргументов; чтобы
редактировать файл, укажите его имя в командной строке:
nano hello.cpp
z z Редактирование текста
При запуске nano вы создаете новый файл либо открываете существующий.
После этого можно вводить в него текст — в этом отношении nano очень
похож на Блокнот (Notepad) Windows. Тем не менее в nano используется
иной подход к копированию и вставке текста: для копирования текста
нажмите Ctrl-K (операция Cut Text — вырезать текст), а для вставки —
Ctrl-U (операция UnCut Text — отменить вырезание текста). Если вы не
выделили текст, по умолчанию эти команды вырежут одну строку текста.
Кроме того, можно искать текст в файле с помощью функции «Where Is»
(Где находится), нажимая Ctrl-W. Вам предлагается задать набор параме-
тров, но проще всего ввести искомую строку и нажать Enter.
Вы можете перемещаться на соседние страницы с помощью функ-
ций Prev Page (предыдущая страница) и Next Page (следующая
Linux 45
z z Сохранение файлов
Сохранение файла в nano называется выводом (WriteOut, Ctrl-O).
z z Открытие файлов
Чтобы открыть файл для редактирования, воспользуйтесь командой Read
File (Считать файл) или Ctrl-R. Эта команда выводит параметры меню.
Команду New Buffer можно вызвать, нажав M-F. Буква M означает meta
key (метаклавиша) — в данном случае, скорее всего, потребуется нажать
Alt: Alt-F1. Это сочетание указывает редактору nano, что вы собираетесь
открыть файл. Затем можно ввести имя редактируемого файла или вывести
список файлов с помощью Ctrl-T и выбрать в нем нужный файл. Отменить
выполненные действия можно, как обычно, Ctrl-C.
ботает, нажмите и отпустите Esc перед тем, как нажать символ (например,
последовательность Esc-F эквивалентна Alt-F).
Linux 47
z z Дополнительная информация
Теперь можно редактировать основные файлы в программе nano, а допол-
нительную информацию о ней можно получить во встроенной справочной
системе (Ctrl-G). Для изучения более сложных возможностей редактора
nano рекомендую воспользоваться веб-сайтом http://freethegnu.wordpress.
com/2007/06/23/nano-shortcuts-syntax-highlightand-nanorc-config-file-pt1/.
Г лава 2
Основы C++
Пример 2: empty.cpp
int main ()
{
}
Первая строка
int main ()
Пример 3: hello.cpp
#include <iostream>
using namespace std;
int main ()
{
cout << "HEY, you, I'm alive! Oh, and Hello
World!\n";
}
Это стандартный код, который входит в состав почти всех программ на C++.
Сейчас просто используйте его в начале всех программ сразу за операто-
рами include. Он облегчает применение сокращенных версий некоторых
процедур, предоставляемых заголовочным файлом iostream. Позже мы
поговорим, как именно он работает, а пока просто не забывайте включать
его в свои программы.
Обратите внимание: строка завершается точкой с запятой. Точка с за-
пятой — часть синтаксиса C++. Она указывает компилятору на конец
оператора. Точка с запятой используется для завершения большинства
операторов в языке C++. Начинающие программисты часто забывают
использовать точки с запятой, поэтому, если ваша программа не работает,
проверяйте, все ли точки с запятой расставлены в ее коде. Рассказывая
о новых концепциях, я буду отмечать, надо ли в них использовать точку
с запятой.
Далее следует функция main, с которой начинается выполнение программы:
int main ().
Здесь язык C++ использует объект cout (читается «си аут») для ото-
бражения текста. Специально для доступа к этому объекту мы включили
в программу заголовочный файл iostream.
После того как вам удастся запустить вашу первую программу, поэкспери-
ментируйте с cout. Выведите другой текст, несколько строк — посмотрите,
на что способен компьютер.
52 Глава 2. Основы C++
[операторы include]
using namespace std;
int main()
{
[здесь ваш код];
}
Комментарии в программах
В процессе освоения программирования стоит научиться документировать
программы (если не для других, то для себя). Для этого в код добавляются
комментарии; так я буду часто пояснять его смысл.
Текст, помеченный как комментарий, игнорируется компилятором при вы-
полнении кода, что позволяет описывать код любым текстом. Чтобы создать
комментарий, воспользуйтесь последовательностью символов //, которая
показывает компилятору, что оставшаяся часть строки — комментарий,
или последовательностями символов /* и */, весь текст между которыми
также считается комментарием:
// это одна строка комментария
Этот код не входит в комментарий
/* это много строк комментария
Эта строка - часть комментария
*/
Пример 4: hello.cpp
#include <iostream>
using namespace std;
int main ()
{
// cout << "HEY, you, I'm alive! Oh, and Hello
// World!\n";
}
54 Глава 2. Основы C++
Проверьте себя
1. Какое значение возвращает операционной системе программа после
успешного выполнения?
А. –1
Б. 1
В. 0
Г. Программы не возвращают значения.
2. Как называется единственная функция, входящая во все программы
на C++?
А. start()
Б. system()
В. main()
Г. program()
3. Какие знаки пунктуации показывают начало и конец блоков кода?
А. { }
Б. -> и <-
56 Глава 2. Основы C++
В. BEGIN и END
Г. ( и )
4. Какой знак пунктуации ставится в конце большинства строк кода C++?
А. .
Б. ;
В. :
Г. '
5. Какая из приведенных ниже конструкций является правильным ком-
ментарием?
А. */ Comments */
Б. ** Comment **
В. /* Comment */
Г. { Comment }
6. Какой заголовочный файл необходим для доступа к объекту cout?
А. stream
Б. Никакой, поскольку объект cout доступен по умолчанию.
В. iostream
Г. using namespace std;
(Решения см. на с. 450.)
Практические задания
1. Напишите программу, которая выводит ваше имя.
2. Напишите программу, которая выводит на экран несколько строк, со-
держащих имя вашего друга.
3. Последовательно закомментируйте каждую из строк первой созданной
нами программы и проверьте, компилируется ли программа. Изучите
появляющиеся ошибки. Ясен ли вам их смысл? Понимаете ли вы, по-
чему изменения, внесенные в код, приводят к этим ошибкам?
Г лава 3
Взаимодействие с пользователем
и работа с переменными
Знакомство с переменными
Для взаимодействия с пользователем программа должна принимать
входные данные — информацию, которая поступает извне программы.
Входные данные надо где-то сохранять; в программировании входные
и прочие данные хранятся в переменных. Существует несколько типов
переменных, которые содержат различные виды информации (например,
числа и буквы). Говоря компилятору, что объявляете переменную, вы
должны указать соответствующий ей тип данных, или просто тип, и имя
переменной.
Основными типами данных являются символы char, целые числа int
и вещественные числа с двойной точностью double. Переменная типа char
хранит один символ, переменная типа int — целое число (без десятич-
ных знаков), а переменная типа double — число с десятичными знаками
(странное имя, верно?). Типы переменных являются ключевыми словами,
которые используются при объявлении переменной.
58 Глава 3. Взаимодействие с пользователем и работа с переменными
Использование переменных
Итак, вы знаете, как сообщить компилятору о переменных, но как исполь-
зовать сами переменные?
Для чтения входных данных используется объект cin (читается «си ин»),
за которым следует оператор вставки, имеющий противоположное на-
правление (>>), и переменная, в которую помещается значение, вводимое
пользователем.
Вот пример программы, который демонстрирует использование пере-
менной:
Пример 5: readnum.cpp
#include <iostream>
using namespace std;
int main ()
{
int thisisanumber;
cout << "Please enter a number: ";
cin >> thisisanumber;
cout << "You entered: " << thisisanumber << "\n";
}
Пример 6: calculator.cpp
#include <iostream>
using namespace std;
int main()
{
int first_argument;
int second_argument;
cout << "Enter first argument: ";
cin >> first_argument;
cout << "Enter second argument: ";
cin >> second_argument;
cout << first_argument << " * " << second_argument
<< " = " << first_argument * second_argument
<< endl;
cout << first_argument << " * " << second_argument
<< " = " << first_argument + second_argument
<< endl;
cout << first_argument << " / " << second_argument
<< " = " << first_argument / second_argument
<< endl;
cout << first_argument << " - " << second_argument
<< " = " << first_argument - second_argument
<< endl;
}
Правильное и неправильное использование переменных 63
int x;
int y;
y = 5;
x = x + y;
Чувствительность к регистру
Пришло время рассмотреть еще одну важную концепцию, способную
легко сбить вас с толку, — чувствительность к регистру. В языке C++
буквы в верхнем и нижнем регистре считаются различными. Компилятор
воспринимает Cat и cat как два разных имени. В C++ все ключевые слова
языка, функции и переменные чувствительны к регистру.
Использование одних и тех же символов в разных регистрах при объявле-
нии и использовании переменной (например, X и x) приводит к тому, что
компилятор возвращает ошибку о необъявленной переменной, хотя вы
уверены, что объявили ее.
Хранение строк
Возможно, вы обратили внимание, что все типы данных, которыми мы
пользовались до настоящего времени, содержат очень простые значения —
например, целое число или символ. С помощью этих типов уже можно
многое сделать, но язык C++ предоставляет и другие типы данных1.
Один из самых полезных типов данных — строка. Строка может содер-
жать множество символов. Вы уже видели, как строки отображали текст
на экране:
cout << "HEY, you, I'm alive! Oh, and Hello World!\n";
Пример 7: string.cpp
#include <string>
using namespace std;
int main ()
{
string my_string;
}
На самом деле в C++ можно создавать собственные типы данных, мы еще
1
Пример 8: string.cpp
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string user_name;
cout << "Please enter your name: ";
cin >> user_name;
cout << "Hi " << user_name << "\n";
}
Пример 9: string_append.cpp
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string user_first_name;
string user_last_name;
cout << "Please enter your first name: ";
cin >> user_first_name;
cout << "Please enter your last name: ";
cin >> user_last_name;
string user_full_name =
user_first_name + " " + user_last_name;
cout << "Your name is: " << user_full_name << "\n";
}
ПРИМЕЧАНИЕ
1
Во избежание путаницы в качестве десятичного разделителя мы всюду будем
использовать принятую в программировании точку, а не запятую. — Примеч.
ред.
Как считывать не только строки, но и другие типы данных 69
пользоваться типом char? Ответ заключается в том, что у типа char есть
особый смысл — ввод и вывод информации в виде символов, а не в виде
чисел. Считывая значение в символьную переменную, пользователь
может ввести символ, а когда вы выводите символ, объект cout отобра-
жает символ, соответствующий числу, которое хранится в переменной,
а не само это число. Вы спросите: «Что это значит? Почему числа — это
символы?» Дело в том, что в действительности компьютер хранит не то,
что мы воспринимаем как символ (например, букву a), а число, которое
соответствует этому символу. Существует таблица соответствий чисел
и символов, которая называется таблицей ASCII. Она содержит ин-
формацию о числе, соответствующем тому или иному символу. Выводя
символ, программа не выводит соответствующее ему число, а находит
символ в таблице ASCII1.
1
Таблица ASCII не велика — всего 256 символов. Это означает, что ASCII нельзя
использовать в японском и китайском языках. Для устранения этой проблемы
используется кодировка Unicode, изучение которой выходит за рамки темы
этой книги. Вы можете ознакомиться с ней на сайте http://www.cprogramming.
com/tutorial/unicode.html
70 Глава 3. Взаимодействие с пользователем и работа с переменными
Проверьте себя
1. Какой тип переменной следует использовать для хранения числа
3.1415?
Проверьте себя 71
А. int
Б. char
В. double
Г. string
2. Какой оператор сравнивает две переменные?
А. :=
Б. =
В. equal
Г. ==
3. Как получить доступ к строковому типу данных?
А. Он встроен в язык, поэтому делать ничего не нужно.
Б. Поскольку строки используются при вводе-выводе, необходимо
включить в программу заголовочный файл iostream.
В. Необходимо включить в программу заголовочный файл string.
Г. Язык C++ не поддерживает строки.
4. Какое из приведенных ниже слов не обозначает тип переменной?
А. double
Б. real
В. int
Г. char
5. Как целиком считать строку, введенную пользователем?
А. С помощью cin>>.
Б. С помощью readline.
В. С помощью getline.
Г. Простого способа нет.
6. Что будет отображено на экране для выражения C++:
cout << 1234/2000?
А. 0
Б. .617
В. Примерно .617, но результат невозможно сохранить с абсолютной
точностью в числе с плавающей точкой.
Г. Это зависит от типов обеих сторон выражения.
7. Почему в языке C++ используется тип char, когда уже есть тип int?
А. Потому что символы и целые числа предназначены для совершенно
разных данных, чисел и букв.
Б. Для обратной совместимости с языком C.
72 Глава 3. Взаимодействие с пользователем и работа с переменными
Практические задания
1. Напишите программу, которая выводит ваше имя.
2. Напишите программу, которая считывает два числа и складывает их.
3. Напишите программу, которая делит два числа, введенные пользовате-
лем, и выводит точный результат. Проверьте работу программы, вводя
как целые значения, так и значения с плавающей точкой.
Г лава 4
Условные операторы
или
если(<выражение истинно>)
{
выполнить все операторы этого блока
}
Выражения
Условные операторы проверяют единственное выражение. Выражение —
это оператор или последовательность взаимосвязанных операторов, вы-
числяющие единственное значение. В большинстве ситуаций, где исполь-
зуются переменные или постоянные значения (например, числа), можно
использовать и выражения. На самом деле и переменные и константы
представляют собой простые выражения. Операции, такие как сложение
и умножение, тоже являются выражениями, хотя и чуть более сложными.
Когда результат выражения используется в контексте сравнения (напри-
мер, в условном операторе), он преобразуется в значение true (истина)
или false (ложь).
никогда.
http://www.bartleby.com/101/625.html
1
76 Глава 4. Условные операторы
В языке C++ есть специальные ключевые слова, true и false, которые мож-
но использовать в коде. Если вывести целое число, соответствующее true,
вы увидите единицу, целое число, соответствующее false, разумеется, ноль.
Сравнивая числа, оператор сравнения возвращает true или false. Напри-
мер, результатом проверки 0 == 2 является false. (Обратите внимание:
при проверке равенства используется двойной знак равенства ==. Один
знак равенства присваивает значение переменной.)
Результатом проверки 2 == 2 является true. Если операция сравнения
используется в условном операторе, нам не нужно проверять ее результат:
if(x == 2)
эквивалентно
if(( x == 2) == true)
Тип bool
Язык C++ позволяет хранить результаты сравнений с помощью специ-
ального типа bool1. Тип bool не очень отличается от целого, но может при-
нимать лишь два возможных значения: true и false. Эти ключевые слова
и тип bool проясняют намерения. Результат всех операторов сравнения
логический (булев).
Тип bool получил свое название в честь Джорджа Буля (George Boole), ма-
1
int x;
cin >> x;
bool is_x_two = x == 2;
// двойной знак равенства для сравнения
if(is_x_two)
{
// выполните некое действие, поскольку x равен 2!
}
Операторы else
Часто требуется, чтобы программа сначала сравнила значения друг с дру-
гом, а затем выполнила одно действие, если сравнение истинно (например,
пользователь ввел правильный пароль), и другое, если сравнение ложно
(пароль неверен).
Оператор else позволяет создавать конструкции вида «если/иначе». Код,
следующий за ключевым словом else (как одна строка, так и несколько
строк в скобках), выполняется, если условие оператора if ложно. Вот при-
мер, который проверяет, отрицательно ли число, введенное пользователем.
Else-if
Оператор else используется и в ситуациях, когда в программе есть несколь-
ко условных операторов, каждый из которых имеет истинное значение, но
при этом необходимо выполнить тело только одного оператора. Например,
нужно изменить приведенный ранее код так, чтобы он обнаруживал три
78 Глава 4. Условные операторы
Сравнение строк
Строковые объекты С++ позволяют использовать все операторы сравне-
ния, о которых шла речь ранее в этой главе. Сравнивая строковые объекты,
можно написать собственную систему проверки паролей.
string password;
cout << "Enter your password: " << "\n";
getline(cin, password, '\n');
if(password == "xyzzy")
{
cout << "Access allowed" << "\n";
}
else
{
cout << "Bad password. Denied access!" << "\n";
// returning is a convenient way to stop the
// program
return 0;
}
// continue onward!
}
Логическое НЕ
Оператор логического НЕ принимает единственный аргумент. Если он
истинен, оператор возвращает ложь, и наоборот. Например, НЕ (истина) =
ложь, а НЕ (ложь) = истина. НЕ (любое ненулевое число) является ложью.
В языке C++ символ логического НЕ — ! (восклицательный знак). Пример:
if(!0)
{
cout << "!0 evaluates to true";
}
Логическое И
Логическое И возвращает истину (true), только если оба операнда истин-
ны. Если один из операндов ложен, логическое И возвращает ложь (false).
Любое ненулевое число И «ложь» равняется «лжи».
В языке C++ оператор «логическое И» обозначается как &&. Он не проверяет
равенство чисел, а определяет, истинны ли оба его аргумента.
if(1 && 2)
{
cout << "Both 1 and 2 evaluate to true";
}
z z Сокращенные проверки
Если первое выражение, аргумент логического И, ложно, значение второго
выражения не вычисляется. Другими словами, выполняется сокращенная
проверка.
Сокращенные проверки полезны, когда второе выражение требуется
проверять лишь при истинности первого выражения — например, для
предотвращения деления на ноль. Рассмотрим условный оператор, который
сравнивает результат деления числа 10 на x с числом 2:
if(x != 0 && 10 / x < 2)
{
cout << "10 / x is less than 2";
}
if(x != 0)
{
if(10 / x < 2)
{
cout << "10 / x is less than 2";
}
}
Логическое ИЛИ
Логическое ИЛИ возвращает истину, если хотя бы один операнд — истина.
Например, истина ИЛИ ложь равно истине, а ложь ИЛИ ложь — лжи. Логиче-
ское ИЛИ обозначается символами || (две вертикальные черты). На кла-
виатуре черта может быть изображена с небольшим разрывом посередине,
хотя в большинстве шрифтов она непрерывна. На многих клавиатурах она
находится там же, где и символ \, и ее можно ввести, нажав Shift.
В операции логического ИЛИ, как и в логическом И, используется со-
кращенная проверка: если первое условие истинно, второе не проверяется.
Комбинация выражений
Базовые логические операторы позволяют проверить два условия одно-
временно. Что делать, если нужны более широкие возможности? Помните,
что выражения можно составлять из переменных, операторов и значений?
Выражения можно составлять и из других выражений.
Например, можно проверить, что значение x равно 2 и значение y равно 3,
объединив операторы проверки равенства логическим И:
x == 2 && y == 3
z z Порядок вычислений
В C++ операторы имеют приоритет, в соответствии с которым происходит
их вычисление. Приоритеты арифметических операторов (+, -, / и *) такие
же, как в обычной арифметике: умножение и деление выполняются раньше
сложения и вычитания.
Среди логических операторов первым вычисляется НЕ, затем следуют
операторы сравнения, далее — логическое И и, наконец, логическое ИЛИ.
Приоритеты логических операторов и операторов сравнения представлены
в следующей таблице.
!
==, <, >, <=, =>, !=
&&
||
Оно равно true, поскольку true && false равно false, а ! false равно true.
Вот несколько задач (ответы даны в сносках):
! ( true || false )1
! ( true || true && false )2
! ( ( true || false ) && false )3
Проверьте себя
1. Какое из значений эквивалентно true?
А. 1
Б. 66
В. .1
Г. -1
Д. Все
2. Как обозначается оператор логического И?
А. &
Б. &&
В. |
Г. |&
3. Чему равно значение выражения !(true && ! (false || true))?
А. true
Б. false
false.
1
true.
3
84 Глава 4. Условные операторы
Практические задания
1. Спросите возраст двух пользователей и покажите, кто старше; если оба
пользователя старше 100 лет, программа выполняет другое действие.
2. Реализуйте простую систему проверки паролей, которая принимает
пароль в виде числа. Сделайте так, чтобы любое из двух чисел было
правильным, но для проверки используйте только один условный
оператор.
3. Напишите простой калькулятор, который в качестве входных данных
принимает одну из четырех арифметических операций и два ее аргу-
мента, а затем выводит ее результат.
4. Расширьте программу проверки паролей, приведенную ранее в этой
главе. Программа должна принимать несколько имен пользователей с
собственными паролями и проверять правильность введенного соче-
тания имени и пароля. Дайте пользователю повторную попытку войти
в систему, если первая попытка оказалась неудачной. Подумайте, на-
сколько легко (или сложно) это сделать для большого количества имен
пользователей и паролей.
5. Подумайте, какие конструкции и возможности языка могли бы упро-
стить добавление новых пользователей без повторной компиляции
программы проверки паролей. (Примечание: не пытайтесь решить эти
задачи с помощью уже изученных средств C++, подумайте об использо-
вании инструментов, которые будут рассмотрены в следующих главах.)
Г лава 5
Циклы
Циклы while
Циклы while — самые простые. Их базовая структура имеет следующий вид:
while(<условие>)
{
[код, который выполняется, пока соблюдается условие]
}
ВНИМАНИЕ
Типичная ошибка
Теперь пришло время сказать, что типичная причина зацикливания про-
граммы состоит в том, что в условии цикла используется не один, а два
знака равенства:
НЕКОРРЕКТНЫЙ КОД
int i = 1;
while (i = 1)
{
cin >> i;
}
Циклы for 87
а не так:
i == 1
Циклы for
Циклы for очень гибки и удобны. Синтаксис цикла for выглядит так:
(инициализация переменной; условие; обновление переменной).
{
// код, который выполняется при соблюдении условия
}
88 Глава 5. Циклы
Инициализация переменной
Инициализация переменной, в данном случае int i = 0, позволяет объ-
явить переменную и присвоить ей значение (или присвоить значение уже
существующей переменной). Здесь мы объявили переменную i. Если в ци-
кле проверяется значение единственной переменной, в данном случае i,
эту переменную иногда называют переменной цикла. В программировании
в качестве переменных цикла традиционно используются i и j. Переменная,
которая увеличивается на единицу на каждой итерации цикла, называется
счетчиком цикла.
Условие цикла
Условие цикла указывает программе, что она должна повторять цикл, пока
условное выражение истинно (аналогично циклу while). В данном случае
мы проверяем, меньше ли значение x, чем 10. Как и в цикле while, условие
впервые проверяется еще до выполнения тела цикла; затем оно проверя-
ется после каждого выполнения цикла, чтобы определить, требуется ли
повторить цикл еще раз.
Обновление переменной
Во фрагменте, обновляющем переменную, значение переменной цикла
можно изменить. В этом фрагменте можно выполнять команды вроде i++,
i = i + 10 или вызывать функции; можно даже вызвать функцию, которая
ничего не делает с переменной, но выполняет какой-либо полезный код.
Поскольку очень многие циклы работают с одной переменной, одним усло-
вием и одним обновлением переменной, оператор for позволяет компактно
описать цикл, разместив его основные элементы в одной строке.
Обратите внимание, что элементы этой строки разделены точками с запя-
той; эти точки с запятой обязательны. Любые элементы (хоть все) могут
быть пустыми, однако точки с запятой должны присутствовать. Незаданное
условие считается истинным, и цикл выполняется до принудительной
остановки; это еще один способ написать бесконечный цикл.
Циклы for 89
Циклы do-while
Циклы do-while имеют особое назначение и используются довольно редко.
Их основная цель — упростить написание тел циклов, которые выполня-
ются хотя бы один раз.
Структура цикла do-while имеет вид
do
{
// тело...
} while ( условие );
Тело этого цикла будет выполнено как минимум один раз, что даст поль-
зователю возможность ввести пароль. Если пароль неверен, цикл будет
повторяться и предлагать пользователю ввести пароль, пока не введен
правильный пароль.
Обратите внимание, что в приведенном примере после слова while следует
завершающая точка с запятой. Эту точку с запятой легко забыть, поскольку
ее не нужно использовать в других циклах, что приводит к путанице.
Управление циклами
Хотя выход из цикла обычно выполняется при проверке его условия,
иногда требуется делать это раньше. Для этой цели в C++ предусмотрено
ключевое слово break. Оператор break немедленно завершает любой цикл,
в котором вы находитесь.
Ниже приведен пример, в котором оператор break используется для пре-
кращения цикла, который в его отсутствие был бы бесконечным. Этот при-
мер является переработанной программой проверки пароля пользователя.
int i = 0;
while (true)
{
i++;
if (i == 10)
{
continue;
}
cout << i << "\n";
}
while (true)
{
cin >> input;
if (! isValid(input))
{
continue;
}
// продолжение обычной обработки входных данных
}
Вложенные циклы 93
Вложенные циклы
В языке C++ очень часто встречаются циклы с использованием не одной,
а двух взаимосвязанных переменных. Например, на интернет-форуме
можно опубликовать список сообщений (один цикл), а для каждого со-
общения — набор его элементов: тему, автора и тело (второй цикл). Однако
необходимо, чтобы второй цикл выполнялся внутри первого — по одной
итерации на каждое сообщение. Такие циклы называются вложенными,
поскольку один цикл находится внутри другого.
Рассмотрим пример, который не так сложен, как публикация сообщений
на форуме. Отличный способ применить вложенные циклы — вывести на
экран таблицу умножения.
Циклы for
Цикл for следует использовать, если точно известно число итераций цик-
ла. Например, он отлично подойдет для счета от 0 до 100 или заполнения
таблицы умножения. Циклы for также являются стандартным способом
обработки массивов, мы рассмотрим ее при изучении массивов (см. главу 10
«Массивы»). Цикл for не рекомендуется использовать, если его переменная
обновляется по сложному принципу; преимущество цикла for в том, что
его работа описывается одним коротким оператором. Если для обновления
цикла требуется несколько строк кода, вы лишаетесь этого преимущества.
Циклы while
Если цикл содержит сложное условие или для определения следующего
значения его переменной требуются объемные математические вычисления,
воспользуйтесь циклом while. Циклы while позволяют легко определить,
когда они завершатся, но обнаружить изменения на каждой их итерации
гораздо труднее. Если изменения сложны, лучше использовать цикл while,
поскольку человек, изучающий код, будет по крайней мере знать, что цикл
обновляется непросто.
Как выбрать подходящий цикл 95
int j = 5;
for (int i = 0; i < 10 && j > 0; i++)
{
cout << i * j;
j = i - j;
}
Циклы do-while
Как я уже сказал, эти циклы являются белыми воронами в программиро-
вании и встречаются крайне редко. Единственная ситуация, в которой есть
смысл использовать цикл do-while, — если вы хотите, чтобы действие было
выполнено как минимум однажды. Хорошим примером такой ситуации яв-
ляется приведенный выше код, запрашивающий пароль пользователя, или,
вообще говоря, любой пользовательский интерфейс, который предлагает
пользователю вводить данные до тех пор, пока они не будут корректными.
Циклы do-while неоптимальны в ситуациях, когда тело цикла повторяется,
но первая итерация немного отличается от остальных — например, при
отображении сообщения, что введенный пользователем пароль неверен.
Как бы вы написали следующий код, используя цикл do-while?
string password;
cout << "Enter your password: ";
cin >> password;
while (password != "xyzzy")
продолжение
96 Глава 5. Циклы
{
cout << "Wrong password--try again: ";
cin >> password;
}
string password;
do
{
if (password == "")
{
cout << "Enter your password: ";
}
else
{
cout << "Wrong password--try again: ";
}
cin >> password;
} while (password != "xyzzy");
Проверьте себя
1. Каким будет окончательное значение переменной x после выполнения
кода int x; for(x=0; x<10; x++) {}?
А. 10
Б. 9
В. 0
Г. 1
2. При каком условии будет выполнен блок кода, следующий за операто-
ром while(x<100)?
А. Если x меньше 100.
Б. Если x больше 100.
В. Если x равно 100.
Г. В любом случае.
3. Что из перечисленного ниже не является структурой цикла?
А. for
Б. do-while
В. while
Г. repeat until
Как выбрать подходящий цикл 97
Практические задания
1. Напишите программу, которая выводит весь текст песни "99 Bottles of
Beer"1.
2. Напишите программу-меню, которая предлагает пользователю выбрать
один вариант списка. Если пользователь вводит вариант, которого
в списке нет, список выводится заново.
3. Напишите программу, которая накапливает сумму чисел, вводимых поль-
зователем. Программа завершает работу, если пользователь вводит ноль.
4. Напишите программу, которая запрашивает у пользователя пароль
с ограниченным числом попыток ввода, чтобы пользователь не мог
легко создать программу для взлома пароля.
5. Решите эти задачи, поочередно используя все типы циклов; опреде-
лите, какие циклы наилучшим образом подходят для каждой из задач.
6. Напишите программу, которая отображает квадраты первых 20 целых
чисел.
7. Напишите программу, которая подсчитывает результаты опроса с тремя
возможными значениями. Первым входным параметром программы
является вопрос, остальные три параметра — варианты ответа на него.
Первому ответу соответствует число 1, второму — число 2, третьему —
число 3. Ответы принимаются, пока не будет введен 0. После этого
программа отображает результаты опроса. Создайте столбчатую диа-
грамму, которая показывает правильно масштабированные результаты
независимо от количества введенных результатов.
1
Текст песни можно найти на странице http://en.wikipedia.org/wiki/99_Bottles_of_Beer
Г лава 6
Функции
Синтаксис функций
Вы уже знаете, как создать функцию, ведь все ваши программы содержат
функцию main.
Рассмотрим устройство функции на другом примере:
int main ()
{
int result = add( 1, 2 ); // вызываем add и присваиваем
// результат переменной
cout << "The result is: " << result << '\n';
cout << "Adding 3 and 4 gives us: " << add( 3, 4 );
}
Локальные переменные
Возьмем простую функцию:
int addTen(int x)
{
int result = x + 10;
return result;
}
int getValueTen()
{
int result = 10;
return result;
}
int addTen(int x)
{
int result = x + getValueTen();
return result;
}
Глобальные переменные
Иногда необходимо использовать единственную переменную, которая
доступна всем вашим функциям. Например, если вы создаете настольную
игру, игровое поле имеет смысл хранить как глобальную переменную, чтобы
Локальные и глобальные переменные 103
Проблема в том, что в момент вызова функция add еще не была объявлена,
а следовательно, ее вызов оказался вне области ее видимости. Когда вы
пытаетесь вызвать необъявленную функцию, компилятор оказывается
в полном недоумении!
Эту проблему можно решить, поместив функцию выше всех ее вызовов, как
я делал в предыдущих примерах. Еще одно решение — объявить функцию
до ее определения.
Хотя объявление и определение функции звучат очень похоже, они имеют
абсолютно разный смысл. Разберемся в этих терминах.
гораздо приятнее, чем код, который реализует все нюансы ввода данных.
Если в процессе работы над кодом вы обнаруживаете, что в нем трудно
уловить общий смысл, стоит подумать, не структурировать ли его в виде
функций.
При написании функции вы концентрируетесь на ее входных и выходных
данных и избавляетесь от необходимости держать в голове все детали про-
граммы в целом.
Вы спросите: «А разве не надо уделять внимание деталям?» В действитель-
ности иногда нужно знать программу до мелочей, но эти мелочи гораздо
проще понять, если вся информация о конкретной функции собрана в од-
ном месте. Если же мелочи разбросаны по разным частям большой про-
граммы, понять ее очень трудно.
Например, рассмотрим программу-меню, которая выполняет сложный
код, когда пользователь выбирает один из пунктов. Программа должна
108 Глава 6. Функции
содержать функции для каждого пункта меню. Смысл пунктов меню ясен
из соответствующих функций. Код, обеспечивающий ввод данных, должен
иметь простую и быстроизучаемую структуру.
Наихудшие программы состоят из единственной обязательной функции
main с многостраничным и хаотичным кодом. Вы ознакомитесь с примером
такой программы в следующей главе.
Только не в C++!
C++ позволяет перегружать функции; можно давать одно имя несколь-
ким функциям, если у них разные списки аргументов. Можно написать
следующий код:
int computeTriangleArea (int x1, int y1, int x2, int y2,
int x3, int y3);
и
int computeTriangleArea (int width, int height);
computeTriangleArea( 1, 1, 1, 4, 1, 9 );
computeTriangleArea( 5, 10 );
Кратко о функциях
Функции, как и переменные, циклы и условные операторы, — один из
базовых инструментов языка C++. Функции за простым интерфейсом
скрывают сложные вычисления и содержат многократно повторяющиеся
фрагменты кода программы, что существенно упрощает повторное ис-
пользование этого кода в будущем.
Проверьте себя
1. Что из перечисленного ниже не является правильным прототипом?
А. int funct(char x, char y);
Б. double funct(char x)
В. void funct();
Г. char x();
2. Какой тип имеет значение, возвращаемое функцией с прототипом int
func(char x, double v, float t);?
А. char
Б. int
В. float
Г. double
3. Что из нижеперечисленного является корректным вызовом функции
(при условии, что функция существует)?
А. funct;
Б. funct x, y;
В. funct();
Г. int funct();
4. Что из нижеперечисленного является законченной функцией?
110 Глава 6. Функции
А. int funct();
Б. int funct(int x) {return x=x+1;}
В. void funct(int) {cout<<"Hello"}
Г. void funct(x) {cout<<"Hello";}
(Решения см. на с. 454.)
Практические задания
1. Структурируйте «программу-меню», написанную ранее, в виде последо-
вательности вызовов функций для каждого из пунктов меню. Добавьте
калькулятор и «99 Bottles of Beer» в виде двух различных функций,
которые можно вызывать.
2. Структурируйте программу-калькулятор так, чтобы каждый тип вы-
числений выполнялся в отдельной функции.
3. Измените написанную ранее программу ввода пароля так, чтобы про-
верка пароля выполнялась в специальной функции отдельно от осталь-
ной части программы.
Г лава 7
Переключатели и перечисления
Переключатель
Переключатели заменяют длинные условные операторы и сравнивают
единственную переменную с несколькими целочисленными значениями.
Целочисленное значение — это число, которое может быть представлено
как целое, например int или char.
Ниже представлен базовый формат переключателя. Значение переменной,
помещенной в переключатель (switch), сравнивается со всеми значениями
вариантов (case), и при совпадении компьютер продолжает выполнять
программу от точки, где было обнаружено совпадение, до конца блока
переключателя или до оператора break.
switch (<переменная>)
{
case первое-значение:
// Код, который выполняется, если <переменная> == первое-значение
112 Глава 7. Переключатели и перечисления
break;
case второе-значение:
код, который выполняется, если <переменная> == второе-значение
break;
// ...
default:
// код, который выполняется, если <переменная> нe равна
// никакому из значений
break;
}
НЕКОРРЕКТНЫЙ КОД
int a = 10;
int b = 10;
switch ( a )
{
case b:
// код
break;
}
Проверьте себя
1. Какой знак следует за оператором case?
А. :
Б. ;
В. -
Г. Символ новой строки.
2. Какой оператор необходимо использовать, чтобы избежать сквозного
выполнения операторов case?
А. end;
Б. break;
В. stop;
Г. Нужно поставить точку с запятой.
3. Какое ключевое слово обрабатывает неучтенные варианты?
А. all
Б. contingency
В. default
Г. other
118 Глава 7. Переключатели и перечисления
А. 1
Б. 0
В. Hello World
Г. ZeroHello World
(Решения см. на с. 454.)
Практические задания
1. Перепишите созданную при решении практической задачи программу-
меню из главы 6 «Функции», используя оператор switch-case.
2. Напишите программу, которая выводит на экран весть текст песни The
Twelve Days of Christmas1, с помощью оператора switch-case (совет:
воспользуйтесь сквозным выполнением операторов case).
3. Напишите игру «крестики-нолики» для двух игроков; по возможности
представляйте содержимое игрового поля с помощью перечислений.
http://en.wikipedia.org/wiki/The_Twelve_Days_of_Christmas_(song)
1
Г лава 8
Добавляем в программу случайности
1
StumbleUpon — это веб-сайт со случайной подборкой интересных веб-страниц:
http://www.stumbleupon.com/
120 Глава 8. Добавляем в программу случайности
1
Функция time возвращает число секунд, прошедших с 1 января 1970 года. Это
соглашение заимствовано из операционной системы Unix, и его иногда назы-
вают Unix-временем. В большинстве случаев время хранится в 32-разрядном
целом числе со знаком. У такого формата хранения времени есть интересная
особенность: когда-нибудь это число переполнится, станет отрицательным
Получение случайных чисел в C++ 121
srand(time(NULL1));
Ура! Теперь при каждом запуске программа ведет себя по-разному, а значит,
можно часами получать от нее удовольствие. Каким будет следующее число?
Возможно, это не очень интересно: в конце концов, можно получить число
в очень широких границах. Гораздо интереснее, если вы получаете число
из определенного диапазона. Оказывается, функция rand возвращает
значение от 0 до константы с именем RAND_MAX (ее минимальное значе-
ние равно 32 767). Скорее всего, вам нужен диапазон значительно уже.
Разумеется, можно вызывать функцию rand в цикле, пока она не вернет
требуемое значение:
int randRange (int low, int high)
{
while(1)
{
int rand_result = rand();
if(rand_result >= low
&&
rand_result <= high)
{
return rand_result;
}
}
}
Проверьте себя
1. Что произойдет, если не вызвать функцию srand перед функцией rand?
А. Функция rand завершится ошибкой.
Б. Функция rand будет всегда возвращать 0.
В. Функция rand будет возвращать одну и ту же последовательность
чисел при каждом запуске программы.
Г. Ничего.
2. Для чего следует задавать начальное число, передавая текущее время
в функцию srand?
А. Чтобы программа всегда выполнялась одинаково.
Б. Чтобы генерировать новые случайные числа при каждом запуске
программы.
В. Чтобы компьютер генерировал совершенно случайные числа.
Г. Это уже сделано за вас; если вы хотите, чтобы начальное число всегда
было одним и тем же, нужно лишь вызвать функцию srand.
3. Какой диапазон значений возвращает функция rand?
А. Какой вы хотите.
Б. От 0 до 1000.
В. От 0 до RAND_MAX.
Г. От 1 до RAND_MAX.
4. Какое значение возвращает выражение 11 % 3?
А. 33
Б. 3
В. 8
Г. 2
Случайные числа и отладка 125
Практические задания
1. Напишите программу, которая имитирует подбрасывание монеты.
Запустите ее несколько раз: кажутся ли случайными ее результаты?
2. Напишите программу, которая выбирает число от 1 до 100 и позволяет
пользователю угадать его. Программа должна информировать пользо-
вателя, что предложенное им значение слишком мало, слишком велико
или он угадал задуманное число.
3. Напишите программу, которая решает проблему игры на угадывание.
Сколько угадываний требуется вашей программе?
4. Создайте эмулятор игрового автомата, который отображает игроку
случайные результаты с помощью как минимум трех различных значе-
ний на каждом барабане. Можете не выводить надпись типа «барабан
вращается»; просто генерируйте результат, отобразите его и определите
размер выигрыша (для этого задайте выигрышные комбинации).
5. Напишите программу для игры в покер. Сдайте игроку пять карт, пре-
доставьте возможность брать новые карты и определите комбинацию,
которую ему удалось получить. Подумайте, насколько легко решить
эту задачу. Какие проблемы могут возникнуть с отслеживанием карт,
взятых из колоды? Сравните сложность этой задачи и задачи об игро-
вом автомате.
Г лава 9
Что делать, когда не понятно,
что делать?
Все, что нужно сделать, — проверить отсутствие остатка при делении числа
на делитель:
bool isDivisible (int number, int divisor)
{
return num % divisor == 0;
}
педии: https://ru.wikipedia.org/wiki/RSA
Кратко об эффективности и безопасности 131
Необязательно сразу знать решения для этих фрагментов, хотя иметь в голо-
ве идеи о том, как их решить, конечно, полезно. Важно, чтобы вы понимали,
какие данные нужны подзадаче и к каким результатам она приводит. Если
вы можете написать программу, которая состоит из функций, решающих
эти подзадачи, можете сделать следующий шаг — приступить к созданию
этих функций. Через некоторое время у вас появится исходный код.
Иногда доведется столкнуться с тем, что решить подзадачу по какой-либо
причине невозможно. Спроектировать программу не всегда легко, в про-
тивном случае количество скучающих разработчиков ПО было бы гораз-
до больше. Если у вас возникают проблемы разбиения задачи на части,
вернитесь на шаг назад и придумайте другой способ, который позволяет
решать подзадачи успешнее.
Такой подход к декомпозиции задач называется нисходящим проекти-
рованием. Он обеспечивает весьма мощные возможности разработки
программ. Существует и другой подход, который называется восходя-
щим проектированием. Он предполагает, что сначала разрабатываются
вспомогательные функции, а затем с их помощью решается проблема
масштабнее. При восходящем проектировании некоторые вспомогатель-
ные функции в итоге оказываются ненужными; тем не менее восходящее
проектирование позволяет легче приступить к программированию, по-
скольку в вашем распоряжении оказываются готовые функции. Однако
новичкам, как правило, рекомендуется пользоваться нисходящим под-
ходом, поскольку он позволяет концентрироваться на создании функций,
которые действительно помогают решить поставленную задачу. Вместо
того чтобы строить догадки о том, какие функции окажутся полезными
для решения проблемы, вы точно определяете, какие вспомогательные
функции понадобятся1.
Кроме того, при проектировании необязательно представлять все компо-
ненты программы в исходном коде. Нарисуйте эскиз программы на бумаге
или на доске; вы сможете увидеть, как ее части складываются в целое, не
заботясь о синтаксисе C++ и ошибках компилятора. Проектируя про-
грамму и непосредственно используя исходный код, вы рискуете упустить
из вида ее общую картину из-за того, что внимание сконцентрировано
Видите ли вы закономерность?
1 one
10 ten
101 one hundred one
1001 one thousand one
Что делать, если алгоритм неизвестен 133
Практические задания
1. Создайте исходный код, который преобразует числа от –999 999 до
999 999 в текст на английском языке. (Совет: воспользуйтесь тем, что
переменные целого типа округляют числа, отбрасывая знаки после
десятичной запятой. Помните, что ваш алгоритм не обязан быть уни-
версальным — он должен работать с числами, состоящими не более чем
из шести цифр.)
2. Подумайте, как решить обратную задачу — считывать английский текст
и переводить его в исходный код. Сравните сложность этой и предыду-
щей задач. Как вы обработаете ввод некорректных данных?
3. Разработайте программу, которая находит все числа от 1 до 1000, сумма
простых множителей которых является простым числом (например,
число 12 имеет простые множители 2, 2 и 3, которые в сумме дают про-
стое число 7). Создайте программный код для этого алгоритма. (Совет:
если вы не знаете, как разложить число на простые множители, спроси-
те об этом Google. Говоря, что необязательно знать математику, чтобы
быть программистом, я имел в виду именно это!)
Ч а с ть I I
Работа с данными
и
for(int i = 0; i < 5; i++)
{
card[i] = getRandomCard();
}
Использование массивов
int values[ 10 ];
sum_array( values );
Очень странно, не так ли? Пока просто запомните, что указывать первую
размерность массива не нужно (это можно сделать, но она будет проигно-
рирована).
Я подробнее расскажу о передаче массивов в функции в главе 12 «Введение
в указатели» и объясню арифметику, которая происходит за кулисами,
а пока считайте это синтаксической причудой C++.
Рассмотрим законченную программу, которая демонстрирует функцию
sum_array.
}
return sum;
}
int main()
{
int values[10];
for(int i = 0; i < 10; i++)
{
cout << "Enter value " << i << ": ";
cin >> values[i];
}
cout << sumArray(values, 10) << endl;
}
Сортировка массивов
Вернемся к вопросу, который я уже задавал раньше: как взять 100 значений
и отсортировать их? Базовая структура кода очевидна: понадобится цикл,
который запрашивает у пользователя 100 значений.
Выглядит разумно, не так ли? Есть одна небольшая проблема: как опреде-
лить, в какой момент остановить цикл? Аргументы функции не содержат
информацию о размере массива! Нужно добавить ее и указать размер мас-
сива в вызове функции findSmallestRemainingElement. Обратите внимание:
в этой ситуации мы вынуждены вернуться к исходному коду, созданному
при нисходящем проектировании, и внести в него изменения. Это действия
естественны для процесса проектирования, и вы можете выполнять их
без каких-либо сомнений. Мы избавимся и от жестко заданного размера
массива (100 элементов).
Сортировка массивов 145
int findSmallestRemainingElement (
int array[],
int size,
int index
).
{
int index_of_smallest_value = index;
for (int i = index + 1; i < size; i++)
{
if(array[i]
<
array[index_of_smallest_value])
{
index_of_smallest_value = i;
}
}
return index_of_smallest_value;
}
void sort (int array[], int size)
{
for(int i = 0; i < size; i++)
{
int index =
findSmallestRemainingElement(
array,
size,
i
);
swap(array, i, index);
}
}
Проверьте себя
1. Что из нижеперечисленного является корректным объявлением массива?
А. int my_array[10];
Б. int anarray;
В. anarray{10};
Г. array anarray[10];
2. Какой индекс имеет последний элемент массива, состоящего из 29
элементов?
А. 29
Б. 28
В. 0
Г. Этот индекс определяется программистом.
3. Что из перечисленного является двумерным массивом?
А. array anarray[20][20];
Б. int anarray[20][20];
В. int array[20, 20];
Г. char array[20];
Практические задания 149
Практические задания
1. Напишите функцию insertion_sort, которая использует созданный
нами код для сортировки методом вставки, но работает с массивами
любого размера.
2. Напишите программу, которая принимает 50 значений и выводит
наибольшее, наименьшее и среднее значения, затем сами введенные
значения по одному в строке.
3. Напишите программу, которая определяет, отсортирован ли массив,
и, если он не отсортирован, сортирует его.
4. Напишите программу, которая позволяет двум игрокам играть в крести-
ки-нолики. Программа должна проверять, выиграл ли кто-то из игроков
и заполнено ли игровое поле целиком (в этом случае игра завершается
вничью). Дополнительный вопрос: можете ли вы сделать так, чтобы
программа определяла, что игра завершится вничью, раньше чем поле
окажется целиком заполненным?
5. Создайте игру в крестики-нолики, в которой размер поля больше 3 × 3,
но для выигрыша требуется составить ряд из четырех крестиков или
ноликов. Дайте игрокам возможность выбирать размер поля, когда
программа уже запущена (совет: пока что придется определять габа-
риты игрового поля на этапе компиляции, поэтому есть смысл задать
максимальный размер поля).
150 Глава 10. Массивы
Связывание значений
Теперь, умея хранить единственное значение в массиве, можно писать про-
граммы, работающие с большим объемом данных. С увеличением объема
данных в программе будут появляться фрагменты, связанные друг с дру-
гом, например имена и координаты игроков на экране в видеоигре. Сейчас
можно оформить эти данные в виде трех отдельных массивов:
int x_coordinates[ 10 ];
int y_coordinates[ 10 ];
string names[ 10 ];
Тем не менее придется учитывать, что эти массивы связаны друг с другом,
и, меняя положение элемента в одном из этих массивов, вы должны из-
менить его положение и в двух других.
Если добавить к этим трем значениям четвертое, работать станет очень не-
удобно: придется создать еще массив и синхронизировать его с тремя дру-
гими. К счастью, люди, которые разрабатывают языки программирования,
не мазохисты, поэтому они придумали более удобный способ связывания
значений друг с другом — структуры. Структуры позволяют хранить раз-
личные значения в переменных с одним и тем же именем. Они полезны
для объединения несколько фрагментов данных в группу.
152 Глава 11. Структуры
Синтаксис
Формат определения структуры имеет следующий вид:
struct SpaceShip
{
int x_coordinate;
int y_coordinate;
string name;
}; // <- Notice that pesky semicolon; you must include it
#include <iostream>
using namespace std;
struct PlayerInfo
{
int skill_level;
string name;
};
int main ()
{
// можно создавать массивы из структур
// так же, как из обычных переменных
PlayerInfo players[5];
for(int i = 0; i < 5; i++)
{
cout << "Please enter the name for player : "
<< i << '\n';
// сначала получаем доступ к первому элементу массива с помощью
// обычного синтаксиса массивов; затем получаем доступ к полю
// структуры с помощью синтаксиса '.'
cin >> players[i].name;
cout << "Please enter the skill level for"
<< players[i].name << '\n';
cin >> players[i].skill_level;
}
for(int i = 0; i < 5; ++i)
{
cout << players[i].name
<< " is at skill level "
<< players[i].skill_level << '\n';
}
}
Структура PlayerInfo объявляет, что у нее есть два поля: имя игрока (name)
и уровень его опыта (skill_level). Поскольку структуру PlayerInfo можно
использовать так же, как любой другой тип переменной (например, int),
можно создать массив игроков. Создавая массив структур, вы работаете
с каждым его элементом так же, как с отдельным экземпляром структуры.
Для доступа к имени игрока, хранящемуся в первой структуре массива,
воспользуемся конструкцией players[0].name.
Эта программа считывает два элемента информации о пяти игроках в од-
ном цикле for с помощью массивов и структур и отображает введенную
информацию во втором цикле.
Необходимости использовать несколько взаимосвязанных массивов для
каждого фрагмента данных нет. Не нужно пользоваться отдельными мас-
сивами player_names и player_skill_level.
154 Глава 11. Структуры
Передача структур
Часто приходится создавать функции, которые принимают структуру
в качестве аргумента или возвращают ее. Допустим, вы пишете небольшую
компьютерную игру с космическими кораблями и хотите создать функцию,
которая инициализирует структуру, описывающую нового противника.
struct EnemySpaceShip
{
int x_coordinate;
int y_coordinate;
int weapon_power;
};
EnemySpaceShip getNewEnemy();
Теперь переменную ship можно использовать так же, как любую другую
структурную переменную. Передача структуры в функцию выглядит так:
Проверьте себя
1. Что из перечисленного получает доступ к переменной в структуре b?
А. b->var;
Б. b.var;
В. b-var;
Г. b>var;
2. Что из перечисленного является корректно определенной структурой?
А. struct {int a;}
Б. struct a_struct {int a};
В. struct a_struct int a;
Г. struct a_struct {int a;};
3. Что из перечисленного объявляет структурную переменную типа foo
с именем my_foo?
А. my_foo as struct foo;
Б. foo my_foo;
В. my_foo;
Г. int my_foo;
4. Каково окончательное значение, получаемое в результате выполнения
следующего кода?
#include <iostream>
using namespace std;
struct MyStruct
{
int x;
};
void updateStruct (MyStruct my_struct)
Практические задания 157
{
my_struct.x = 10;
}
int main()
{
MyStruct my_struct;
my_struct.x = 5;
updateStruct(my_struct);
cout << my_struct.x << '\n';
}
А. 5
Б. 10
В. Этот код не скомпилируется.
(Решения см. на с. 457.)
Практические задания
1. Напишите программу, которая позволяет пользователю заполнить
структуру, содержащую имя, адрес и номер телефона одного человека.
2. Создайте массив объектов космических кораблей и напишите про-
грамму, которая периодически обновляет их местоположение, пока они
не переместятся за пределы экрана. Считайте, что экран имеет размер
1024 × 768 пикселей.
3. На основе задачи 1 создайте программу для ведения адресной книги;
здесь пользователь должен иметь возможность не только заполнять
единственную структуру, но и добавлять новые записи с отдельными
именами и номерами телефона. Дайте пользователю возможность вве-
сти столько записей, сколько он хочет. Легко ли это сделать? Возможно
ли это вообще?
4. Добавьте возможность отображения всех записей и отдельных записей,
которые пользователь выбирает из их списка.
5. Напишите программу, которая позволяет вводить результаты игр, фик-
сируя имена игроков и их результаты. Добавьте возможность отобра-
жения лучших результатов каждого игрока, всех результатов данного
игрока, всех результатов всех игроков и списка игроков.
Г лава 1 2
Введение в указатели
1
На самом деле эта схема относится только к 32-разрядным компьютерам
(32 бита — это 4 байта), в которых большинство встроенных операций про-
цессора работает с 4-байтными значениями. Но даже на таких компьютерах
есть переменные, размер которых превышает 4 байта (например, double). Тем
не менее для простоты мы не будем уделять большого внимания этим деталям.
2
Шестнадцатеричные числа представлены в системе с основанием 16, например
0x10ab0200. Символы 0x указывают на шестнадцатеричный формат числа,
а буквы от A до F соответствуют цифрам от 10 до 15.
Что такое память 161
Переменные и адреса
Возможно, вас несколько запутывает различие переменной и адреса. Пере-
менная представляет значение; это значение фактически хранится в опре-
деленном месте памяти (по определенному адресу). Другими словами,
компилятор использует адреса памяти, чтобы реализовать переменные
в вашей программе. Указатель — это особый тип переменной, который
позволяет хранить адрес другой переменной.
Если адрес переменной известен, с его помощью можно считать значе-
ние этой переменной. Если нужно передать большое количество данных
в функцию (во время выполнения программы), гораздо эффективнее ука-
зать их местоположение, а не копировать все эти данные, как мы видели на
примере массивов. Этот подход также позволяет избежать копирования
структур при их передаче в функции. Основная идея в том, чтобы взять
адрес, по которому хранятся данные, связанные со структурной пере-
менной, и передать его в функцию вместо того, чтобы копировать данные
структуры.
Самая важная функция указателей — дать возможность в любое время
получить от операционной системы дополнительную память. Как это
сделать?1 Операционная система сообщает адрес памяти. Для его сохра-
нения требуется указатель. Если позже понадобится дополнительная па-
мять, можно запросить ее и изменить значение указателя. Таким образом,
с помощью указателей мы можем выходить за пределы фиксированного
объема данных и решать, сколько памяти требуется, в процессе выполне-
ния программы.
1
Управляет памятью операционная система, поэтому наше утверждение в целом
верно, однако в действительности существует несколько уровней кода, которые
занимаются выделением памяти. Операционная система является одним из
этих уровней, однако есть уровни выше нее. Я не буду принимать во внимание
эти нюансы, чтобы не вносить путаницу в материал книги. Если вы не поняли
вышеизложенное, не беспокойтесь — будь оно важным, я не писал бы о нем
в сноске. Сейчас перечисленные детали не имеют существенного значения, но
позднее вы поймете их смысл.
162 Глава 12. Введение в указатели
z z Замечание о терминах
Под термином «указатель» понимается:
1) сам адрес памяти;
2) переменная, которая хранит адрес памяти.
Как правило, различие этих понятий непринципиально: передавая пере-
менную-указатель в функцию, вы передаете значение, которое хранится
в указателе, — адрес памяти.
Говоря о местоположении памяти, я называю его адресом памяти или про-
сто адресом. Если же речь о переменной, которая хранит адрес памяти,
я называю ее указателем.
Когда переменная хранит адрес другой переменной, я говорю, что первая
переменная указывает на вторую переменную.
Структура памяти
Откуда именно берется память? Почему вы всегда должны запрашивать
ее у операционной системы?
В Excel есть одна большая группа ячеек, к которым у вас есть доступ.
В памяти компьютера тоже есть большой объем доступного пространства,
однако память имеет иную структуру. Некоторые фрагменты памяти, до-
ступной программе, уже программой используются. Один из фрагментов
хранит переменные, объявленные в функциях, выполняемых в текущий
момент: он называется стеком. Его название обусловлено тем, что при вы-
зове нескольких функций их локальные переменные складываются (stack
up) друг над другом внутри этого фрагмента памяти. Все переменные,
с которыми мы работали до текущего момента, хранились в стеке.
Следующий фрагмент памяти — свободное хранилище, иногда называемое
кучей: оно представляет собой невыделенную память, которую можно за-
прашивать по частям. Этот фрагмент управляется операционной системой:
предоставленная область памяти должна использоваться только кодом,
который ее выделил, или кодом, которому распределитель памяти сообщил
ее адрес. Доступ к этой памяти можно получить с помощью указателей.
Это мощная возможность, однако она накладывает серьезную ответ-
ственность. Память — дефицитный ресурс. Разумеется, ее нехватка не
ощущается столь же остро, как во времена, когда ее объем не измерялся
гигабайтами, однако количество памяти до сих пор ограничено. Про-
грамма должна освобождать элемент памяти, выделенный в свободном
Что такое память 163
z z Некорректные указатели
Одна из возможных причин некорректного доступа к памяти — неиници-
ализированный указатель. Только что объявленный указатель содержит
случайные данные. Он может указывать как на доступную, так и на недо-
ступную память, однако пользоваться им в любом случае весьма опасно.
На самом деле его значение почти так же случайно, как результат работы
генератора случайных чисел. Использование его значения приведет к краху
программы или к повреждению данных. Указатели всегда следует иници-
ализировать до использования.
z z Память и массивы
Помните, я говорил, что запись данных за пределами массива вызывает про-
блемы? Теперь, зная о памяти больше, вы понимаете, какие это проблемы.
С массивом связано некоторое количество памяти, зависящее от его размера.
Пытаясь получить доступ к элементу за пределами массива, вы обратитесь
к памяти, которая с ним не связана: ее содержимое зависит от конкретного
кода и реализации компилятора. В любом случае, использование памяти,
которая не является частью массива, почти наверняка вызовет проблемы.
Проверьте себя
1. Что из нижеперечисленного НЕ является веской причиной для ис-
пользования указателей?
А. Вы хотите, чтобы функция изменяла переданный ей аргумент.
Б. Вы хотите сэкономить место, избегая копирования переменной
большого размера.
166 Глава 12. Введение в указатели
Практические задания
1. Возьмите небольшую написанную вами программу (например, ка-
кую-нибудь практическую задачу из предыдущих глав этой книги).
Найдите в ней все переменные и представьте себе память, которая свя-
зана с каждой из них. Нарисуйте схему с прямоугольниками, аналогич-
ную представленной в этой главе и демонстрирующую связь перемен-
ных и памяти. Подумайте, как можно представить последовательность
переменных, которые не входят в состав одного массива, но занимают
смежные области в памяти.
2. Подумайте, сколько областей памяти необходимо следующей про-
грамме:
int main ()
{
int i;
int votes[ 10 ];
}
Синтаксис указателей
Объявление указателя
В языке C++ имеется специальный синтаксис, который определяет, что
переменная является указателем, и описывает тип памяти, на которую
она указывает.
Объявление указателя выглядит следующим образом:
<тип> *<имя_указателя>;
и
int* p_points_to_integer;
int x;
int *p_x = & x;
*p_x = 2; // начальное значение x равно 2
Использование указателя
Использование указателя требует нового синтаксиса, потому что вы долж-
ны выполнять два разных действия:
1) запрашивать адрес памяти, хранимый в указателе;
2) запрашивать значение, хранимое по этому адресу.
Используя указатель как обычную переменную, вы получаете адрес памяти,
который хранится в указателе.
Следующий фрагмент кода выводит адрес переменной x, на которую ука-
зывает (адрес которой хранит) p_pointer_to_integer:
int x = 5;
int *p_pointer_to_integer = & x;
cout << p_pointer_to_integer; // выводит адрес x
// это эквивалентно cout << & x;
Указатель NULL можно добавить в схему памяти, просто написав, что его
значение равно NULL, а не нарисовав стрелку, указывающую на NULL:
Указатели и функции 175
Указатели и функции
Указатели позволяют передавать в функцию адрес локальной переменной,
а затем изменять ее. Стандартный пример, иллюстрирующий этот меха-
низм, — функция, меняющая местами значения двух переменных.
Ссылки
Иногда нужны лишь некоторые возможности указателей (исключение
копирования больших объемов данных), а не вся их мощь. В таких ситуа-
циях можно воспользоваться ссылками. Ссылка — это переменная, которая
указывает на другую переменную, хранящуюся в той же памяти. Ссылки
используются как и обычные переменные. Ссылку можно представить как
упрощенный указатель, который не требует использования звездочки или
амперсанда для доступа к значению, на которое указывает, и присваивания
ссылке значения. В отличие от указателя, ссылка должна всегда указывать
на доступную память. Ссылки объявляются при помощи амперсанда:
int &ref;
Ссылку можно изобразить и как указатель; разница в том, что при исполь-
зовании ссылки вы получаете значение, хранящееся в памяти, на которую
она ссылается, а не адрес этой памяти:
int x = 5;
int &ref = x;
178 Глава 13. Указатели
Проверьте себя
1. Что из нижеперечисленного является корректным объявлением ука-
зателя?
А. int x;
Б. int &x;
В. ptr x;
Г. int *x;
2. Что из нижеперечисленного является адресом памяти целочисленной
переменной a?
А. *a;
Б. a;
В. &a;
Г. address(a);
3. Что из нижеперечисленного является адресом памяти переменной, на
которую указывает указатель p_a?
А. p_a;
Б. *p_a;
В. &p_a;
Г. address(p_a);
180 Глава 13. Указатели
Практические задания
1. Напишите функцию, которая запрашивает у пользователя его имя и фа-
милию в виде двух отдельных значений. Функция должна возвращать
оба значения вызывающему окружению при помощи дополнительного
параметра — указателя или ссылки, передаваемой в функцию. Решите
задачу, используя сначала указатель, а затем ссылку (подсказка: про-
тотип этой функции похож на прототип функции swap из приведенного
ранее примера).
2. Для функции, написанной в упражнении 1, нарисуйте схему, аналогич-
ную иллюстрирующей функцию swap.
3. Измените программу, написанную в упражнении 1, так, чтобы она за-
прашивала у пользователя фамилию только при условии, что вызыва-
ющее окружение передает в качестве фамилии указатель NULL.
Практические задания 181
Указатели и массивы
Возникает вопрос: как получить дополнительную память с помощью опера-
тора new после запуска программы, если он может лишь инициализировать
один указатель? Дело в том, что указатели могут указывать на последова-
тельность значений; другими словами, с указателем можно обращаться так
же, как с массивом. Массив, по сути, представляет собой набор значений,
которые последовательно расположены в памяти. Поскольку указатель
хранит адрес памяти, он может хранить и адрес первого элемента массива.
Чтобы получить доступ к конкретному элементу массива, нужно отсчитать
фиксированное расстояние от начала массива до интересующего значения.
Этот принцип полезен тем, что можно динамически создавать новые масси-
вы в свободном хранилище, определяя требуемый объем памяти в процессе
выполнения программы. Чуть позже я продемонстрирую это на примере,
а пока разъясню несколько ключевых аспектов.
Массив можно присвоить указателю, не используя оператор получения
адреса:
int numbers[8];
int* p_numbers = numbers;
int main()
{
int next_element = 0;
int size = 10;
int *p_values = new int[size];
int val;
cout << "Please enter a number: ";
cin >> val;
while (val > 0)
{
if(size == next_element + 1)
{
// Теперь нужно реализовать функцию growArray.
// Обратите внимание: мы должны передать
// размер в виде указателя, поскольку необходимо
// следить за размером массива при его
// расширении!
p_values = growArray(p_values, & size);
}
p_values[ next_element ] = val;
next_element++;
cout << "Current array values are: " << endl;
printArray( p_values, size, next_element );
cout << "Please enter a number (or 0 to exit):
";
cin >> val;
}
delete[] p_values;
}
void printArray (int *p_values, int size, int elements_set)
{
cout << "The total size of the array is: " << size
<< endl;
cout << "Number of slots set so far: "
<< elements_set << endl;
cout << "Values in the array: " << endl;
for(int i = 0; i < elements_set; ++i)
{
cout << "p_values[" << i << "] = "
<< p_values[i] << endl;
}
}
Теперь подумаем, как увеличить размер массива. Что для этого нужно
сделать? Нельзя просто попросить систему расширить выделенную нам
память по аналогии с созданием нового столбца в Excel, если требуется
дополнительное место в электронной таблице. Мы должны запросить до-
полнительную память и скопировать в нее имеющиеся данные.
Многомерные массивы 187
Многомерные массивы
Изменение размера одного большого массива — очень полезное действие,
однако иногда придется работать не только с единственным большим
массивом данных. Помните рассмотренную ранее концепцию многомер-
ных массивов? Было бы здорово выбирать размер таких массивов. Такая
возможность существует, и ее изучение не только принесет практическую
пользу, но послужит хорошим упражнением, чтобы тщательно разобраться
в указателях. Тем не менее чтобы освоить эту тему, понадобятся некоторые
дополнительные знания. Речь о них пойдет в следующих двух разделах
этой главы, после чего мы рассмотрим динамическое выделение много-
мерных структур данных.
188 Глава 14. Динамическое выделение памяти
Арифметика указателей
Этот раздел посвящен более глубокому изучению указателей, и потре-
буется напрячь воображение, чтобы освоить его непростые концепции.
Если не удастся понять этот раздел с первой попытки, прочтите его еще
раз. Поняв все его содержание, в том числе выделение памяти двумерным
массивам, вы сможете освоить любые аспекты указателей. Изложенный
материал несколько тяжел для восприятия, и практическая отдача от него
неочевидна, однако разобравшись в нем, вы быстрее изучите оставшуюся
часть книги (поверьте на слово).
Поговорим об адресах памяти и о том, как их представлять. Указатели хра-
нят адреса памяти, которые являются обычными числами. Над указателями,
как и над числами, можно выполнять некоторые математические операции,
например складывать указатель с числом или вычитать один указатель
из другого. Для чего? Прежде всего, чтобы записать блок памяти по из-
вестному смещению. Вы уже много раз делали это, работая с массивами.
Код
int x[10];
x[3] = 120;
[0][0][0][0]
[1][1][1][1]
[2][2][2][2]
[3][3][3][3]
Теперь ясно, что необходимо знать ширину массива: без нее расчет невоз-
можен. Второе измерение и представляет собой ширину массива.
Нельзя провести аналогичный расчет, используя высоту массива, так как
он не согласуется с физическим расположением массива в памяти (высоту
можно было бы использовать, если бы массив располагался в памяти ина-
че — по строкам). По этой причине можно передать в функцию аргумент,
являющийся двумерным массивом с переменной высотой, но его ширина
должна быть известна. На самом деле следует указывать все размерности
многомерного массива, кроме высоты. Одномерный массив можно считать
особым типом массива, единственным измерением которого является высота.
К сожалению, при объявлении двумерного массива необходимо задавать
его ширину, а следовательно, для динамического выделения двумерного
массива с произвольной шириной требуется еще одна возможность языка
C++ — указатели на указатели.
Указатели на указатели
Указатели могут указывать не только на обычные данные, но и на другие
указатели. Указатель — это всего лишь переменная с адресом, к которому
можно получить доступ.
Объявление указателя на указатель выглядит следующим образом:
int **p_p_x;
Заключение об указателях
На первый взгляд тема указателей кажется очень запутанной, однако с ней
вполне можно справиться. Если не удалось понять указатели от начала
до конца, наберитесь терпения, прочитайте эту главу еще раз, пройдите
тест и решите практические задания. Необязательно разбираться во всех
тонкостях работы с указателями, но вы должны понимать синтаксис их
использования и инициализации и принципы выделения памяти.
194 Глава 14. Динамическое выделение памяти
Проверьте себя
1. Что из нижеперечисленного является ключевым словом, с помощью
которого в C++ выделяется память?
А. new
Б. malloc
В. create
Г. value
2. Что из нижеперечисленного является ключевым словом, с помощью
которого в C++ освобождается память?1
А. free
Б. delete
В. clear
Г. remove
3. Какое из перечисленных ниже утверждений верно?
А. Указатели и массивы — это одно и то же.
Б. Массивы нельзя присваивать указателям.
В. Указатели можно использовать как массивы, но указатели и масси-
вы — не одно и то же.
Г. Указатели можно использовать как массивы, но указатели нельзя
выделять так же, как массивы.
4. Каковы окончательные значения переменных x, p_int и p_p_int в сле-
дующем коде? (Поскольку целые числа и указатели — разные типы
данных, этот код не компилируется, однако данное упражнение полезно
выполнить на бумаге, чтобы разобраться, что происходит с нескольки-
ми указателями.)
int x = 0;
int *p_int = & x;
int **p_p_int = & p_int;
*p_int = 12;
**p_p_int = 25;
p_int = 12;
*p_p_int = 3;
p_p_int = 27;
1
Если вы ответили malloc и free, то вы правы, поскольку это функции языка C,
однако эти ответы не относятся к теме данной главы.
Практические задания 195
Практические задания
1. Напишите функцию, которая создает двумерную таблицу умножения
произвольных размеров.
2. Напишите функцию, которая принимает три аргумента, длину, ширину
и высоту, динамически выделяет трехмерный массив с этими размер-
ностями и заполняет его как таблицу умножения. По окончании работы
с массивом освободите занимаемую им память.
3. Напишите программу, которая выводит адреса памяти всех элементов
двумерного массива. Удостоверьтесь, что понимаете, почему выведен-
ные значения образуют последовательность, структуру которой я объ-
яснял ранее.
4. Напишите программу, которая позволяет пользователю отслеживать
время последних разговоров с его друзьями. Пользователи должны
иметь возможность добавлять новых друзей в любом количестве и хра-
нить число дней с момента последнего общения с каждым из друзей.
Предоставьте пользователю возможность обновлять это значение, но не
позволяйте ему вводить некорректные данные (например, отрицатель-
ные числа). Реализуйте возможность отображения списка, отсортиро-
ванного по именам друзей и времени, прошедшему с момента вашего
последнего контакта с ними.
5. Напишите версию игры Connect Four (четыре в ряд) для двух игроков1,
в которой пользователь может задать ширину и высоту поля и оба
игрока по очереди роняют фишки в ячейки. Отображайте одну сторо-
ну поля значками +, другую — значками x, а пустые ячейки помечайте
значками _.
https://ru.wikipedia.org/wiki/Четыре_в_ряд
1
196 Глава 14. Динамическое выделение памяти
Указатели и структуры
Для доступа к полям структуры через указатель вместо оператора . ис-
пользуется оператор ->:
p_my_struct->my_field;
Все поля структуры имеют разные адреса памяти; как правило, они рас-
полагаются в нескольких байтах от начала структуры. Синтаксис со стрел-
кой вычисляет смещение, необходимое для доступа к соответствующему
полю структуры, при этом все остальные свойства указателей сохраняются
(указатель указывает на область памяти, нельзя использовать недействи-
тельные указатели и др.). Синтаксис со стрелкой в точности эквивалентен
конструкции:
(*p_my_struct).my_field;
int weapon_power;
EnemySpaceShip* p_next_enemy;
};
EnemySpaceShip* getNewEnemy()
{
EnemySpaceShip* p_ship = new EnemySpaceShip;
p_ship->x_coordinate = 0;
p_ship->y_coordinate = 0;
p_ship->weapon_power = 20;
p_ship->p_next_enemy = NULL;
return p_ship;
}
void upgradeWeapons(EnemySpaceShip* p_ship)
{
p_ship->weapon_power += 10;
}
int main()
{
EnemySpaceShip* p_enemy = getNewEnemy();
upgradeWeapons(p_enemy);
}
struct EnemySpaceShip
{
int x_coordinate;
int y_coordinate;
int weapon_power;
EnemySpaceShip* p_next_enemy;
};
EnemySpaceShip* p_enemies = NULL;
действия, общие для всех врагов). Эту переменную можно было бы на-
звать p_first или p_head, чтобы показать, что она первый элемент списка.
Вводя в игру нового врага, мы добавляем его в начало списка.
EnemySpaceShip* getNewEnemy ()
{
EnemySpaceShip* p_ship = new EnemySpaceShip;
p_ship->x_coordinate = 0;
p_ship->y_coordinate = 0;
p_ship->weapon_power = 20;
p_ship->p_next_enemy = p_enemies;
p_enemies = p_ship;
return p_ship;
}
Первый проход
Начальным значением переменной p_enemies является NULL; другими
словами, в игре не участвует ни один враг (мы всегда будем использовать
значение NULL, чтобы показать, что находимся в конце списка).
1. Создается новый вражеский корабль p_ship. Теперь у нас есть враг
с именем SHIP 1, которого пока еще нет в списке. На схеме видно, что
указателю p_next_enemy еще не присвоено значение, и он указывает на
неизвестную область памяти.
2. Полю p_next_enemy корабля SHIP 1 присваивается указатель на теку-
щий список врагов, который в данном случае имеет значение NULL.
3. Затем переменной p_enemies присваивается указатель на новый ко-
рабль.
4. Наша функция возвращает указатель p_ship. Вызывающее окружение
может использовать его по своему усмотрению, а переменная p_enemies
обеспечивает доступ ко всему списку, который в текущий момент со-
стоит из одного элемента.
Создание связанного списка 203
Второй проход
В начале второго прохода переменная p_enemies указывает на только что
созданный корабль.
1. Мы создаем новый корабль p_ship: у нас есть второй враг, переменная
p_next_enemy которого снова указывает на неизвестную область памяти.
2. Переменной p_next_enemy присваивается указатель на текущий список
врагов — в данном случае на врага, созданного на первом проходе.
3. Переменной p_enemies присваивается указатель на только что создан-
ный корабль (теперь она указывает на второй корабль, который, в свою
очередь, указывает на первый корабль).
4. Наша функция возвращает указатель p_ship. Вызывающее окружение
может использовать его по своему усмотрению, а переменная p_enemies
обеспечивает доступ ко всему списку, который в текущий момент со-
стоит из двух элементов.
204 Глава 15. Введение в структуры данных с использованием связанных списков
Можно считать, что при каждом добавлении нового элемента в список все
его существующие элементы сдвигаются на одну позицию вниз. При этом
копировать весь список, в отличие от массива, не требуется. Вы просто
обновляете указатель на начало списка, направляя его на новый первый
элемент. Первый элемент списка называется его головой, как правило,
существует указатель на головной элемент списка — в данном случае p_
enemies. В конце функции указатели p_ship и p_enemies указывают на один
и тот же элемент, однако до этого мы использовали указатель p_ship для
хранения новой памяти, чтобы иметь возможность перенаправить указа-
тель p_next_enemy на предыдущую голову списка, хранящуюся в p_enemies.
Хотя написанная мной функция использует глобальную переменную,
можно создать функцию, которая принимает голову списка в качестве
аргумента. Такая функция будет работать с любым списком, а не с един-
ственным глобальным списком. Она может выглядеть следующим образом:
EnemySpaceShip* addNewEnemyToList (EnemySpaceShip* p_list)
{
EnemySpaceShip* p_ship = new EnemySpaceShip;
p_ship->x_coordinate = 0;
p_ship->y_coordinate = 0;
p_ship->weapon_power = 20;
p_ship->p_next_enemy = p_list;
return p_ship;
}
Этот код почти так же короток, как при использовании массива. Перемен-
ная p_current указывает на текущий элемент списка. Сначала она указывает
на первого врага в списке (на которого указывает переменная p_enemies).
Пока значение p_current не равно NULL (то если мы не достигли конца
списка), обновляем оружие текущего врага и присваиваем переменной
p_current указатель на следующего врага в списке.
Как поступить, чтобы добавить в список новый элемент, при этом не на-
рушив сортировку списка? Например, если список состоит из элементов
1, 2, 5, 9, 10 и нужно вставить в него число 6, оно должно оказаться между
элементами 5 и 9. Используя массив, вы должны были бы изменить его
размер, чтобы вставить новый элемент, а затем переместить все элемен-
ты, начиная с 9, в конец списка. Если бы массив содержал еще тысячу
элементов после числа 10, каждый из них пришлось бы сдвинуть на одну
позицию вправо. Другими словами, быстродействие операции вставки
элемента в середину массива пропорционально длине массива. Работая
же со связанным списком, вы переводите указатель элемента 5 на новый
элемент, присваиваете указателю нового элемента адрес элемента 9, и всё!
Длительность этой операции не зависит от длины списка.
Главное преимущество массива перед связанным списком в том, что массив
позволяет очень быстро выбрать любой элемент по его индексу. Чтобы
выбрать элемент связанного списка, необходимо просматривать все его
элементы, пока не найдется нужный. Таким образом, чтобы воспользо-
ваться преимуществами массива, индекс элемента должен быть связан со
значением, хранимым в наборе элементов. В противном случае для поиска
нужного элемента придется просмотреть весь набор.
Например, можно воспользоваться массивом для регистрации результатов
голосования, в котором пользователи отдают предпочтение кандидату, вво-
дя число от 0 до 9. Каждый элемент массива содержит количество голосов,
отданных за кандидата, номер которого соответствует индексу элемента.
Между числами и кандидатами не существует естественной связи, однако
можно присвоить каждому кандидату определенное число, а затем полу-
чить информацию о кандидате с помощью соответствующего ему числа.
Приводим реализацию этого голосования с помощью массива.
z z Прочие соображения
Массивы могут быть многомерными. К примеру, шахматную доску раз-
мером 8 на 8 можно легко представить многомерным массивом. Чтобы
сделать это с помощью связанного списка, придется формировать список,
состоящий из других списков, а доступ к конкретному элементу будет
гораздо медленнее и труднее для понимания.
z z Общие рекомендации
Приводим две рекомендации по использованию связанных списков и мас-
сивов.
Используйте массивы, если необходим доступ к элементам по индексам
при неизменном времени, если заранее известно, сколько элементов нужно
хранить, и если минимизировать объем одного элемента.
Проверьте себя
1. В чем преимущество связанного списка перед массивом?
А. В связанных списках меньше средний размер элемента.
Б. Связанные списки могут динамически расширяться; в них можно
добавлять отдельные новые элементы, не копируя уже существующие.
В. Поиск отдельного элемента в связанных списках быстрее, чем в мас-
сивах.
Г. Элементами связанных списков могут являться структуры.
2. Какое из перечисленных утверждений верно?
А. Нет никаких причин пользоваться массивами.
Б. Характеристики эффективности связанных списков и массивов
одинаковы.
В. Связанные списки и массивы обеспечивают одинаковое время до-
ступа к элементу по его индексу.
Г. Добавление элемента в середину связанного списка требует меньше
времени, чем добавление в середину массива.
3. Когда обычно используется связанный список?
А. Если необходимо хранить единственный элемент.
Б. Если число хранимых элементов известно на этапе компиляции.
В. Чтобы динамически добавлять и удалять элементы.
Г. Чтобы мгновенно получать доступ к любому элементу отсортиро-
ванного списка без каких-либо итераций.
4. Почему можно объявить связанный список со ссылкой на тип его эле-
мента? (struct Node {Node* p_next;};)
Практические задания
1. Напишите программу, которая удаляет элемент из связанного списка.
Функция, выполняющая удаление, должна принимать в качестве аргу-
мента только удаляемый элемент. Легко ли написать такую функцию?
Будет ли она работать быстро? Можно ли упростить или ускорить ее
работу, добавив в список указатель?1
2. Напишите программу, которая добавляет элементы в связанный список
в порядке его сортировки, а не в начало.
1
Подсказка. Что будет, если добавить указатель на предыдущий узел? Поможет
ли он решить задачу?
212 Глава 15. Введение в структуры данных с использованием связанных списков
node *next;
};
node* search(node* list, int value_to_find)
{
if(list == NULL)
{
return NULL;
}
if(list->value == value_to_find)
{
return list;
}
else
{
return search(list->next, value_to_find);
}
}
Factorial(x) =
If(x == 1) 1
Else x * Factorial(x - 1)
int factorial(int x)
{
if(x == 1)
{
return 1;
}
return x * factorial(x – 1);
}
Циклы и рекурсия
В некоторых случаях рекурсивный алгоритм можно легко выразить как
цикл с такой же структурой. Например, поиск в списке можно реализовать
следующим образом:
return NULL;
}
if ( list->value == value_to_find )
{
return list;
}
else
{
list = list->next;
}
}
}
int factorial(int x)
{
int cur = x;
while(x > 1)
{
x--;
cur *= x;
}
return x;
}
Стек
Пришло время подробнее обсудить, как работают вызовы функций, и рас-
смотреть несколько любопытных схем. Освоив принцип действия вызовов
функций вы сможете разобраться в рекурсии и поймете, почему некоторые
алгоритмы проще реализовать с помощью рекурсии, а не циклов.
Вся информация, которой пользуется функция, хранится внутри стека.
Представьте стопку тарелок: можно положить тарелку поверх либо взять
верхнюю тарелку. Стек устроен аналогично, однако он состоит не из таре-
лок, а из стековых фреймов. В момент вызова функции выделяется новый
фрейм на вершине стека и хранит в нем все локальные переменные, кото-
рыми пользуется. Когда эта функция вызывает другую функцию, исход-
ный стековый фрейм сохраняется, а на вершине стека размещается новый
фрейм, в котором вызываемая функция хранит собственные переменные.
Функция, которая выполняется в текущий момент, всегда использует
верхний фрейм стека.
В простейшем случае, когда выполняется только функция main, стек вы-
глядит следующим образом:
Переменные main
Переменные main
Переменные main
buildWall(x = 0)
buildWall(x = 1)
buildWall(height = 2)
main()
Достоинства стека
Ключевая ценность рекурсии в том, что есть стек вызовов функций, а не
единственный стековый фрейм. Рекурсивные алгоритмы используют всю
дополнительную информацию, которая хранится в каждом фрейме, в то
время как у цикла есть лишь один набор локальных переменных. В резуль-
тате рекурсивная функция может дожидаться возврата из рекурсивного
вызова и продолжать выполнение с того места, где она прервалась. Чтобы
создать цикл, способный работать подобным образом, придется реализовать
собственную версию стека.
Недостатки рекурсии
Поскольку стек имеет фиксированный размер, рекурсия не может быть
бесконечной. В какой-то момент в стеке не хватит места для размещения
нового фрейма на его вершине (в шкафу закончится свободное простран-
ство для тарелок).
Ниже приведен пример рекурсии, которая теоретически бесконечна:
void recurse ()
{
recurse(); // Функция вызывает себя
}
int main ()
{
recurse(); // Запускает рекурсию
}
Производительность
Рекурсия подразумевает большое количество вызовов функций. Каждый
вызов функции создает стековый фрейм и передает аргументы, что при-
водит к дополнительным затратам времени, которые отсутствуют при ис-
пользовании циклов. На современных компьютерах этими затратами почти
всегда можно пренебречь, однако если код исполняется с очень высокой
периодичностью (миллионы или миллиарды раз за короткий промежуток
времени), затраты на вызовы функций могут стать заметны.
Подводя итоги
Рекурсия позволяет создавать алгоритмы, которые решают задачи, сводя
их к их собственным уменьшенным копиям. Кроме того, рекурсия обеспе-
чивает более мощные возможности, чем циклы, поскольку хранение теку-
щего состояния каждой рекурсивной функции в стеке позволяет функции
продолжить выполнение после получения результата решения подзадачи.
Рекурсивная реализация алгоритма часто естественнее равнозначной ей
реализации в виде цикла. Мы познакомимся с несколькими соответству-
ющими примерами в следующей главе о двоичных деревьях. По мере на-
копления практического опыта написания программ вы придете к выводу,
что существует широкий круг задач, решать которые с помощью рекурсии
проще, чем лишь с использованием циклов.
Ниже приведены практические советы о том, когда следует использовать
рекурсию и циклы.
Пользуйтесь рекурсией, если:
1) задача разбивается на уменьшенные копии самой себя, и нет очевид-
ного способа решить ее написанием цикла;
228 Глава 16. Рекурсия
Проверьте себя
1. Что такое хвостовая рекурсия?
А. Когда вы подзываете свою собаку.
Б. Когда функция вызывает сама себя.
В. Когда рекурсивная функция вызывает себя непосредственно перед
возвращением.
Г. Когда можно написать рекурсивный алгоритм в виде цикла.
2. Когда можно воспользоваться рекурсией?
А. Когда невозможно написать алгоритм в виде цикла.
Б. Когда алгоритм естественнее выражается в виде подзадачи, а не
цикла.
В. Никогда — это слишком сложно.
Г. При работе с массивами и связанными списками.
3. Каковы неотъемлемые составляющие рекурсивного алгоритма?
А. Базовый вариант и рекурсивный вызов.
Б. Базовый вариант и способ сведения задачи к уменьшенной версии
самой себя.
В. Способ перекомпоновки уменьшенных версий задачи.
Г. Все вышеперечисленное.
4. Что может произойти при неполном базовом варианте?
А. Преждевременное завершение алгоритма.
Б. Ошибка компилятора.
В. Ничего — это не проблема.
Г. Переполнение стека.
(Решения см. на с. 463.)
Практические задания 229
Практические задания
1. Напишите рекурсивный алгоритм, реализующий возведение в степень
с помощью функции pow(x, y) = x^y.
2. Напишите рекурсивную функцию, которая принимает массив и ото-
бражает его элементы в обратном порядке, не индексируя их с конца
массива (другими словами, обойдитесь без написания цикла, который
начинает выводить элементы с конца массива).
3. Напишите рекурсивный алгоритм, удаляющий элементы из связанно-
го списка. Напишите рекурсивный алгоритм, добавляющий элементы
в связанный список. Реализуйте те же алгоритмы с помощью итерации.
Какие реализации естественнее: рекурсивные или итеративные?
4. Напишите рекурсивную функцию, которая ищет элемент в отсортиро-
ванном массиве (отсортированный массив и искомый элемент являют-
ся ее аргументами). Функция возвращает индекс элемента или –1, если
элемент отсутствует в массиве. Насколько быстро можно осуществить
этот поиск? Можете ли вы предложить способ его выполнения, более
эффективный, чем просмотр всех элементов?
5. Напишите рекурсивный алгоритм для задачи Ханойской башни. Вы
можете прочесть описание задачи на сайте http://www.mazeworks.com/
hanoi/index.htm и попытаться решить ее самостоятельно.
Г лава 1 7
Двоичные деревья
ПРИМЕЧАНИЕ
10
14
11 18
для очень больших деревьев (оно равно 32 для дерева с 4 млрд элементов;
поиск в таком дереве будет почти в 100 млн раз быстрее, чем в связанном
списке такого же размера, где придется просматривать каждый элемент).
Тем не менее если дерево не сбалансировано, его деление точно пополам
может оказаться невозможным. В худшем случае у каждого узла будет
один дочерний, и дерево превратится в псевдосвязанный список (с до-
полнительными указателями), время поиска в котором равно n.
Если дерево относительно (необязательно идеально) сбалансировано,
поиск узлов в нем существенно быстрее, чем в связанном списке. Все это
возможно благодаря тому, что вы способны структурировать память по
своему желанию, не ограничиваясь простыми списками1.
Соглашение о терминах
Для удобства изучения примеров кода, работающего с двоичными де-
ревьями, сначала договоримся о принципах изображения и именования
различных частей дерева.
Простейшее дерево — это пустое дерево, которому соответствует значение
NULL. Я не буду изображать ссылки на пустые деревья на схеме.
10
14
11 18
1
Простейшее двоичное дерево редко имеет такую же структуру, как связанный
список, что обусловливается порядком вставки элементов в дерево. Существу-
ют более сложные виды двоичных деревьев, которые всегда сбалансированы,
однако их рассмотрение выходит за рамки этой книги. Примером такой струк-
туры является красно-черное дерево: https://ru.wikipedia.org/wiki/Красно-черное
_дерево
234 Глава 17. Двоичные деревья
Этот узел хранит значение в виде простого целого числа key_value и со-
держит два дочерних поддерева с названиями p_left и p_right.
Основные действия над двоичными деревьями — вставка нового узла,
поиск значения в дереве, удаление узла и уничтожение дерева для осво-
бождения памяти.
node* insert (node* p_tree, int key);
node *search (node* p_tree, int key);
void destroyTree(node* p_tree);
node *remove(node* p_tree, int key);
Вставка в дерево
Начнем с того, что реализуем вставку узла в дерево с помощью рекур-
сивного алгоритма. Рекурсия отлично сочетается с деревьями, поскольку
каждое дерево содержит два дерева меньшего размера, а следовательно,
имеет рекурсивную структуру (если бы дерево включало, скажем, массив
или указатель на связанный список, а не на другие деревья, оно не было
бы рекурсивным).
Наша функция принимает ключ и существующее дерево (возможно, пустое)
и возвращает новое дерево, содержащее вставленное значение.
Реализация двоичных деревьев 235
Базовая логика этого алгоритма такова: если дерево пустое — создать новое
дерево; в противном случае, если вставляемое значение больше текущего
узла, вставить его в левое поддерево и заменить новым поддеревом; в про-
тивном случае вставить значение в правое поддерево и заменить его новым
поддеревом.
Рассмотрим этот код в действии, превратив пустое дерево в дерево с двумя
узлами. При вставке значения 10 в пустое дерево (NULL) мы сразу попадаем
в базовый вариант. В результате получается очень простое дерево:
236 Глава 17. Двоичные деревья
Вызов
insert(NULL, 7)
Затем
insert(<дерево с родителем 5>, 7)
Поиск в дереве
Теперь рассмотрим реализацию поиска в дереве. Ключевая логика поиска
почти в точности повторяет логику вставки в дерево: сначала мы прове-
ряем два базовых варианта (мы нашли узел или ищем в пустом дереве),
а затем выясняем, в каком поддереве выполнять поиск, если не попадаем
в базовый вариант.
238 Глава 17. Двоичные деревья
Уничтожение дерева
Функция destroy_tree также должна быть рекурсивной. Следующий
алгоритм уничтожает два поддерева текущего узла, а затем и сам узел:
destroy_tree(p_tree->p_right);
delete p_tree;
}
}
Чтобы понять этот код, полезно вручную разобрать его работу на примере
пары деревьев.
Удаление узла из дерева — рекурсивный алгоритм, который весьма за-
труднительно реализовать в итеративной форме: придется написать цикл,
который должен каким-либо образом одновременно работать как с левой,
так и с правой ветвью дерева! Проблема в том, что необходимо удалять одно
поддерево, при этом отслеживая второе удаляемое поддерево, на каждом
240 Глава 17. Двоичные деревья
if(p_tree->key_value == key)
{
// что делать?
}
else if(key < p_tree->key_value)
{
p_tree->left = remove(p_tree->left, key);
}
else
{
p_tree->right = remove(p_tree->right, key);
}
return p_tree;
}
Что будет, если удалить элемент 10? Нельзя просто заменить его элемен-
том 6, поскольку дерево примет следующий вид:
14
11 18
Теперь число 8, которое больше числа 6, находится слева от него. Это раз-
рушает дерево: поиск числа 8 будет происходить справа от числа 6 и никогда
не завершится удачно. По этой же причине нельзя взять элемент справа:
14
11
18
Здесь число 11, меньшее 14, находится справа от дерева, что недопустимо.
В двоичном дереве нельзя произвольным образом поднимать узлы.
Что же делать? Значения всех элементов слева от узла должны быть
меньше значения этого узла. Может быть, следует найти максимальное
значение слева от удаляемого узла и переместить его на вершину дерева?
Максимальное значение слева от дерева можно смело перенести на место
текущего узла: оно гарантированно больше любого другого узла слева от
него и, поскольку оно оказалось слева от дерева, с которым мы начали
работать, оно гарантированно меньше любого узла справа от него1.
По этой же причине в правой части дерева нельзя найти узел с наименьшим
1
14
11 18
Обратите внимание, что нам нужны два базовых варианта: один для отсут-
ствия дерева, а другой — для достижения конца списка дочерних деревьев
справа1. Чтобы возвращать указатель на последний узел, нужно заглядывать
на один узел вперед, пока есть действительный указатель.
Итак, попробуем воспользоваться нашими соображениями и написать
функцию remove. Если в базовом варианте функция find_max возвращает
1
В нашей реализации удаления узла проверка первого базового варианта (дерево
пусто) необязательна, но защита от некорректных данных является хорошим
тоном в программировании.
244 Глава 17. Двоичные деревья
Мы должны сделать так, потому что значение max_node максимальное в под-
1
10
14
11 18
Если мы решим удалить из дерева узел 10, функция remove сразу найдет
его. Она обнаружит, что у этого узла есть левое и правое поддерево, и опре-
делит узел с максимальным значением в поддереве с корнем 6. Значение
этого узла равно 8. Затем функция remove свяжет левое поддерево узла 8
с новым поддеревом с корнем 6, которое не содержит узел 8.
Удалить узел 8 из поддерева легко. Мы начинаем с поддерева
14
11 18
На самом деле в словаре хранится не сам пароль, а его хэшированная версия.
1
числе хэш-таблицы.
Проверьте себя 251
(столько же, сколько для поиска узла, поскольку и при поиске, и при до-
бавлении узла дерево делится пополам). Таким образом, для построения
дерева понадобится n log2n операций. При каждом линейном поиске
в связанном списке просматривается в среднем n/2 узлов, поэтому если
вы выполните 2*log2n операций поиска в связанном списке, то потратите
на них примерно столько же времени, сколько требуется для построения
двоичного дерева. (Почему? Суммарное время равно среднему времени
одного поиска, умноженному на количество операций поиска: n/2×2log2n =
= n log2n.) Другими словами, если вы собираетесь воспользоваться двоич-
ным деревом лишь однажды, нет смысла его создавать; если же оно будет
использоваться многократно, создайте его (даже если в словаре будет
миллион узлов, средняя производительность программы увеличится уже
после 40 операций поиска). Для компаний, обрабатывающих миллионы
транзакций по кредитным картам, это однозначно выгодно; для записной
книжки мобильного телефона выгода зависит от количества звонков
и размера адресной книги (выгоду нетрудно подсчитать самостоятельно).
Проверьте себя
1. В чем главное достоинство двоичного дерева?
А. Оно использует указатели.
Б. Оно может хранить любое количество данных.
В. Оно позволяет быстро искать данные.
Г. Из него легко удалить данные.
2. В каких случаях вы предпочтете использовать связанный список вместо
двоичного дерева?
А. Если необходимо обеспечить быстрый поиск данных.
Б. Если необходимо получать доступ к отсортированным элементам.
В. Если необходимо быстро добавлять элементы в начало или конец
структуры, при этом элементы в ее середине никогда не используются.
Г. Если не нужно освобождать используемую память.
3. Какое из нижеперечисленных высказываний верно?
А. Порядок добавления элементов в двоичное дерево может изменить
его структуру.
Б. Чтобы структура двоичного дерева была оптимальной, следует до-
бавлять в него элементы в отсортированном порядке.
В. Поиск элементов в связанном списке будет быстрее, чем в двоичном
дереве, если элементы вставляются в двоичное дерево в случайном по-
рядке.
252 Глава 17. Двоичные деревья
Практические задания
1. Напишите программу, которая отображает содержимое двоичного дере-
ва. Можете ли вы написать программу, которая выводит узлы двоичного
дерева в отсортированном порядке? В обратном порядке сортировки?
2. Напишите программу, которая подсчитывает число узлов двоичного
дерева.
3. Напишите программу, которая проверяет, сбалансировано ли двоичное
дерево.
4. Напишите программу, которая проверяет, правильно ли отсортировано
двоичное дерево (значения всех его узлов слева от определенного узла
меньше значения этого узла, а значения всех узлов справа — больше).
5. Напишите программу, которая удаляет все узлы двоичного дерева, не
используя рекурсию.
6. Реализуйте в виде двоичного дерева простой словарь, который хранит
содержимое адресной книги. Ключом карты должно быть имя человека,
а значением — адрес его электронной почты. Обеспечьте возможность
добавлять, удалять, обновлять и находить в карте адреса электрон-
ной почты. При завершении программы содержимое адресной книги
должно быть очищено. Напоминание: для сравнения двух строк можно
использовать любые стандартные операторы сравнения C++: ==, < и >.
Г лава 1 8
Стандартная библиотека шаблонов
и
#include <vector>
using namespace std;
vector<int> a_vector(10);
Этот код вызывает метод size объекта a_vector и возвращает размер век-
тора. Это немного напоминает доступ к полю структуры, однако вместо
него вызывается метод, принадлежащий этой структуре. Хотя очевидно,
256 Глава 18. Стандартная библиотека шаблонов
аналогичен
size(a_vector);
Словари
Мы уже немного поговорили о концепции словарей, которая позволяет
искать одно значение по другому значению. Этот прием постоянно ис-
пользуется в программировании — например, для поиска электронного
адреса по имени владельца, информации о банковском счете по номеру
счета или для регистрации пользователя в компьютерной игре.
Стандартная библиотека шаблонов поддерживает очень удобный тип map
(словарь), который позволяет задавать типы ключей и значений словаря.
Например, структура данных, хранящая простую адресную книгу (вроде
созданной при работе над упражнениями последней главы), может быть
реализована следующим образом:
#include <map>
#include <string>
using namespace std;
map<string, string> name_to_email;
Всего хорошего!
Проверить размер словаря можно при помощи метода size:
name_to_address.size();
Итераторы
Иногда необходимо не только хранить данные и получать доступ к от-
дельным элементам, но и перебирать все элементы в конкретной структуре
данных. Работая с массивом или вектором, можно прочитать каждый от-
дельный элемент, используя длину массива. А что делать со словарями?
Поскольку в словарях часто используются нечисловые ключи, перебирать
их с помощью переменной-счетчика не всегда возможно.
Итераторы 259
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vector<int>::iterator itr = vec.begin();
Этот код создает итератор, считывает первый элемент вектора целых чи-
сел, и, пока интератор не равен конечному итератору, происходит перебор
вектора и вывод каждого элемента.
Можно внести в этот цикл несколько небольших улучшений. Следует из-
бегать вызова vec.end() на каждой итерации цикла:
vector<int>::iterator end = vec.end();
for(vector<int>::iterator itr = vec.begin();
itr != end;
++itr)
{
cout << *itr << endl;
}
Заключение об STL
Тема STL в этой главе далеко не исчерпана, но теперь у вас достаточно
знаний, чтобы пользоваться многими ее ключевыми типами данных. Тип
vector полностью заменяет массивы и может использоваться вместо связан-
ных списков, если несущественно время вставки или изменения элемента
списка. Массивы очень редко имеют преимущества перед векторами — как
правило, только в сложных ситуациях, например при вводе-выводе файлов.
Тип map, возможно, наилучший из существующих — я постоянно использую
словареподобные структуры, существенно упрощающие сложные про-
граммы и избавляющие от необходимости создавать различные структуры
данных. Словари позволяют концентрироваться на конкретной задаче. Они
часто заменяют двоичные деревья. Как правило, разработчик не реализует
двоичные деревья самостоятельно, если только нет особых требований
к быстродействию программы и структуре дерева. В этом мощь STL:
примерно в 80% случаев библиотека предоставляет ключевые структуры
данных и дает возможность заниматься решением целевой задачи про-
граммы. В остальных 20% случаев надо знать, как строить собственные
структуры данных.
Некоторые программисты страдают синдромом «придумано не здесь» —
стремлением использовать свой, а не чужой код. Как правило, не следует
реализовывать собственные структуры данных, поскольку встроенные
структуры чаще всего работают лучше, быстрее и полнее реализованы.
Тем не менее зная, как их создавать, вы гораздо глубже разбираетесь в их
использовании и при необходимости можете сформировать собственные
структуры данных.
Когда же возникает такая необходимость? Предположим, вы хотите создать
небольшой калькулятор для ввода арифметических выражений и вычис-
лений с соблюдением приоритета операций. Например, калькулятор полу-
чает выражение 5 * 8 + 9 / 3 и при вычислении выполняет умножение
и деление раньше сложения.
Естественный способ решения этой задачи — дерево. Представить выра-
жение 5 * 8 + 9 / 3 в виде дерева можно следующим образом:
Проверьте себя 263
Проверьте себя
1. Когда уместно использовать вектор?
А. Для хранения связи ключа и значения.
Б. Для максимального быстродействия при изменении коллекции
элементов.
264 Глава 18. Стандартная библиотека шаблонов
Практические задания
1. Создайте небольшую программу для работы с адресной книгой, которая
позволяет пользователям вводить имена и адреса электронной почты,
удалять, изменять и перечислять записи в адресной книге. Не беспо-
койтесь о сохранении адресной книги на диск; можно терять введенные
данные при выходе из программы1.
2. Реализуйте список лучших результатов видеоигры с помощью векторов.
Результаты должны обновляться автоматически, а новые результаты до-
бавляться в правильную позицию в списке. Дополнительные операции
над векторами описаны на приведенном выше веб-сайте SGI.
3. Напишите программу, которая предоставляет две функции: регистра-
цию пользователя и вход в систему. Регистрация позволяет пользова-
телю создать логин и пароль, а вход в систему — авторизоваться и по-
лучить доступ к функциям «Изменить пароль» и «Выйти из системы».
Функция «Изменить пароль» дает пользователю возможность задать
новый пароль, а функция «Выйти из системы» возвращает его к ис-
ходному экрану.
Считывание строк
Иногда необходимо целиком считывать строку, введенную пользователем,
а не отдельные слова, разделенные пробелами.
Для этого используется функция getline. Функция getline принимает
входной поток и считывает из него текстовую строку. Примером входного
потока является объект cin, с помощью которого вы обычно считываете
отдельные слова. (Открою маленький секрет: метод cin представляет собой
тип данных «входной поток», аналогичный строке или вектору, а cin>> —
метод, считывающий данные. Объяснять это в первой главе было бы не
лучшей идеей!)
Приведем простой пример, который считывает одну строку, вводимую
пользователем:
if(first_name.size() == 0)
{
break;
}
string last_name;
getline(cin, last_name, ',');
string player_class;
getline(cin, player_class, '\n');
cout << first_name << " " << last_name
<< " is a " << player_class << endl;
}
}
Поиск и подстроки
Класс string поддерживает простой поиск и получение подстрок при
помощи методов find, rfind и substr. Метод find принимает подстроку
и позицию в исходной строке и обнаруживает первый экземпляр подстроки
в строке, начиная с указанной позиции. Результатом является индекс перво-
го экземпляра подстроки или специальное целое значение string::npos,
которое показывает, что найти подстроку не удалось.
В следующем примере выполняется поиск всех экземпляров «cat» в за-
данной строке и подсчитывается их количество.
{
string input;
int i = 0;
int cat_appearances = 0;
int main()
{
string my_string = "abcdefghijklmnop";
string first_ten_of_alphabet =
my_string.substr(0, 10);
cout << "The first ten letters of the alphabet are "
<< first_ten_of_alphabet;
}
Передача по ссылке 271
Передача по ссылке
Строки могут содержать много данных и иметь большую длину. Разумеется,
это не относится к каждой строке, но строковые параметры рекомендуется
передавать по ссылке:
void printString (string& str);
{
return;
}
cout << *p_val; // OK, доступ к памяти корректен
*p_val = 20; // НЕ ОК, p_val указывает на измененную
// память
p_val = NULL; // ОК, изменяем не память, а только
// указатель
}
Константы и STL
В последней главе, посвященной библиотеке STL, мы рассматривали
функцию, которая отображала содержимое словаря. Возможно, вы обра-
тили внимание, что словарь передавался по значению и, следовательно,
копировался в функцию displayMap. Приведем текст этой функции снова:
// словарь копируется!
void displayMap(map<string, string> map_to_print)
{
for(map<string, string>::iterator itr =
map_to_print.begin(),
end = map_to_print.end();
itr != end;
++itr)
{
cout << itr->first << " --> " << itr->second
<< endl;
}
}
end = map_to_print.end();
itr != end;
++itr)
{
cout << itr->first << " --> " << itr->second
<< endl;
}
}
Это код изменит мой адрес в адресной книге. К счастью, библиотека STL
поддерживает константы, и ее контейнеры содержат специальные итера-
торы — const_iterator. Их можно использовать как обычный итератор, за
одним исключением: нельзя изменять контейнер, по которому выполняется
итерация, путем записи в const_iterator:
void displayMap(const map<string, string>& map_to_print)
{
for(map<string, string>::const_iterator itr =
map_to_print.begin(),
end = map_to_print.end();
itr != end;
++itr)
{
cout << itr->first << " --> " << itr->second
<< endl;
}
}
Проверьте себя
1. Какой код корректен?
А. const int& x;
Б. const int x = 3; int *p_int = & x;
276 Глава 19. Еще о строках
Практические задания
При решении всех практических задач по возможности используйте кон-
станты и константные ссылки. Это означает, что строки следует переда-
вать по константной ссылке почти во все функции, принимающие строки
в качестве аргументов.
1. Напишите программу, которая считывает две строки и подсчитывает
число случаев, когда одна строка встречается в другой.
2. Напишите программу ввода табличных данных в формате, аналогич-
ном CSV, но в качестве разделителя используйте не запятую, а другой
символ, который программа должна распознать. Сначала предложите
пользователю ввести строки с табличными данными. Затем определите
возможные разделители, проанализировав небуквенные, нецифровые
и непробельные символы, встречающиеся в данных. Найдите такие
символы, встречающиеся в каждой строке, выведите список и попро-
Практические задания 277
Работая в Linux, можно использовать GDB. Visual Studio или Visual Studio
1
int main()
{
double base_val;
double rate;
int years;
cout << "Enter a base value: ";
cin >> base_val;
cout << "Enter an interest rate: ";
cin >> rate;
cout << "Enter the number of years to compound: ";
cin >> years;
cout << "After " << years << " you will have "
<< computeInterest( base_val, rate, years )
<< " money" << endl;
}
Настройка
Сначала мы должны проверить, что отладчик Code::Blocks настроен пра-
вильно, чтобы облегчить процесс отладки.
Для этого мы создадим так называемые символы отладки. Символы от-
ладки позволяют отладчику определить, какая строка кода выполняется
в текущий момент и в каком месте программы вы находитесь. Чтобы
корректно настроить символы, в интерфейсе Code::Blocks воспользуйтесь
командой Project|Build Options (Проект|Параметры сборки). Вы увидите
следующее диалоговое окно:
Если двух целей, Debug и Release (Выпуск), нет, можно установить флажок
Produce debugging symbols [-g] (Создать отладочные символы [-g]) для теку-
щей цели сборки. Проверьте, что параметр Strip all symbols from binary (mini-
mizes size) [-s] (Удалить все символы из двоичного файла (уменьшает раз-
мер) [-s]) НЕ установлен. (Как правило, эти цели сборки создаются вместе
с проектом. Проще всего обеспечить правильность настройки Code::Blocks,
приняв параметры по умолчанию при конфигурировании проекта.)
По окончании настройки можно приступать к отладке. Собрав програм-
му ранее, но изменив затем конфигурацию, соберите программу заново.
Приступим!
Прерывание программы
Ценность отладчика в том, чтобы позволить наблюдать действия про-
граммы — код, который она выполняет, и значения ее переменных. Чтобы
взглянуть на эту информацию, необходимо прервать программу — дать
отладчику команду приостановить ее выполнение. Для этого определяем
в программе точку останова, а затем запускаем программу в отладчике.
Отладчик выполняет программу, пока не встречает точку останова, после
чего можно изучить состояние программы или продолжить ее выполнение
в пошаговом режиме, наблюдая, как каждая строка кода влияет на значения
переменных.
282 Глава 20. Отладка в Code::Blocks
cout << "After " << years << " you will have "
<< computeInterest(base_val, rate, years)
<< " money" << endl;
Теперь мы знаем, что пока все идет как надо, и рассмотрим, что происходит
внутри функции computeInterest. Для этого воспользуемся еще одной
командой отладчика Step Into (Вход в функцию):
int main()
{
LinkedList *lst;
lst = new LinkedList;
lst->val = 10;
lst->next = new LinkedList;
lst->next->val = 11;
printList(lst);
return 0;
}
1
В некоторых средах используется термин нарушение доступа (access violation),
имеющий то же значение.
290 Глава 20. Отладка в Code::Blocks
1
Этот синтаксис применяется для записи шестнадцатеричных чисел. Как пра-
вило, они начинаются с префикса 0x и используют буквы A–F для обозначения
цифр от 0 до 15 (например, число 0xA соответствует 10 в десятичной системе
счисления).
2
В качестве альтернативы можно использовать значение, хранившееся в пере-
менной, которая раньше располагалась там, где теперь хранится указатель.
Поскольку это значение предугадать невозможно, не исключено, что программа
станет вести себя странно, и определить причину такого поведения окажется
сложно. Программа может считать неразрешенную память и аварийно завер-
шиться не сразу, а при попытке воспользоваться считанным значением. От-
ладчик упрощает жизнь — он вносит определенность в поведение программы,
позволяя максимально быстро обнаружить причину неполадки.
3
Адрес функции пригодится при отладке на уровне ассемблера.
Прерывание программы 291
Это наш код. Видно, что отладчик поместил желтую стрелку на строку
29, показав, что она будет выполнена следующей. Вот соответствующий
фрагмент кода:
Изменение переменных
Иногда в процессе отладки требуется изменять значение переменной,
например, чтобы удостовериться, что при определенном значении пере-
менной код работает правильно. Это можно сделать в окне контрольных
переменных. Щелкнув на значении переменной, вы сможете его изменить.
Заключение
Отладчик Code::Blocks позволяет быстро начать поиск неполадок в про-
граммах. Работая в операционной системе, отличной от Windows, можно
воспользоваться многими (если не всеми) изложенными концепциями,
возможно, с некоторыми изменениями. Суть отладки в том, что вы полу-
чаете дополнительную информацию о состоянии программы при помощи
таких инструментов, как точки останова, пошаговое выполнение кода,
просмотр стека вызовов и значений различных переменных.
Практические задания
Здесь, в отличие от других глав, я предлагаю не пройти тест и написать код,
а отладить несколько ведущих себя неправильно программ с ошибками.
Создайте для каждой из них отдельный проект в Code::Blocks и отладьте
его. Некоторые программы содержат не одну, а несколько ошибок!
int main()
{
int base;
int exp;
{
if(n == 0)
{
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main()
{
int n;
cout << "Enter the number to compute fibonacci for: "
<< endl;
cin >> n;
cout << fibonacci(n);
}
ПРИМЕЧАНИЕ
Предварительная обработка
На первом шаге в процессе сборки компилятор запускает препроцессор C.
Цель препроцессора C — внести изменения в текст программы перед ее ком-
пиляцией. Препроцессор понимает директивы препроцессора — команды,
встраиваемые непосредственно в файл с исходным кодом, но адресованные
препроцессору, а не компилятору.
Все директивы препроцессора начинаются с символа решетки (#). Сам
компилятор никогда не знает об их существовании!
Например, оператор
#include <iostream>
Чтобы использовать вместо «Alex» другое имя, нужно всего лишь из-
менить строку #define, в которой определено это имя, а не обрабатывать
весь написанный код функцией поиска с заменой. Макросы позволяют
концентрировать информацию в одном месте, чтобы ее было проще из-
менять. Чтобы присвоить программе номер версии и использовать его
в коде, применим макрос:
#define VERSION 4
// ...
cout << "The version is " << VERSION
Компиляция
Компиляция означает преобразование файла с исходным кодом (.cpp)
в объектный файл (.o или .obj). Объектный файл содержит программу
в виде воспринимаемых процессором компьютера инструкций машинного
языка, которые создаются для каждой функции исходного файла. Каждый
файл с исходным кодом проходит процедуру отдельной компиляции. Это
означает, что объектный файл содержит машинные инструкции только для
одного файла исходного кода. Например, если вы скомпилируете (но не
скомпонуете) три отдельных файла, то создадите три объектных файла,
каждый из которых имеет имя вида <имя_файла>.o или <имя_файла>.obj
(расширение зависит от компилятора). Каждый из этих файлов содержит
перевод исходного кода на машинный язык. Тем не менее объектные файлы
нельзя запустить: их необходимо преобразовать в исполняемые файлы,
которые может использовать операционная система. Для этой цели ис-
пользуется компоновщик.
Компоновка
Компоновка — это создание единственного исполняемого файла (например,
EXE или DLL) из набора объектных файлов и библиотек1. Компоновщик
создает исполняемый файл в подходящем формате и передает в него со-
держимое каждого отдельного объекта. Кроме того, компоновщик обра-
батывает объектные файлы со ссылками на функции, определенные вне
исходных файлов, из которых получены эти объектные файлы, — например,
функции стандартной библиотеки C++. Функции стандартной библиотеки
C++ (например, cout << "Hi") объявлены не в вашем коде, а в объектном
файле, который предоставляется поставщиком компилятора. В момент
компиляции компилятор считает вызовы функций стандартной библиотеки
корректными, поскольку вы включили в программу заголовочный файл
iostream. Тем не менее поскольку эти функции не входят в состав файла .cpp,
компилятор оставляет на месте вызова заглушку. Компоновщик обходит
объектный файл, находит адрес соответствующей функции для каждой
Или единственного объектного файла, если у вас один исходный файл. Ком-
1
Общие и файловые
Общие и файловые объявления
объявления
и реализации
Общие и файловые
определения
Файловые реализации
Как разделить программу на файлы 307
Файловые реализации
z z Пример
Ниже приведена простая программа с общим кодом, создающим связанный
список и помещенным в файл Orig.cpp. Разобьем этот код и разместим
в заголовочном и исходном файлах для повторного использования.
struct Node
{
Node *p_next;
int value;
};
p_new_node->p_next = p_list;
return p_new_node;
}
int main()
{
Node *p_list = NULL;
for(int i = 0; i < 10; ++i)
{
int value;
cout << "Enter value for list node: ";
cin >> value;
p_list = addNode( p_list, value);
}
printList(p_list);
}
int value;
};
return p_new_node;
}
int main()
{
Node *p_list = NULL;
for(int i = 0; i < 10; ++i)
{
int value;
cout << "Enter value for list node: ";
cin >> value;
p_list = addNode(p_list, value);
}
printList(p_list);
}
310 Глава 21. Разбиение программ на части
Orig.cpp включает linkedlist.h дважды: один раз прямо и один раз косвенно,
за счет включения newheader.h.
Для решения этой проблемы требуется защита от повторных включений,
использующая препроцессор C для принятия решений, следует ли вклю-
чать тот или иной файл. В основе такой защиты лежит следующая идея:
если <мы еще не включили этот файл>
<сделать пометку, что мы включили файл>
<включить файл>
Этот код говорит: «если никто не определил ORIG_H, то включить весь код
до #endif». Затем определяем ORIG_H:
#ifndef ORIG_H
#define ORIG_H
Как разделить программу на файлы 311
z z Code::Blocks
Чтобы добавить новый файл в текущий проект в Code::Blocks, выберите
в меню команду FileNewEmpty Source File… (ФайлСоздатьПустой
исходный файл…).
чество чужого кода, можете указать имя или название компании в директиве
#define.
312 Глава 21. Разбиение программ на части
z z g++
Если вы используете g++, нужно лишь создать файлы и присвоить им имена
в командной строке. Например, если вы работаете с исходными файлами
Как разделить программу на файлы 313
или
g++ *.o
z z Xcode
Чтобы добавить новый исходный файл в проект Xcode, выполните команду
меню FileNew File (ФайлНовый файл). Чтобы новые файлы отобра-
жались в папке Sources в иерархическом представлении слева, перед вы-
полнением команды FileNew File… выберите Sources в качестве каталога,
в котором располагается файл main.cpp. Это необязательно, но полезно
для структурирования проекта.
Выполнив команду FileNew, выберите один из нескольких типов файлов:
314 Глава 21. Разбиение программ на части
На левой панели выберите C and C++ (C и C++), а затем C++ file (Файл
C++) справа (или Header file (Заголовочный файл), если хотите добавить
только заголовочный файл). Чтобы добавить одновременно исходный и
заголовочный файлы, выберите вариант C++ file. В следующем окне будет
предложено создать заголовочный файл. Нажмите Next (Далее).
Проверьте себя 315
Проверьте себя
1. Что из нижеперечисленного не входит в процесс сборки C++?
А. Компоновка.
Б. Компиляция.
В. Предварительная обработка.
Г. Окончательная обработка.
2. Когда происходит ошибка, связанная с отсутствием определения функ-
ции?
А. На этапе компоновки.
Б. На этапе компиляции.
В. При запуске программы.
Г. При вызове функции.
3. Что произойдет, если включить заголовочный файл несколько раз?
А. Появятся ошибки из-за повторных объявлений.
Б. Ничего, заголовочные файлы всегда загружаются однократно.
В. Зависит от реализации заголовочного файла.
Г. Заголовочные файлы не могут быть включены несколькими исход-
ными файлами одновременно, поэтому никаких проблем не возникнет.
4. В чем преимущества разделения компиляции и компоновки?
А. Ни в чем: процесс сборки становится запутанным и медленным из-за
одновременной работы нескольких программ.
Б. Проще обнаруживать ошибки, поскольку компоновщик и компиля-
тор позволяют определить местоположение проблемы.
316 Глава 21. Разбиение программ на части
Практические задания
1. Напишите программу, которая содержит функции add, subtract, multi-
ply и divide. Каждая должна принимать два целых числа и возвращать
результат операции. Создайте небольшой калькулятор, который ис-
пользует эти функции. Поместите объявления функций в заголовочный
файл, оставив при этом код этих функций в исходном файле.
2. Переработайте программу, описанную в задаче 1: поместите определе-
ния функций в новый исходный файл отдельно от кода калькулятора.
3. Возьмите реализацию двоичных деревьев, созданную при работе над
упражнениями главы 17, и поместите все объявления функций в один
заголовочный файл, объявления структур — в другой заголовочный
файл, а всю реализацию — в исходный файл. Создайте простую про-
грамму, которая использует базовые функции двоичного дерева.
Г лава 2 2
Введение в проектирование программ
Теперь, поняв, как физически хранить код на диске так, чтобы легко его
редактировать по мере расширения программы, перейдем к решению задачи
более высокого уровня: как логически организовать код с той же целью.
Рассмотрим самые распространенные проблемы, возникающие с ростом
размера программы.
Избыточный код
Хотя при первом знакомстве с функциями мы кратко затронули проблему
повторяющегося кода, изучим ее подробнее. По мере расширения программ
их фрагменты начинают многократно повторяться. При работе над созда-
нием видеоигры потребуется код, который отображает на экране различные
графические элементы (например, космический корабль или снаряд).
Чтобы нарисовать космический корабль, сначала необходимо решить зада-
чу попроще: нарисовать пиксель. Пиксель представляет собой цветную точ-
ку на экране, расположение которой задается двумерными координатами.
Как правило, отображение пикселя выполняет графическая библиотека1.
Кроме того, понадобится код, который использует пиксели (и другие базо-
вые графические элементы, предоставляемые графической библиотекой,
1
В этой книге мы не будем пользоваться графикой, но более подробную инфор-
мацию можно найти на странице http://www.cprogramming.com/graphics-programming.
html
318 Глава 22. Введение в проектирование программ
См. http://en.wikipedia.org/wiki/Bitboard
1
320 Глава 22. Введение в проектирование программ
При условии, что вы всегда использовали эту функцию для доступа к доске.
1
Проектирование и комментарии
Продуманные функции следует не только создавать, но и документировать.
Однако хорошо документировать функцию не так просто, как кажется.
Хорошие комментарии отвечают на вопросы, которые возникают у того,
кто читает код. Не следует применять на практике комментарии, которые
я использовал в примерах этой книги:
// объявляем переменную i и присваиваем ей начальное значение, равное 3
int i = 3;
Проверьте себя
1. В чем преимущество использования функции перед прямым доступом
к данным?
А. Компилятор может оптимизировать функцию и ускорить доступ
к данным.
Б. Функция скрывает нюансы реализации от вызывающего окружения,
что упрощает его изменение.
В. Функции — единственный способ хранения одной структуры данных
в нескольких файлах с исходным кодом.
Г. Такого преимущества нет.
2. Когда следует помещать код в общую функцию?
А. Если собираетесь вызвать ее хотя бы один раз.
Б. Если используете один и тот же код более чем в двух местах про-
граммы.
В. Если компилятор выводит ошибки, связанные с большим размером
функции.
Г. Б и В.
3. С какой целью скрывается представление структуры данных?
А. Для упрощения ее замены.
Б. Для упрощения чтения кода, использующего структуру данных.
В. Для упрощения использования структуры данных в новых фрагмен-
тах кода.
Г. Во всех вышеперечисленных целях.
(Решения см. на с. 467.)
Г лава 2 3
Скрытие представления
структурированных данных
enum ChessPiece {
EMPTY_SQUARE,
WHITE_PAWN
/* и др. */
};
enum PlayerColor {PC_WHITE, PC_BLACK};
struct ChessBoard
{
ChessPiece board[8][8];
PlayerColor whose_move;
};
ChessPiece getPiece (
const ChessBoard *p_board,
int x,
int y
)
{
return p_board->board[x][y];
}
Скрытие формата структуры с помощью функций 325
ChessBoard b;
// сначала инициализируем шахматную доску;
// затем воспользуемся ею следующим образом:
getMove(& b);
// передвигаем фигуру с позиции 0, 0 на позицию 1, 0
makeMove(& b, 0, 0, 1, 0);
Как видите, методы объявлены внутри структуры. Это означает, что методы
являются ее неотъемлемой частью.
Кроме того, объявления методов не включают отдельный аргумент типа
ChessBoard, поскольку метод имеет прямой доступ ко всем полям струк-
туры. Обращение board[x][y] получает непосредственный доступ к полю
board структуры, метод которой был вызван. Но как код определяет, с каким
экземпляром структуры он должен работать, если в программе существует
несколько переменных типа ChessBoard?
Скрытие формата структуры с помощью функций 327
ChessBoard b;
// код, инициализирующий доску
b.getMove();
enum ChessPiece {
EMPTY_SQUARE,
WHITE_PAWN
/* и др. */
};
enum PlayerColor {PC_WHITE, PC_BLACK};
struct ChessBoard
{
ChessPiece board[8][8];
PlayerColor whose_move;
Проверьте себя
1. Почему следует использовать метод вместо прямого доступа к полю
структуры?
А. Метод легче читать.
Б. Метод работает быстрее.
В. Следует пользоваться не методом, а прямым доступом к полю.
Г. Это позволяет изменить представление данных.
Проверьте себя 329
Практические задания
1. Напишите структуру, предоставляющую интерфейс с полем для игры
в крестики-нолики. Реализуйте игру в крестики-нолики, используя
методы этой структуры. Основные операции, такие как ходы игроков
и проверка выигрыша одного из них, должны выполняться интерфей-
сом структуры.
Г лава 2 4
Классы
Создавая язык C++, Бьерн Страуструп хотел развить идею создания струк-
тур, в которых главное место занимают не данные, а функции. Он мог бы
достичь своей цели, расширив существующую концепцию структуры, но
вместо этого создал абсолютно новую идею класса.
Класс подобен структуре, но дает дополнительную возможность опре-
делять, какие методы и данные относятся к его внутренней реализации,
а какие предназначены для пользователей класса. Класс является сино-
нимом категории: определяя класс, вы создаете новую категорию или вид
предметов. Класс уже не является структурированным набором данных: он
определяется методами, из которых образован его интерфейс. Более того,
классы способны упреждать непреднамеренное использование внутренних
механизмов их реализации.
Язык C++ позволяет методам, не принадлежащим классу, запретить ис-
пользовать его внутренние данные. На самом деле когда вы создаете класс,
по умолчанию его внешнему окружению доступны только его методы!
Вы должны явно указывать, какие составляющие класса общедоступны.
Возможность запрещать использование данных класса за его пределами
позволяет компилятору пресекать доступ программистов к внутренним
данным класса. Это очень хорошо с точки зрения надежности программы:
можно изменять базовые структурные элементы класса (например, формат
представления шахматной доски), сохраняя неизменным код вне его.
Даже являясь единственным программистом в проекте, приятно быть уве-
ренным, что никто не попытается проникнуть в написанные вами методы.
Скрытие данных 331
Скрытие данных
Рассмотрим синтаксис скрытия данных классами. Воспользуемся классом,
чтобы скрыть некоторые данные и при этом обеспечить общий доступ
к ряду методов. Класс позволяет определить все методы и поля (их часто
называют членами класса) как открытые и закрытые. Открытые члены
доступны любому, а закрытые — только другим членам класса1.
Приведем пример класса, в котором методы объявлены открытыми, а все
данные — закрытыми.
class ChessBoard
{
public:
int to_x,
int to_y
);
private:
ChessPiece _board[8][8];
PlayerColor _whose_move;
};
// Те же определения методов!
ChessPiece ChessBoard::getPiece(int x, int y)
{
return _board[ x ][ y ];
}
PlayerColor ChessBoard::getMove()
{
return _whose_move;
}
void ChessBoard::makeMove(
int from_x,
int from_y,
int to_x,
int to_y
)
{
// по идее, нужен код, который сначала проверяет
// ход
_board[to_x][to_y] = _board[from_x][from_y];
_board[from_x][from_y] = EMPTY_SQUARE;
}
class ChessBoard
{
public:
Обязанности класса 333
private:
ChessPiece _board[8][8];
PlayerColor _whose_move;
public:
int getMove();
void makeMove(int from_x, int from_y, to_x, to_y);
};
Обязанности класса
Создавая класс в C++, представляйте его себе как новый вид переменной
(тип данных), который похож на int или char, но обладает более мощными
возможностями. Вы уже знакомы с классами на примере строк: строки
334 Глава 24. Классы
Может показаться, что скрывать все поля данных класса неудобно. При-
дется написать много методов чтения и записи вроде getMove, которые
всего лишь возвращают значения закрытых полей структуры, таких как
_whose_move.
Заключение
Класс — один из фундаментальных компонентов, образующих большин-
ство реальных программ на языке C++. С помощью классов программисты
создают масштабные компоненты, которые легко понять и использовать.
Вы изучили одну из самых мощных возможностей классов — скрытие
данных, и в следующих главах продолжите знакомиться с другими много-
численными возможностями классов.
336 Глава 24. Классы
Проверьте себя
1. Для чего используются закрытые данные?
А. Для защиты от хакеров.
Б. Чтобы другие программисты не могли их изменять.
В. Для внутренней реализации класса.
Г. Не следует использовать закрытые данные: они усложняют про-
граммирование.
2. Чем класс отличается от структуры?
А. Ничем.
Б. Все члены класса по умолчанию являются открытыми.
В. Все члены класса по умолчанию являются закрытыми.
Г. Класс позволяет указывать, какие его члены являются открытыми,
а какие — закрытыми.
3. Что делать с полями данных класса?
А. По умолчанию объявлять их открытыми.
Б. По умолчанию объявлять их закрытыми, но при необходимости
делать открытыми.
В. Никогда не объявлять их открытыми.
Г. Как правило, классы не содержат данные, но если содержат, то ничего.
4. Как определить, следует ли объявить метод открытым?
А. Никогда не объявляйте методы открытыми.
Б. Всегда объявляйте методы открытыми.
В. Объявляйте методы открытыми только при условии, что они по-
зволяют использовать основные возможности класса.
Г. Объявляйте методы открытыми, если есть хотя бы малейший шанс,
что кто-нибудь ими воспользуется.
(Решения см. на с. 468.)
Практические задания
1. Возьмите структуру (поле для игры в крестики-нолики) из практиче-
ской задачи в конце предыдущей главы и реализуйте ее в виде класса.
Объявите общедоступные методы открытыми, а данные и все вспомо-
гательные функции — закрытыми. Сколько кода пришлось изменить?
Г лава 2 5
Жизненный цикл класса
Создание объекта
Возможно, вы обратили внимание, что в интерфейсе ChessBoard (открытой
части класса) не было кода, который инициализировал доску. Исправим.
Переменную класса при объявлении необходимо как-то инициализировать:
ChessBoard board;
class ChessBoard
{
public:
PlayerColor getMove();
ChessPiece getPiece(int x, int y);
void makeMove(
int from_x,
int from_y,
int to_x,
int to_y
);
Создание объекта 339
private:
ChessPiece _board[8][8];
PlayerColor _whose_move;
};
ChessBoard a;
ChessBoard b;
ChessBoard();
string getMove();
ChessPiece getPiece(int x, int y);
void makeMove(
int from_x,
int from_y,
int to_x,
int to_y
);
private:
PlayerColor _board[8][8];
string _whose_move;
};
ChessBoard::ChessBoard()
{
_whose_move = "white";
}
ChessBoard();
string getMove();
ChessPiece getPiece(int x, int y);
void makeMove(
int from_x,
int from_y,
int to_x,
int to_y
);
Уничтожение объекта 343
private:
PlayerColor _board[8][8];
string _whose_move;
int _move_count;
};
ChessBoard::ChessBoard()
// после двоеточия следует список переменных
// с аргументами конструктора
: _whose_move("white")
, _move_count(0)
{
}
private:
const int _val;
};
ConstHolder::ConstHolder(int val)
: _val(val)
{}
Уничтожение объекта
Нужен не только конструктор, инициализирующий объект, но и некий
код, который удаляет объект, когда он больше не нужен. Например, если
344 Глава 25. Жизненный цикл класса
struct LinkedListNode
{
int val;
LinkedListNode *p_next;
};
class LinkedList
{
public:
LinkedList(); // конструктор
void insert(int val); // добавляет узел
private:
LinkedListNode *_p_head;
};
class LinkedList
{
public:
LinkedList(); // конструктор
~LinkedList(); // деструктор, обратите внимание на тильду (~)
private:
LinkedListNode *_p_head;
};
Уничтожение объекта 345
LinkedList::~LinkedList()
{
LinkedListNode *p_itr = _p_head;
while(p_itr != NULL)
{
LinkedListNode *p_tmp = p_itr->p_next;
delete p_itr;
p_itr = p_tmp;
}
}
class LinkedListNode
{
public:
~LinkedListNode();
int val;
LinkedListNode *p_next;
};
LinkedListNode::~LinkedListNode()
{
delete p_next;
}
LinkedList::~LinkedList()
{
delete _p_head;
}
// код
if(/* some condition */)
{
return;
}
} // деструктор list вызывается здесь
Уничтожение объекта 347
Хотя в этом случае оператор return находится внутри оператора if, де-
структор выполняется по достижении функцией закрывающей фигурной
скобки. Главное, что следует понять: деструктор выполняется один раз
в тот момент, когда объект оказывается за пределами видимости, то есть
когда обращение к нему вызовет ошибку компилятора.
Если у вас несколько объектов с деструкторами, которые должны быть
выполнены в конце блока кода, они выполняются в порядке, обратном
порядку создания объектов. Например, если объекты создавались следу-
ющим образом:
{
LinkedList a;
LinkedList b;
}
class NameAndEmail
{
/* здесь должны быть какие-нибудь методы */
private:
string _name;
string _email;
};
Копирование классов
Третий важный аспект работы с классами — копирование их экземпляров.
В C++ принято создавать новые классы, которые можно копировать:
LinkedList list_one;
LinkedList list_two;
list_two = list_one;
LinkedList list_three = list_two;
Какое-то значение
<Остаток списка>
Оператор присваивания
Оператор присваивания вызывается при присваивании одному объекту
содержимого существующего объекта, например:
list_two = list_one;
LinkedList& operator= (
LinkedList& lhs,
const LinkedList& rhs
);
private:
LinkedListNode *_p_head;
};
функция operator= работает с переменной lhs так же, как если написать
lhs.operator=(rhs);
Конструктор копирования
Осталось рассмотреть последний ключевой вопрос: что делать для создания
копии существующего объекта.
LinkedList list_one;
LinkedList list_two( list_one );
class LinkedList
{
public:
LinkedList(); // конструктор
~LinkedList(); // деструктор, обратите внимание на тильду
LinkedList& operator= (const LinkedList& other);
LinkedList(const LinkedList& other);
private:
LinkedListNode *_p_head;
};
LinkedList list_one;
LinkedList list_two = list_one;
class Player
{
public:
Player();
~Player();
private:
// эти методы запрещены, поскольку объявлены,
// но не определены; компилятор не генерирует их
// автоматически
operator=(const Player& other);
Player (const Player& other);
PlayerInformation *_p_player_info;
};
// реализации оператора присваивания и конструктора
// копирования отсутствуют
Проверьте себя
1. Когда необходимо создавать конструктор класса?
А. Всегда — без конструктора класс нельзя использовать.
Б. Если необходимо инициализировать класс значениями, отличными
от значений по умолчанию.
В. Никогда — компилятор всегда предоставляет конструктор класса.
Г. Только если уже есть деструктор.
2. Как связаны деструктор и оператор присваивания?
А. Никак.
Б. Деструктор класса вызывается до выполнения оператора присваи-
вания.
356 Глава 25. Жизненный цикл класса
Практические задания
1. Реализуйте собственный вектор vectorOfInt, работающий только с це-
лыми числами (обычно не нужно работать с шаблонами вроде STL).
Интерфейс класса должен быть следующим.
• Конструктор без аргументов, создающий 32-элементный вектор.
• Конструктор, принимающий начальный размер вектора в качестве
аргумента.
• Метод get , принимающий индекс и возвращающий значение по
этому индексу.
• Метод set , принимающий индекс и значение и присваивающий
значение элементу с заданным индексом.
• Метод pushback, добавляющий элемент в конец массива и при не-
обходимости изменяющий размер массива.
• Метод pushfront, добавляющий элемент в начало массива.
• Конструктор копирования и оператор присваивания.
Класс не должен допускать утечек памяти; следует освобождать всю вы-
деленную память. Тщательно обдумайте возможные варианты некоррект-
ного использования класса (пользователь вводит отрицательный размер
вектора, отрицательный индекс и т. д.) и способы реагирования на них.
Г лава 2 6
Наследование и полиморфизм
Все, кто использует объекты этого вектора, смогут вызывать лишь методы,
определенные в интерфейсе Drawable, но ничего другого нам сейчас и не
нужно.
У меня для вас хорошая новость: C++ действительно позволяет сделать
то, о чем я только что сказал! Посмотрим, как именно.
Наследование в C++
Сначала введем новый термин — наследование. Наследование означает,
что один класс получает свойства другого класса. В данном случае на-
следуемым свойством является интерфейс класса Drawable , а именно
метод draw. Класс, который наследует свойства другого класса, называется
подклассом, а класс, свойства которого наследуются, — суперклассом1.
Суперкласс часто определяет метод (или методы) интерфейса, которые
могут по-разному реализовываться каждым подклассом. В нашем примере
класс Drawable является суперклассом. Каждый объект Drawable в игре
будет подклассом класса Drawable. Каждый класс унаследует метод draw,
что позволит коду, получающему объект Drawable, рассчитывать, что в нем
доступен метод draw. Затем все классы реализуют собственные версии ме-
тода draw: фактически они обязаны сделать это для гарантии присутствия
метода draw во всех подклассах класса Drawable.
Теперь, поняв базовую концепцию, перейдем к синтаксису.
class Ship : public Drawable
{
};
вызов draw обратится к реализации метода draw класса Drawable. Это не то,
что нужно, поскольку класс Ship должен отрисовывать себя собственным
методом, а не методом интерфейса Drawable.
1
Иногда вместо «суперкласс» используют термин родительский класс, а вместо
«подкласс» — дочерний класс. В этой книге я буду пользоваться терминами
«суперкласс» и «дочерний класс».
Наследование в C++ 361
Ship::draw()
{
/* код отрисовки */
}
int main()
{
// красивый слон ;)
Bar bar;
}
int main()
{
// красивый слон ;)
Bar bar;
}
Foo's constructor
Bar's constructor
Bar's destructor
Foo's destructor
class FooSuperclass
{
public:
FooSuperclass(const string& val);
};
private:
int *_my_data;
};
MyDrawable::MyDrawable()
{
_my_data = new int;
}
MyDrawable::~MyDrawable()
{
delete _my_data;
}
продолжение
368 Глава 26. Наследование и полиморфизм
void MyDrawable::draw()
{
/* код отрисовки на экране */
}
int main()
{
deleteDrawable(new MyDrawable());
}
Проблема срезки
При работе с наследованием следует иметь в виду проблему срезки. Срезка
объектов происходит, если код подобен приведенному ниже:
class Superclass
{};
class Subclass : public Superclass
{
int val;
};
int main()
{
Subclass sub;
Superclass super = sub;
}
class Superclass
{
public:
// объявляя конструктор копирования,
// мы должны предоставить собственный
// конструктор по умолчанию
Superclass() {}
private:
продолжение
370 Глава 26. Наследование и полиморфизм
int main()
{
Subclass sub;
// теперь эта строка вызывает ошибку компиляции
Superclass super = sub;}
virtual ~Drawable();
void clearRegion(int x1, int y1, int x2, int y2);
};
Защищенные данные
Решение заключается в том, чтобы воспользоваться третьим, последним
модификатором доступа — защищенным (protected).
В отличие от закрытых, защищенные члены суперкласса доступны его
подклассам, но недоступны вне класса — в отличие от открытых. Защи-
щенные члены класса описываются тем же синтаксисом, что открытые
и закрытые члены:
class Drawable
{
public:
virtual void draw();
virtual ~Drawable();
protected:
void clearRegion(int x1, int y1, int x2, int y2);
};
class Node
{
public:
static int serial_number;
};
// объявление вне класса – мы должны указать
// префикс Node::
static int Node::serial_number = 0;
private:
static int _getNextSerialNumber ();
Node::Node()
: _serial_number(_getNextSerialNumber())
{ }
int Node::_getNextSerialNumber ()
{
// воспользуемся постфиксной версией оператора ++ для возврата
// предыдущего значения переменной
return _next_serial_number++;
}
vector<Drawable*> drawables;
void drawEverything()
{
for ( int i = 0; i < drawables.size(); i++ )
{
drawables[i]->draw();
}
}
ПРИМЕЧАНИЕ
следующим образом.
1. Считать указатель, хранящийся в drawables[i].
2. По этому указателю найти адрес виртуальной таблицы для группы
методов, связанных с интерфейсом типа Drawable (в данном случае эта
группа состоит из единственного метода).
3. Найти в этой таблице функцию с заданным именем (в данном случае
draw). Эта таблица буквально содержит набор адресов, по которым рас-
положены все функции.
4. Вызвать эту функцию с соответствующими аргументами.
Как правило, на шаге 2 используется не имя функции, а индекс таблицы,
в который компилятор преобразует имя функции. Это обеспечивает высо-
кую скорость вызовов виртуальных функций при выполнении программы.
Вызовы обычных и виртуальных функций выполняются почти за одно
и то же время.
Можно считать, что компилятор генерирует следующий код (разумеется,
операция call является вымышленной).
call drawables[i]->vtable[0];
Проверьте себя
1. Когда запускается деструктор суперкласса?
А. Только когда объект уничтожается путем освобождения указателя
на суперкласс с помощью оператора delete.
Б. Перед вызовом деструктора подкласса.
В. После вызова деструктора подкласса.
Г. В процессе выполнения деструктора подкласса.
2. Что необходимо сделать в конструкторе класса Cat в условиях приво-
димой иерархии классов?
class Mammal {
public:
Mammal (const string& species_name);
};
А. Ничего особенного.
Б. Вызвать конструктор Mammal с аргументом cat, воспользовавшись
списком инициализации.
В. Вызвать конструктор Mammal с аргументом cat.
Г. Удалить конструктор Cat и воспользоваться конструктором по умол-
чанию, который выполнит все требуемые действия.
3. В чем ошибка следующего определения класса?
class Nameable
{
virtual string getName();
};
Практические задания
1. Реализуйте функцию sort, принимающую вектор указателей на ин-
терфейсный класс Comparable , в котором определен метод compare
(Comparable& other). Функция возвращает 0, если объекты одинаковы,
1, если текущий объект больше, чем other, и –1, если текущий объект
меньше, чем other. Создайте класс, который реализует этот интерфейс,
и несколько экземпляров этого класса и отсортируйте их. Например,
можно создать класс HighScoreElement, который содержит имя игрока
и количество набранных им очков. Функция сортирует объекты этого
класса в порядке убывания результатов, но если у игроков одинаковый
результат, они упорядочиваются по имени.
2. Реализуйте функцию сортировки другим способом. Создайте интер-
фейс Comparator с методом compare(const string& lhs, const string&
378 Глава 26. Наследование и полиморфизм
rhs), который устроен так же, как метод compare в предыдущей задаче:
он возвращает 0, если два значения совпадают, 1, если lhs > rhs, и –1,
если lhs < rhs . Напишите два класса, один из которых сравнивает
строки без учета регистра, а второй сортирует их в порядке, обратном
алфавитному.
3. Реализуйте метод ведения журнала, который принимает в каче-
стве аргумента интерфейсный класс StringConvertable . Класс
StringConvertable содержит метод toString, который преобразует
результирующий объект в строку. Метод ведения журнала должен вы-
водить дату, время и сам объект (вы можете узнать о том, как считать
дату, на странице http://www.cplusplus.com/reference/clibrary/ctime/). Еще
раз обратите внимание: можно повторно использовать метод ведения
журнала, реализовав интерфейс.
Г лава 2 7
Пространства имен
В этом примере используется тот же оператор ::, что и при доступе к ста-
тическим членам класса и объявлении метода, но в данном случае этот
оператор обеспечивает доступ к элементам не класса, а пространства имен.
Вы спросите: если пространства имен так удобны, почему стандартная
библиотека их не использует? Не получается ли так, что длинные имена
используются без всякого смысла?
На самом деле вы уже встречались с пространствами имен. В начале каждой
программы мы указывали оператор во избежание использования полных
имен при обращении к объектам вроде cin и cout
using namespace std;
В пространство имен можно добавить код из обоих файлов, что вам и нужно.
382 Глава 27. Пространства имен
вместо
#include <iostream>
#include <iostream>
using namespace std;
Проверьте себя
1. Когда следует использовать директиву using namespace?
А. Во всех заголовочных файлах после директивы include.
Б. Никогда, это опасно.
В. В начале любого файла cpp при отсутствии конфликтов пространств
имен.
Г. Непосредственно перед использованием переменной из соответству-
ющего пространства имен.
2. Для чего нужны пространства имен?
А. Чтобы поставить интересную задачу перед разработчиками компи-
ляторов.
Б. Чтобы усилить инкапсуляцию кода.
В. Чтобы предотвращать конфликты имен в крупных базах кода.
Г. Чтобы пояснять, для чего предназначен класс.
384 Глава 27. Пространства имен
Практические задания
1. Добавьте в пространство имен реализацию вектора из последней прак-
тической главы 25.
Г лава 2 8
Файловый ввод-вывод
Чтение из файлов
Сначала рассмотрим считывание данных из файлов. Для этого восполь-
зуемся типом ifstream. Экземпляр ifstream можно инициализировать по
имени файла, из которого мы хотим считать данные:
Эта небольшая программа пытается открыть файл myfile.txt: она ищет его
в том каталоге, где выполняется (это рабочий каталог программы). При не-
обходимости можно указать полный путь к файлу, например c:\myfile.txt.
Хочу обратить особое внимание, что программа лишь делает попытку
открыть файл: возможно, этот файл не существует. Можно проверить
результат создания объекта ifstream и определить, был ли файл успешно
открыт, с помощью метода is_open1:
1
Информацию об этих стандартных функциях можно найти на веб-сайтах вроде
http://en.cppreference.com/w/cpp или http://cplusplus.com/reference/
Основы файлового ввода-вывода 387
Эта строка считывает цифры из файла так же, как если бы их вводил поль-
зователь. Считывание продолжается до обнаружения пробела или другого
разделителя. Например, если файл содержит текст
12 a b c
{
cout << "Could not open file!" << '\n';
}
int number;
// здесь проверяем успешность считывания целого значения
if ( file_reader >> number )
{
cout << "The value is: " << number;
}
}
Форматы файлов
Запрашивая данные у пользователя, можно указать ему, какие значения вы
хотите получить, и, если вводимые данные некорректны, инструктировать
его, как их исправить. При считывании из файлов такой возможности нет.
Файлы создаются заранее — возможно, еще до того, как вы заканчиваете
писать программу. Чтобы считать данные, нужно знать формат файла.
Формат файла представляет собой его структуру (необязательно сложную).
Допустим, есть таблица рекордов, которую вы хотите сохранять между за-
пусками программы. Пример простого формата файла — это десять строк,
каждая из которых содержит одно число:
Конец файла
Этот код работает лишь с конкретным файловым форматом и не преду
сматривает никакой обработки ошибок. Например, если количество запи-
сей в файле меньше десяти, код не прекратит считывание, даже достигнув
конца файла. Ситуация, когда количество рекордов меньше десяти, вполне
390 Глава 28. Файловый ввод-вывод
возможна — например, если в игру играли лишь дважды. Конец файла часто
обозначается аббревиатурой EOF (end of file, конец файла).
Можно сделать код устойчивее (либеральнее по отношению к вводимым
данным), если обрабатывать ситуации, в которых файл содержит меньше
десяти элементов. Для этого можно еще раз проверить результат метода,
который считывает данные.
int main ()
{
ifstream file_reader( "myfile.txt" );
if ( ! file_reader.is_open() )
{
cout << "Could not open file!" << '\n';
}
vector<int> scores;
for ( int i = 0; i < 10; i++ )
{
int score;
if ( ! ( file_reader >> score ) )
{
break;
}
scores.push_back( score );
}
}
Запись в файлы
Для записи в файлы применяется тип ofstream (сокращение от output file
stream — исходящий файловый поток). Этот тип почти идентичен типу
ifstream с единственным исключением: он используется аналогично не
объекту cin, а объекту cout.
Рассмотрим простую программу, которая выводит числа от 0 до 9 в файл
highscores.txt (этот код скоро создаст нам нечто более похожее на таблицу
рекордов).
int main ()
{
ofstream file_writer( "highscores.txt" );
if ( ! file_writer.is_open() )
{
cout << "Could not open file!" << '\n';
return 0;
}
Этот код открывает файл, при этом не уничтожая его текущее содержимое,
и позволяет добавлять двоичные данные в конец файла.
Позиция в файле
Когда программа читает файл или записывает в него, код ввода-вывода
должен знать, в каком месте файла следует выполнить соответствующую
операцию. Позиция в файле аналогична курсору на экране, который ука-
зывает, где будет отображен следующий введенный символ.
При выполнении базовых операций нет необходимости уделять особое
внимание позиции в файле: код может просто считывать или записывать
очередной фрагмент данных. Тем не менее можно изменять позицию
в файле, не выполняя операцию чтения. Это часто необходимо при работе
с файлами, которые хранят сложные структуры данных, например ZIP или
PDF, и с файлами, считывание каждого байта из которых занимает много
времени или невозможно (например, если вы реализуете базу данных).
Фактически в файле существуют две позиции, одна из которых опреде-
ляет место для следующей операции чтения, а другая — для следующей
операции записи. Узнать текущую позицию для чтения можно с помощью
метода tellg (g означает get — получение, чтение данных), а текущую
позицию для записи — с помощью метода tellp (p означает put — раз-
мещение, запись данных).
Новую позицию в файле также можно задать, перемещаясь от текущей
позиции с помощью методов seekp и seekg. Перемещение по файлу назы-
вается поиском (это отражено в названиях методов, которые происходят
от слова seek — искать). При поиске в файле позиция чтения или записи
перемещается на новое место. Оба указанных метода принимают два па-
раметра: расстояние и начало поиска. Расстояние измеряется в байтах,
а началом поиска является текущая позиция, начало или конец файла. По-
сле поиска вы можете считывать или записывать данные в файл, начиная
с новой позиции. Изменение одной позиции с помощью операции поиска
не влияет на другую позицию.
Позиция в файле задается с помощью одного из трех флагов:
int main ()
{
fstream file (
"highscores.txt",
ios::in | ios::out
);
if ( ! file.is_open() )
{
cout << "Could not open file!" << '\n';
return 0;
}
int new_high_score;
cout << "Enter a new high score: ";
cin >> new_high_score;
{
file << endl;
}
// запись нового рекорда
file << new_high_score << endl;
// циклическая запись всех оставшихся рекордов
if ( argc != 2)
{
// При выводе на экран инструкций по использованию программы можно
// воспользоваться именем файла, хранящимся в argv[0]
cout << "usage: " << argv[ 0 ] << " <filename>"
<< endl;
}
else
{
// Мы считаем, что в argv[1] хранится имя открываемого файла
ifstream the_file( argv[1] );
// Всегда проверяйте успешность открытия файла
if ( ! the_file.is_open() )
{
cout << "Could not open file "
<< argv[ 1 ] << endl;
return 1;
}
char x;
// вызов the_file.get(x) считывает следующий символ
// из файла в переменную x; он возвращает false при
// достижении конца файла или появлении ошибки
while ( the_file.get( x ) )
{
cout << x;
}
} // здесь деструктор неявно закрывает файл
}
Ввод-вывод в двоичные файлы 399
Преобразование в char*
Итак, как нам указать компилятору, чтобы он обращался с переменной как
с указателем на тип char, а не на ее настоящий тип? Для этого придется
воспользоваться преобразователями типов. Преобразователь типов уве-
домляет компилятор, что вы берете на себя ответственность за обработку
переменной нестандартным для нее образом. Мы хотим использовать пере-
менную как набор отдельных байтов и с помощью преобразователя типов
потребуем от компилятора, чтобы он предоставил нам доступ к каждому
байту этой переменной.
Двумя наиболее распространенными преобразователями типов явля-
ются static_cast и reinterpret_cast. static_cast используется для
Ввод-вывод в двоичные файлы 403
a_file.write(
reinterpret_cast<char*>(& rec),
sizeof(rec)
);
struct PlayerRecord
{
int age;
int score;
string name;
};
fstream a_file(
"records.bin",
ios::trunc | ios::binary | ios::in | ios::out
);
a_file.write(
reinterpret_cast<char*>(& rec.age),
sizeof(rec.age)
);
a_file.write(
reinterpret_cast<char*>(& rec.score),
sizeof(rec.score)
);
продолжение
406 Глава 28. Файловый ввод-вывод
Чтение из файла
Чтобы считать данные из двоичного файла, воспользуемся методом read.
Его аргументы почти такие же, как у метода write: место, в которое следует
поместить данные, и количество этих данных2. Считать из файла целое
значение можно с помощью следующего кода:
int x = 3;
a_file.read(reinterpret_cast<char*>(& x), sizeof(x));
1
Иногда нуль-ограничитель записывают как '\0'. Это абсолютно корректно; раз-
ница между 0 и '\0' в том, что значение '\0' имеет тип char, а 0 — целое число,
которое преобразуется в char. В нашей ситуации подойдут оба варианта.
2
Важное различие методов read и write в том, что в write можно передавать
константный указатель на объект, участвующий в записи. Не забудьте, что
понадобится преобразователь reinterpret_cast<const char*> (обратите
внимание на модификатор const).
Ввод-вывод в двоичные файлы 407
PlayerRecord in_rec;
if (! a_file.read(
reinterpret_cast<char*>(& in_rec.age),
sizeof(in_rec.age)
))
{
// обработка ошибки
}
if (! a_file.read(
reinterpret_cast<char*>( & in_rec.score ),
sizeof( in_rec.score )
) )
{
// обработка ошибки
}
if ( ! a_file.read(
reinterpret_cast<char*>(& str_len),
sizeof(str_len)
) )
{
// обработка ошибки
}
// проверяем, что выделяемое количество памяти
// не слишком велико
else if ( str_len > 0 && str_len < 10000 )
{
char *p_str_buf = new char[ str_len ];
// + 1 для нуль-ограничителя
продолжение
408 Глава 28. Файловый ввод-вывод
cout << in_rec.age << " " <<in_rec.score << " " << in_rec.name << endl;
struct PlayerRecord
{
int age;
int score;
string name;
};
int main ()
{
PlayerRecord rec;
rec.age = 11;
rec.score = 200;
rec.name = "John";
fstream a_file(
"records.bin",
ios::trunc | ios::binary | ios::in | ios::out
);
a_file.write(
reinterpret_cast<char*>(& rec.age),
sizeof(rec.age)
);
a_file.write(
reinterpret_cast<char*>(& rec.score),
Ввод-вывод в двоичные файлы 409
sizeof(rec.score)
);
a_file.write(
rec.name.c_str(),
rec.name.length() + 1
);
PlayerRecord in_rec;
a_file.seekg(0, ios::beg);
if (! a_file.read(
reinterpret_cast<char*>(& in_rec.age ),
sizeof(in_rec.age)
))
{
cout << "Error reading from file" << endl;
return 1;
}
if (! a_file.read(
reinterpret_cast<char*>(& in_rec.score),
sizeof(in_rec.score)
))
{
cout << "Error reading from file" << endl;
return 1;
}
int str_len;
if (! a_file.read(
reinterpret_cast<char*>(& str_len),
sizeof(str_len)
))
{
cout << "Error reading from file" << endl;
return 1;
}
// проверяем, что выделяемое количество памяти
// не слишком велико
if (str_len > 0 && str_len < 10000)
{
char *p_str_buf = new char[ str_len + 1];
if (! a_file.read(p_str_buf, str_len + 1))
продолжение
410 Глава 28. Файловый ввод-вывод
Проверьте себя
1. Какой тип можно использовать для чтения из файла?
А. ifstream
Б. ofstream
В. fstream
Г. А и В
2. Какое из нижеперечисленных утверждений верно?
А. Текстовые файлы занимают меньше места, чем двоичные.
Б. Двоичные файлы легче отлаживать.
В. Двоичные файлы используют пространство эффективнее текстовых.
Г. Текстовые файлы слишком медленны для использования в реальных
программах.
3. Почему нельзя передавать указатель на строковый объект при написа-
нии двоичного файла?
А. В метод write всегда необходимо передавать char*.
Б. Строковый объект невозможно хранить в памяти.
В. Нам неизвестна структура строкового объекта, она может содержать
указатели, которые будут записаны в файл.
Ввод-вывод в двоичные файлы 411
Практические задания
1. Перепишите программу, которая вставляет новые значения в таблицу
рекордов, заменив текстовый файл двоичным. Как узнать, корректно
ли работает программа? Создайте программу, которая преобразует
двоичный файл в текстовый вид.
2. Измените программу, разбирающую код HTML, которую вы реализо-
вали в главе 19, таким образом, чтобы она могла считывать данные из
файла на диске.
3. Создайте простую программу, которая анализирует код XML. XML
представляет собой язык разметки, схожий с HTML. Документ XML
представляет собой дерево узлов вида <узел>[данные]</узел>, где эле-
мент [данные] представляет собой текст или вложенный узел. Узлы
XML могут иметь атрибуты вида <узел атрибут="значение"></узел>
(реальная спецификация XML содержит гораздо больше деталей, но
для их реализации потребуется много усилий). Программа разбора
должна включать интерфейсный класс с методами, которые вызыва-
ются при наступлении интересных событий.
1) При чтении узла вызывается метод nodeStart, которому передается
имя узла.
2) При считывании атрибута вызывается метод attributeRead ; он
должен вызываться сразу же после метода nodeStart для узла,
с которым связан атрибут.
3) Если узел содержит текст, вызывается метод nodeTextRead ,
которому текст передается в виде строки. Если узел имеет формат
412 Глава 28. Файловый ввод-вывод
и
<html>
<head>
<title>Doc title</title>
</head>
<body>This is a nice <a href="http://www.cprogramming.com">link</a>
to a website.</body>
</html>
а список
<nl>
<li>first item</li>
<li>second item</li>
</nl>
в виде
1. first item
2. second item
Шаблонные функции
С помощью шаблонов удобно создавать обобщенные функции. Рассмотрим
небольшую вспомогательную функцию, которая вычисляет площадь тре-
угольника:
int triangleArea (int base, int height)
{
return base * height * .5;
}
то во всем коде параметр T будет заменен типом double. Это будет выгля-
деть так, будто мы написали функцию triangleAreaDouble. Созданный код
фактически является шаблоном, с помощью которого компилятор создает
конкретную функцию для работы с типом double.
Другими словами, строка
template <typename T>
компилятор поймет, что вместо T следует подставить тип double. Это объ-
ясняется тем, что шаблонный параметр T используется для объявления
аргументов функции. Поскольку компилятору известны типы аргументов,
он догадывается, чем является T.
Вывод типа данных всегда выполняется, если в качестве типа одного из
аргументов функции используется параметр шаблона.
Утиная типизация
Есть поговорка: «Если что-то выглядит как утка, ходит как утка и говорит
как утка, это на самом деле утка». Как ни странно, поговорка часто имеет
отношение к шаблонам языка C++, и вот почему.
Шаблонные функции 417
НЕКОРРЕКТНЫЙ КОД
int main ()
{
vector<int> a, b, c;
triangleArea( a, b, c );
}
Это длинное сообщение можно разделить на части. Его первая строка со-
общает, в какой шаблонной функции возникла ошибка (triangleArea),
а вторая строка — в какой строке кода вы пытались воспользоваться
этой шаблонной функцией (как правило, интересует именно это). Фраза
«instantiated from here» (созданный здесь экземпляр) означает «место, где
вы пытались воспользоваться шаблоном». В данном случае создание эк-
земпляра подразумевает попытку реализации шаблона compute_equation
с параметром vector<int>.
В следующих строках сообщения описывается причина неудачи при ком-
пиляции: no match for 'operator*' in 'base * height' ('operator*' отсутствует
в 'base * height'). Это означает, что компилятор не смог определить, как
перемножить переменные base и height (оператор * для векторов не
определен). Поскольку обе переменные — векторы, можно догадаться, что
векторы нельзя перемножать1.
Шаблонные классы
Шаблонные классы часто используются программистами, которые пишут
библиотеки и хотят сами создавать классы вроде vector и map. Тем не менее
обобщение кода полезно и при написании обычных программ. Не стоит
пользоваться шаблонами лишь потому, что вы умеете это делать, но по
возможности избавляйтесь от классов, отличающихся друг от друга только
типами своих членов. Скорее всего, вам придется создавать шаблонные
методы чаще, чем шаблонные классы, однако полезно знать, как их ис-
пользовать (например, для реализации собственной структуры данных).
Объявление шаблонного класса очень похоже на объявление шаблонной
функции.
Например, создадим небольшой класс, который обертывает массив1:
{
public:
ArrayWrapper (int size);
private:
T *_p_mem;
};
Calc::Calc ()
{}
int main ()
{
// демонстрируем объявление
Calc<int> c;
}
Заключение о шаблонах
Шаблоны позволяют создавать обобщенный код, работающий с любыми
типами данных, а не с конкретным типом, например int. Шаблоны часто
используются при разработке библиотек C++ (например, стандартной
библиотеки шаблонов). Скорее всего, вам нечасто придется создавать
Заключение о шаблонах 423
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In instantiation of 'std::_Vector_base<int, int>::_
Vector_impl':
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:123: instantiated from 'std::_Vector_base<int, int>'
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:159: instantiated from 'std::vector<int, int>'
template_err.cc:6: instantiated from here
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:82: error: 'int' is not a class, struct, or union type
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:86: error: 'int' is not a class, struct, or union type
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In instantiation of 'std::vector<int, int>':
template_err.cc:6: instantiated from here
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:161: error: 'int' is not a class, struct, or union type
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:193: error: no members matching 'std::_Vector_base<int,
int>::_M_get_Tp_allocator' in 'struct std::_Vector_base<int, int>'
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In destructor 'std::vector<_Tp, _Alloc>::~vector()
[with _Tp = int, _Alloc = int]':
template_err.cc:6: instantiated from here
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:272: error: '_M_get_Tp_allocator' was not declared in
this scope
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In constructor 'std::_Vector_base<_Tp, _Alloc>::_
Vector_base(const _Alloc&) [with _Tp = int, _Alloc = int]':
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:203: instantiated from 'std::vector<_Tp, _
Alloc>::vector(const _Alloc&) [with _Tp = int, _Alloc = int]'
template_err.cc:6: instantiated from here
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:107: error: no matching function for call to 'std::_
Vector_base<int, int>::_Vector_impl::_Vector_impl(const int&)'
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:82: note: candidates are: std::_Vector_base<int,
int>::_Vector_impl::_Vector_impl(const std::_Vector_base<int, int>::_
Vector_impl&)
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In member function 'void std::_Vector_base<_Tp, _
Alloc>::_M_deallocate(_Tp*, size_t) [with _Tp = int, _Alloc = int]':
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:119: instantiated from 'std::_Vector_base<_Tp, _
Alloc>::~_Vector_base() [with _Tp = int, _Alloc = int]'
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:203: instantiated from 'std::vector<_Tp, _
Alloc>::vector(const _Alloc&) [with _Tp = int, _Alloc = int]'
Заключение о шаблонах 425
то код не скомпилируется.
Мы снова имеем дело с утиной типизацией (см. с. 416): шаблон интересу-
ется не конкретным типом данных, а тем, как этот тип вписывается в код.
В данном случае целое число не поддерживает синтаксис x.val, и компи-
лятор отказывается его обрабатывать.
Похожее ограничение действует на второй параметр шаблона vector —
у него должен быть тип, который поддерживает более широкую функ-
циональность, чем простое целое число. Приведенные выше сообщения
об ошибках по-разному жалуются на то, тип int не подходит к этому
параметру шаблона!
426 Глава 29. Шаблоны в C++
Получив столь массивный текст, лучше всего, как обычно, начать с перво-
го сообщения и последовательно исправить ошибки. Ниже приведены
фрагменты текста до слова error.
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h: In instantiation of 'std::_Vector_base<int, int>':
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:159: instantiated from 'std::vector<int, int>'
template_err.cc:6: instantiated from here
/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/
bits/stl_vector.h:78: error: 'int' is not a class, struct, or union type
Выглядит гораздо лучше, не так ли? Этот текст состоит всего лишь из не-
скольких строк — почти как в разделе «Утиная типизация» (с. 416). Мы
вполне способны с ним разобраться!
Рассмотрим первую строку: In instantiation of std::_Vector_base<int,
int>. (При создании экземпляра std::_Vector_base<int, int>.) Словосо-
четание «при создании экземпляра шаблона» означает «при попытке
скомпилировать шаблон с конкретным набором параметров». Эта ошибка
появилась потому, что при создании шаблона с заданными параметрами
возникла проблема (_Vector_base представляет собой вспомогатель-
ный класс реализации вектора). Вторая строка показывает, что шаблон
Vector_base не удалось скомпилировать при попытке создать шаблон
vector<int,int>, и ошибка произошла в строке 6 файла template_err.cc.
Поскольку файл template_err.cc содержит наш код, мы знаем, какая его
часть вызвала ошибку.
Первое действие при поиске ошибки — обнаружить строку кода, которая
вызывает проблему. Чтобы понять причину ошибки, часто достаточно
просто взглянуть на код. Если эта причина неочевидна, можно просмо-
треть список создания экземпляров и в конечном счете увидеть реальное
сообщение об ошибке: error: 'int' is not a class, struct, or union
type (ошибка: тип 'int' не является классом, структурой или объеди-
нением). Оно говорит, что компилятор ожидал использование класса или
структуры, а не встроенного типа данных вроде int. Поскольку векторы
должны уметь хранить данные любого типа, можно предположить, что про-
блема связана с шаблонным параметром, переданным вектору. Если еще
раз проверить объявление вектора, увидим, что необходимо использовать
только один шаблонный параметр.
Теперь, когда мы диагностировали первую проблему, необходимо устра-
нить ее и заново скомпилировать программу. Обычно удается одновре-
менно обнаружить как минимум две ошибки, однако при использовании
Проверьте себя 427
шаблонов одна ошибка часто влечет за собой все остальные. Исправив ее,
вы избавляетесь и от других ошибок, которые устраняются автоматически.
В приведенном выше примере многочисленные ошибки, сообщения о ко-
торых заняли целую страницу, были вызваны единственной причиной —
использованием второго параметра int в шаблоне.
Проверьте себя
1. Для чего следует использовать шаблоны?
А. Для экономии времени.
Б. Для ускорения выполнения кода.
В. Для написания одного и того же кода для разных типов данных.
Г. Для возможности повторного использования кода в будущем.
2. Когда необходимо указывать тип параметра шаблона?
А. Всегда.
Б. Только при объявлении экземпляра класса шаблона.
В. Только если тип нельзя определить автоматически.
Г. В шаблонных функциях, при условии, что тип нельзя определить
автоматически, и всегда для классов шаблона.
3. Как компилятор определяет, можно ли применить шаблонный параметр
к конкретному шаблону?
А. Он реализует определенный интерфейс C++.
Б. Следует задать соответствующие ограничения при объявлении ша-
блона.
В. Он пытается использовать шаблонный параметр: если тип шаблона
поддерживает все требуемые операции, параметр принимается.
Г. Следует перечислить все допустимые типы шаблонов при его объ-
явлении.
4. В чем различие размещения в заголовочном файле класса шаблона
и обычного класса?
А. Различий нет.
Б. Методы обычного класса нельзя определять в заголовочном файле.
В. Все методы класса шаблона должны быть определены в заголовочном
файле.
Г. Класс шаблона, в отличие от обычного класса, может не иметь файла
.cpp.
428 Глава 29. Шаблоны в C++
Практические задания
1. Напишите функцию, которая принимает вектор и суммирует все его
значения независимо от типа хранимых числовых данных.
2. Переработайте класс, заменяющий вектор, реализованный в одной из
практических задач главы 25, и сделайте его шаблоном, который спо-
собен хранить данные любого типа.
3. Напишите метод поиска, который принимает вектор и значение любого
типа и возвращает true, если значение присутствует в векторе, и false
в противном случае.
4. Реализуйте функцию сортировки, которая принимает вектор любого
типа и сортирует его значения в естественном порядке (который фор-
мируется при использовании операторов <or>).
Ч а с ть I V
Дополнительная информация
int main()
{
cout << setw( 10 ) << "ten" << "four" << "four";
}
int main()
{
cout << setw( 10 ) << left << "ten" << "four" << "four";
}
Изменение символа-заполнителя
Иногда в качестве заполнителя необходимо использовать не пробел,
а другой символ. Чтобы указать заполнитель, следует воспользоваться
методом setfill. Метод setfill, как и setw, передается непосредственно
объекту cout.
Добавим в исходный пример метод setfill, устанавливающий в качестве
заполнителя дефис:
cout << setfill( '-' ) << setw( 10 ) << "ten" << "four"
<< "four";
Результат программы:
-------tenfourfour
дает результат:
---------A---------B---------C
cout.fill( last_fill );
Этот вызов, как и метод fill, возвращает предыдущее значение, чтобы его
было возможно восстановить.
Добавьте приведенный вызов setf в предыдущий пример и оцените раз-
личия форматирования.
struct Person
{
Person (
const string& firstname,
const string& lastname
)
: _firstname( firstname )
, _lastname( lastname )
{} продолжение
string _firstname;
string _lastname;
};
int main ()
{
vector<Person> people;
int firstname_max_width = 0;
int lastname_max_width = 0;
Вывод чисел
Выводимые численные данные также необходимо правильно оформлять.
Шестнадцатеричные значения желательно предварять префиксом «0x»,
указывая на систему счисления. Кроме того, стоит подумать о необходи-
мом количестве разрядов после десятичной точки (например, 2, если ваше
приложение работает с «деньгами»).
получим результат:
2.72
1.41
генерирует результат
1.2e3
Код
cout << setprecision( 2 ) << 1234 << endl;
генерирует результат
1234
генерирует результат
0x20
дает
0x20
if ( result != 0 )
{
cout << "Function call failed: " << result;
}
int res_val;
if ( result != 0 )
{
cout << "Function call failed: " << result;
}
else
{
// используем res_val для каких-либо действий
}
try
{
// код, генерирующий исключение при ошибке
}
catch(const FileNotFoundException& e)
{
// обработка ошибок
// невозможности найти файл
}
catch(const HardDriveFullException& e)
{
// обработка ошибок нехватки
// свободного места на диске
}
catch(...)
{
// обработка всех остальных ошибок
// (т. е. место, куда функция возвращает управление)
}
int main()
{
try
{
callFailableFunction();
}
catch(...)
{
// обработка ошибки
}
}
Исключения и отчеты об ошибках 441
int main()
{
try
{
callFailableFunction();
}
catch(...)
{
// обработка ошибки
}
}
442 Глава 31. Исключения и отчеты об ошибках
Создание исключений
Вы уже видели много примеров, иллюстрирующих перехват и обработку
исключения. Но как сгенерировать исключение самостоятельно? В этом
нет ничего сложного, поскольку исключение представляет собой обычный
класс. Так что заполняем поля по собственному усмотрению и предоставля-
ем методы для чтения информации об исключении. Интерфейс типичного
исключения выглядит так:
class Exception
{
public:
virtual ~Exception() = 0;
virtual int getErrorCode() = 0;
virtual string getErrorReport() = 0;
};
FileNotFoundException(
int err_code,
const string& details
)
: _err_code(err_code)
, _details(details)
{}
virtual ~FileNotFoundException()
{}
private:
int _err_code;
string _details;
};
Исключения и отчеты об ошибках 443
Спецификация исключений
Генерировать и перехватывать исключения можно при появлении ошибок,
но как узнать, создает ли исключения конкретная функция? В C++ можно
задать исключения, которые может генерировать функция, с помощью
спецификации исключений. Спецификация исключений представляет
собой список исключений (возможно, пустой) в конце объявления и опре-
деления функции.
В заголовочном файле:
void canFail() throw(FileNotFoundException);
void cannotFail() throw();
В файле cpp:
Преимущества исключений
У исключений есть два важных преимущества. Первое из них в том, что
исключения упрощают логику обработки ошибок, целиком помещая ее
в единственный блок catch и устраняя необходимость выполнения много-
численных проверок кодов ошибок. Второе преимущество в том, что ис-
ключения содержат больше информации об ошибке, чем ее код.
Благодаря первому преимуществу исключений можно преобразовать код
if(funCall1() == ERROR)
{
// обработка ошибки
}
if(funCall2() == ERROR)
{
// обработка ошибки
}
if(funCall3() == ERROR)
{
// обработка ошибки
}
в код
try
{
funCall1();
funCall2();
funCall3();
}
catch(const Exception& e)
{
// обработка ошибки
}
Поскольку язык HTML, как правило, задает тип элемента страницы корот-
ким ключевым словом, можно легко написать методы, которые определяют,
является ли следующий элемент ссылкой или таблицей. Теперь вместо
сложных исключений программа использует простые операторы if.
Коротко об исключениях
Исключения — это четко структурированные оповещения об ошибках,
которые позволяют не загромождать код обработкой отдельных некоррект-
ных ситуаций. Благодаря очистке стека и деструкторам, уничтожающим
объекты, исключения позволяют создавать код, большая часть которого
реализует основной алгоритм, а не проверяет коды ошибок.
Исключения и отчеты об ошибках 447
Глава 2
1. Какое значение возвращает операционной системе программа после
успешного выполнения?
А. –1
Б. 1
В. 0
Г. Программы не возвращают значения.
2. Как называется единственная функция, входящая во все программы
на C++?
А. start()
Б. system()
В. main(
Г. program()
3. Какие знаки пунктуации показывают начало и окончание блоков кода?
А. { }
Б. -> и <-
В. BEGIN и END
Г. ( и )
4. Какой знак пунктуации ставится в конце большинства строк кода C++?
А. .
Б. ;
В. :
Г. '
Ответы к разделам «Проверьте себя» 451
Глава 3
1. Какой тип переменной следует использовать для хранения числа
3.1415?
А. int
Б. char
В. double
Г. string
2. Какой оператор сравнивает две переменные?
А. :=
Б. =
В. equal
Г. ==
3. Как получить доступ к строковому типу данных?
А. Он встроен в язык, поэтому не нужно предпринимать никаких спе-
циальных действий.
Б. Поскольку строки используются при вводе-выводе, необходимо
включить в программу заголовочный файл iostream.
В. Необходимо включить в программу заголовочный файл string.
Г. Язык C++ не поддерживает строки.
4. Какое из приведенных ниже слов не обозначает тип переменной?
А. double
Б. real
В. int
Г. char
452 Ответы к разделам «Проверьте себя»
Глава 4
1. Какое из значений эквивалентно true?
А. 1
Б. 66
В. .1
Г. -1
Д. Все.
2. Как обозначается оператор логического И?
А. &
Б. &&
В. |
Г. |&
3. Чему равно значение выражения !(true && ! (false || true))?
А. true
Б. false
Ответы к разделам «Проверьте себя» 453
Глава 6
1. Что из перечисленного ниже не является правильным прототипом?
А. int funct(char x, char y);
Б. double funct(char x)
В. void funct();
Г. char x();
(Обратите внимание на отсутствие точки с запятой.)
Глава 7
1. Какой знак следует за оператором case?
А. :
Б. ;
В. -
Г. Символ новой строки.
2. Какой оператор необходимо использовать, чтобы избежать сквозного
выполнения операторов case?
А. end;
Б. break;
Ответы к разделам «Проверьте себя» 455
В. stop;
Г. Нужно поставить точку с запятой.
3. Какое ключевое слово обрабатывает неучтенные варианты?
А. all
Б. contingency
В. default
Г. other
4. Чему равен результат выполнения следующего кода?
int x = 0;
switch(x)
{
case 1: cout << "One";
case 0: cout << "Zero";
case 2: cout << "Hello World";
}
А. 1
Б. 0
В. Hello World
Г. ZeroHello World
Глава 8
1. Что произойдет, если не вызвать функцию srand перед функцией rand?
А. Функция rand завершится ошибкой.
Б. Функция rand будет всегда возвращать 0.
В. Функция rand будет возвращать одну и ту же последовательность
чисел при каждом запуске программы.
Г. Ничего.
2. Для чего следует задавать начальное число, передавая текущее время
в функцию srand?
А. Чтобы программа всегда выполнялась одинаково.
Б. Чтобы генерировать новые случайные числа при каждом запуске
программы.
В. Чтобы компьютер генерировал совершенно случайные числа.
Г. Это уже сделано за вас; если вы хотите, чтобы начальное число всегда
было одним и тем же, то вам нужно лишь вызвать функцию srand.
456 Ответы к разделам «Проверьте себя»
Глава 10
1. Что из нижеперечисленного является корректным объявлением массива?
А. int my_array[10];
Б. int anarray;
В. anarray{10};
Г. array anarray[10];
2. Какой индекс имеет последний элемент массива, состоящего из 29 эле-
ментов?
А. 29
Б. 28
В. 0
Г. Этот индекс определяется программистом.
3. Что из перечисленного является двумерным массивом?
А. array anarray[20][20];
Б. int anarray[20][20];
В. int array[20, 20];
Г. char array[20];
Ответы к разделам «Проверьте себя» 457
Глава 11
1. Что из перечисленного получает доступ к переменной в структуре b?
А. b->var;
Б. b.var;
В. b-var;
Г. b>var;
2. Что из перечисленного является корректно определенной структурой?
А. struct {int a;}
Б. struct a_struct {int a};
В. struct a_struct int a;
Г. struct a_struct {int a;};
3. Что из перечисленного объявляет структурную переменную типа foo
с именем my_foo?
А. my_foo as struct foo;
Б. foo my_foo;
В. my_foo;
Г. int my_foo;
4. Каково окончательное значение, получаемое в результате выполнения
следующего кода?
#include <iostream>
using namespace std;
struct MyStruct
продолжение
458 Ответы к разделам «Проверьте себя»
{
int x;
};
void updateStruct (MyStruct my_struct)
{
my_struct.x = 10;
}
int main()
{
MyStruct my_struct;
my_struct.x = 5;
updateStruct(my_struct);
cout << my_struct.x << '\n';
}
А. 5
Б. 10
В. Этот код не скомпилируется.
Глава 12
1. Что из нижеперечисленного НЕ является веской причиной для ис-
пользования указателей?
А. Вы хотите, чтобы функция изменяла переданный ей аргумент.
Б. Вы хотите сэкономить место, избегая копирования переменной
большого размера.
В. Вы хотите иметь возможность запрашивать дополнительную память
у операционной системы.
Г. Вы хотите быстрее получать доступ к переменным.
2. Что хранит указатель?
А. Имя другой переменной.
Б. Целое значение.
В. Адрес другой переменной в памяти.
Г. Адрес в памяти, но не обязательно относящийся к другой пере-
менной.
3. Как получить дополнительную память в процессе выполнения про-
граммы?
А. Вы не можете получить дополнительную память.
Б. В стеке.
В. В свободном хранилище.
Г. Объявив другую переменную.
Ответы к разделам «Проверьте себя» 459
Глава 13
1. Что из нижеперечисленного является корректным объявлением ука-
зателя?
А. int x;
Б. int &x;
В. ptr x;
Г. int *x;
2. Что из нижеперечисленного является адресом памяти целочисленной
переменной a?
А. *a;
Б. a;
В. &a;
Г. address(a);
460 Ответы к разделам «Проверьте себя»
Глава 14
1. Что из нижеперечисленного является ключевым словом, с помощью
которого в C++ выделяется память?
А. new
Б. malloc
В. create
Г. value
Ответы к разделам «Проверьте себя» 461
Глава 15
1. В чем преимущество связанного списка перед массивом?
А. В связанных списках меньше средний размер элемента.
Б. Связанные списки могут динамически расширяться; в них можно
добавлять отдельные новые элементы, не копируя уже существующие.
В. Поиск отдельного элемента в связанных списках быстрее, чем в мас-
сивах.
Г. Элементами связанных списков могут являться структуры.
2. Какое из перечисленных утверждений верно?
А. Нет никаких причин пользоваться массивами.
Б. Характеристики эффективности связанных списков и массивов
одинаковы.
В. Связанные списки и массивы обеспечивают одинаковое время до-
ступа к элементу по его индексу.
Г. Добавление элемента в середину связанного списка требует меньше
времени, чем добавление в середину массива.
3. Когда обычно используется связанный список?
А. Если необходимо хранить единственный элемент.
Б. Если число хранимых элементов известно на этапе компиляции.
В. Чтобы динамически добавлять и удалять элементы.
Г. Чтобы мгновенно получать доступ к любому элементу отсортиро-
ванного списка без каких-либо итераций.
4. Почему можно объявить связанный список со ссылкой на тип его эле-
мента? (struct Node {Node* p_next;};)
А. Объявить список таким способом нельзя.
Б. Потому что компилятор может определить, что на самом деле не
требуется память для элементов, которые ссылаются сами на себя.
В. Поскольку тип является указателем, требуется лишь достаточно
места для хранения одного указателя; память для следующего узла
будет выделена позже.
Г. Такое объявление допустимо только при условии, что указателю
p_next не присвоен адрес следующей структуры.
5. Почему в конце связанного списка важно использовать значение NULL?
А. Потому что оно отмечает конец списка и предотвращает доступ
кода к неинициализированной памяти.
Б. Потому что без него список превращается в последовательность
циклических ссылок.
Ответы к разделам «Проверьте себя» 463
Глава 16
1. Что такое хвостовая рекурсия?
А. Когда вы подзываете свою собаку.
Б. Когда функция вызывает сама себя.
В. Когда рекурсивная функция вызывает себя непосредственно перед
возвращением.
Г. Когда можно написать рекурсивный алгоритм в виде цикла.
2. Когда можно воспользоваться рекурсией?
А. Когда невозможно написать алгоритм в виде цикла.
Б. Когда алгоритм естественнее выражается в виде подзадачи, а не
цикла.
В. Никогда — это слишком сложно.
Г. При работе с массивами и связанными списками.
3. Каковы неотъемлемые составляющие рекурсивного алгоритма?
А. Базовый вариант и рекурсивный вызов.
Б. Базовый вариант и способ сведения задачи к уменьшенной версии
самой себя.
В. Способ перекомпоновки уменьшенных версий задачи.
Г. Все вышеперечисленное.
4. Что может произойти при неполном базовом варианте?
А. Преждевременное завершение алгоритма.
Б. Ошибка компилятора.
В. Ничего — это не проблема.
Г. Переполнение стека.
464 Ответы к разделам «Проверьте себя»
Глава 17
1. В чем главное достоинство двоичного дерева?
А. Оно использует указатели.
Б. Оно может хранить любое количество данных.
В. Оно позволяет быстро искать данные.
Г. Из него легко удалить данные.
2. В каких случаях вы предпочтете использовать связанный список вместо
двоичного дерева?
А. Если необходимо обеспечить быстрый поиск данных.
Б. Если необходимо получать доступ к отсортированным элемен-
там.
В. Если необходимо быстро добавлять элементы в начало или конец
структуры, при этом элементы в ее середине никогда не использу-
ются.
Г. Если не нужно освобождать используемую память.
3. Какое из нижеперечисленных высказываний верно?
А. Порядок добавления элементов в двоичное дерево может изменить
его структуру.
Б. Чтобы структура двоичного дерева была оптимальной, следует до-
бавлять в него элементы в отсортированном порядке.
В. Поиск элементов в связанном списке будет быстрее, чем в двоичном
дереве, если элементы вставляются в двоичное дерево в случайном по-
рядке.
Г. Структура двоичного дерева никогда не может быть такой же, как
у связанного списка.
4. Какая из причин объясняет высокую скорость поиска узлов в двоичных
деревьях?
А. Двоичные деревья не обеспечивают быстрый поиск узлов — наличие
двух указателей увеличивает объем работы при обходе дерева.
Б. Спускаясь по дереву на один уровень, вы приблизительно вдвое
сокращаете количество узлов, в которых требуется найти искомый
узел.
В. Эффективность двоичных деревьев не выше, чем у связанных спи-
сков.
Г. Обработка двоичных деревьев с помощью рекурсивных вызовов
происходит быстрее, чем обработка связанных списков с помощью
циклов.
Ответы к разделам «Проверьте себя» 465
Глава 18
1. Когда уместно использовать вектор?
А. Для хранения связи ключа и значения.
Б. Для максимального быстродействия при изменении коллекции
элементов.
В. Если программист хочет избавить себя от нюансов обновления
структуры данных.
Г. Использование вектора всегда уместно.
2. Как одновременно удалить все элементы из словаря?
А. Присвоить элементу пустую строку.
Б. Вызвать метод erase.
В. Вызвать метод empty.
Г. Вызвать метод clear.
3. Когда следует реализовывать собственные структуры данных?
А. Когда требуется обеспечить высокое быстродействие программы.
Б. Когда требуется обеспечить высокую надежность программы.
В. Когда требуется напрямую работать со структурой данных, на-
пример строить дерево выражений.
Г. Реализовывать собственные структуры данных можно лишь ради
удовольствия.
4. Какая из конструкций корректно объявляет итератор, который можно
использовать с вектором vector<int>?
А. iterator<int> itr;
Б. vector::iterator itr;
В. vector<int>::iterator itr;
Г. vector<int>::iterator<int> itr;
5. Что из нижеперечисленного получает доступ к ключу элемента, на
который в текущий момент указывает итератор словаря?
А. itr.first
Б. itr->first
В. itr->key
Г. itr.key
6. Как определить, можно ли использовать итератор?
А. Сравнить его с NULL.
Б. Сравнить его со значением, возвращенным методом end() контей-
нера, перебор которого выполняется.
466 Ответы к разделам «Проверьте себя»
В. Сравнить его с 0.
Г. Сравнить его со значением, возвращенным методом begin() контей-
нера, перебор которого выполняется.
Глава 19
1. Какой код корректен?
А. const int& x;
Б. const int x = 3; int *p_int = & x;
В. const int x = 12; const int *p_int = & x;
Г. int x = 3; const int y = x; int& z = y;
2. Какие из перечисленных ниже прототипов функций позволяют ском-
пилировать следующий код: const int x = 3; fun( x );?
А. void fun (int x);
Б. void fun (int& x);
В. void fun (const int& x);
Г. А и Б.
3. Как лучше всего определить, что поиск строки завершится неудачно?
А. Сравнить позицию результата с 0.
Б. Сравнить позицию результата с –1.
В. Сравнить позицию результата с string::npos.
Г. Проверить, больше ли позиция результата длины строки.
4. Как создать итератор для константного контейнера STL?
А. Объявить итератор константным.
Б. Воспользоваться индексами для циклического прохода по нему, а не
итератором.
В. Воспользоваться const_iterator.
Г. Объявить типы шаблонов константными.
Глава 21
1. Что из нижеперечисленного не входит в состав процесса сборки C++?
А. Компоновка.
Б. Компиляция.
В. Предварительная обработка.
Г. Окончательная обработка.
Ответы к разделам «Проверьте себя» 467
Глава 22
1. В чем заключается преимущество использования функции перед пря-
мым доступом к данным?
А. Компилятор может оптимизировать функцию и ускорить доступ
к данным.
Б. Функция скрывает нюансы реализации от вызывающего окруже-
ния, что упрощает его изменение.
В. Функции — единственный способ хранения одной структуры данных
в нескольких файлах с исходным кодом.
Г. Такого преимущества нет.
2. Когда следует помещать код в общую функцию?
А. Если собираетесь вызвать ее хотя бы один раз.
Б. Если используете один и тот же код более чем в двух местах про-
граммы.
468 Ответы к разделам «Проверьте себя»
Глава 23
1. Почему следует использовать метод вместо прямого доступа к полю
структуры?
А. Метод легче читать.
Б. Метод работает быстрее.
В. Следует пользоваться не методом, а прямым доступом к полю.
Г. Это позволяет изменить представление данных.
2. Что из нижеперечисленного определяет метод, связанный со структу-
рой struct MyStruct{int func();};?
А. int func() {return 1;}
Б. MyStruct::int func() {return 1;}
В. int MyStruct::func() {return 1;}
Г. int MyStruct func () {return 1;}
3. Почему следует размещать определение метода внутри класса?
А. Чтобы пользователи класса понимали, как он работает.
Б. Чтобы ускорить выполнение кода.
В. Не следует, поскольку это разглашает излишние подробности
реализации.
Г. Не следует, поскольку это замедляет работу программы.
Глава 24
1. Для чего используются закрытые данные?
А. Для защиты от хакеров.
Б. Чтобы другие программисты не могли их изменять.
В. Для внутренней реализации класса.
Ответы к разделам «Проверьте себя» 469
Глава 25
1. Когда необходимо создавать конструктор класса?
А. Всегда — без конструктора класс нельзя использовать.
Б. Если необходимо инициализировать класс значениями, отличными
от значений по умолчанию.
В. Никогда — компилятор всегда предоставляет конструктор класса.
Г. Только если уже есть деструктор.
2. Как связаны деструктор и оператор присваивания?
А. Никак.
Б. Деструктор класса вызывается до выполнения оператора присваи-
вания.
В. Оператор присваивания должен указать, какую память должен уда-
лить деструктор.
470 Ответы к разделам «Проверьте себя»
Глава 26
1. Когда запускается деструктор суперкласса?
А. Только когда объект уничтожается путем освобождения указателя
на суперкласс с помощью оператора delete.
Б. Перед вызовом деструктора подкласса.
В. После вызова деструктора подкласса.
Г. В процессе выполнения деструктора подкласса.
2. Что необходимо сделать в конструкторе класса Cat в условиях приве-
денной ниже иерархии классов?
class Mammal {
public:
Mammal (const string& species_name);
};
А. Ничего особенного.
Б. Вызвать конструктор Mammal с аргументом cat, воспользовавшись
списком инициализации.
В. Вызвать конструктор Mammal с аргументом cat.
Г. Удалить конструктор Cat и воспользоваться конструктором по умол-
чанию, который выполнит все требуемые действия.
3. В чем ошибка в следующем определении класса?
class Nameable
{
virtual string getName();
};
Глава 27
1. Когда следует использовать директиву using namespace?
А. Во всех заголовочных файлах после директивы include.
Б. Никогда, это опасно.
В. В начале любого файла cpp при отсутствии конфликтов пространств
имен.
Г. Непосредственно перед использованием переменной из соответству-
ющего пространства имен.
Ответы к разделам «Проверьте себя» 473
Глава 28
1. Какой тип можно использовать для чтения из файла?
А. ifstream
Б. ofstream
В. fstream
Г. А и В
2. Какое из нижеперечисленных утверждений верно?
А. Текстовые файлы занимают меньше места, чем двоичные.
Б. Двоичные файлы легче отлаживать.
В. Двоичные файлы эффективнее используют пространство, чем
текстовые.
Г. Текстовые файлы слишком медленны для использования в реальных
программах.
474 Ответы к разделам «Проверьте себя»
Глава 29
1. Для чего следует использовать шаблоны?
А. Для экономии времени.
Б. Для ускорения выполнения кода.
В. Для написания одного и того же кода для разных типов данных.
Г. Для возможности повторного использования кода в будущем.
2. Когда необходимо указывать тип параметра шаблона?
А. Всегда.
Б. Только при объявлении экземпляра класса шаблона.
В. Только если тип нельзя определить автоматически.
Г. В шаблонных функциях, при условии, что тип нельзя определить
автоматически, и всегда для классов шаблона.
3. Как компилятор определяет, можно ли применить шаблонный параметр
к конкретному шаблону?
А. Он реализует определенный интерфейс C++.
Б. Следует задать соответствующие ограничения при объявлении ша-
блона.
Ответы к разделам «Проверьте себя» 475
ООО «Питер Пресс», 192102, Санкт-Петербург, ул. Андреевская (д. Волкова), д. 3, литер А, пом. 7Н.
Налоговая льгота — общероссийский классификатор продукции ОК 034-2014,
58.11.12.000 — Книги печатные профессиональные, технические и научные.
Подписано в печать 27.02.15. Формат 70х100/16. Усл. п. л. 38,700. Тираж 1000. Заказ
Отпечатано в ООО «Чеховский печатник». МО, г. Чехов, ул. Полиграфистов, 1.
Изучаем C#
3-е издание
Э. Стиллмен, Дж. Грин