/
Автор: Фридман А. Клаидер Л. Михаэлис М. Шильдт X.
Теги: языки программирования программирование компьютерные технологии язык программирования c++ язык программирования c
ISBN: 5-7989-0205-6
Год: 2001
Текст
УДК 004.43
ББК 32.973-018.1
Ф88
Фридман А., Клаидер Л., Михаэлис М., Шильдт X.
C/C++. Архив программ — М.: ЗАО «Издательство БИНОМ», 2001 г. —
640 с: ил.
В книге представлен код работоспособных программ на C/C++, относящихся к
самым разнообразным областям и аспектам написания приложений — от работы с
достаточно простыми структурами данных (списки, деревья) до построения
синтаксических анализаторов и интерпретаторов, доступа к Internet и т. п. Программный код
может использоваться в ваших программах без изменений или в модифицированном и
усовершенствованном виде. Чрезвычайно полезно также его изучение в целях
освоения главнейших принципов, алгоритмов и приемов решения разных задач. Программы
снабжены развернутыми комментариями и детальными пояснениями их работы.
Для широкого круга программистов, пишущих на языках C/C++.
ISBN 5-7989-0205-6 (русск.)
ISBN 0-07-882504-0 (англ.)
Authorized translation from the English
language edition.
© Original copyright. Osborne/McGraw-Hill, 1999
© Издание на русском языке.
ЗАО «Издательство БИНОМ», 2001
Содержание
Предисловие 11
Глава 1. Сортировка 13
Введение в сортировку 14
Пузырьковая сортировка 15
Выборочная сортировка 18
Базовая быстрая сортировка 21
Улучшенная быстрая сортировка 26
Сортировка слиянием 31
Глава 2. Связанные списки 37
Введение в связанные списки 38
Односвязные списки 40
Простой односвязный список 40
Шаблон односвязного списка 46
Двусвязные списки 54
Простой двусвязный список 54
Шаблон двусвязного списка 60
Глава 3. Двоичные деревья 71
Введение в двоичные деревья 72
Двоичные деревья поиска 73
Простой шаблон двоичного дерева поиска 74
Сбалансированное двоичное дерево поиска 92
Глава 4. Смешанные таблицы
и разреженные массивы 119
Введение в смешанные таблицы и разреженные массивы 120
Проектирование смешанной таблицы 120
Простая смешанная таблица с целыми ключами 122
Смешанная таблица с ключами типа char * 129
Проектирование разреженного массива 137
Одномерный разреженный массив 138
Двумерный разреженный массив 142
Глава 5. Управление памятью 149
Управление памятью в С и C++ 150
Перегрузка глобальной операции new 151
Простой аллокатор, основанный на массиве 154
Простой аллокатор, основанный на списке 159
6 C/C++. Архив программ
Глава 6. Работа с файлами и каталогами 165
Поиск и замена в текстовом файле 166
Работа с файловой системой 178
Просмотр содержимого файла 193
Глава 7. Основы шифровки 199
Что такое шифровка 200
Усиление защиты шифра 206
Более сложный алгоритм шифровки 206
Ограниченность традиционных систем шифровки с одним ключом . . . 215
Криптосистемы с общим ключом 216
Цифровые подписи 217
Построение цифровой подписи 217
Алгоритм Райвеста-Шамира-Эйдлмана (RSA) 219
Свойства алгоритма RSA 220
Алгоритм RSA как таковой 221
Математика алгоритма 221
Смешанные значения 222
API шифровки системы Windows 222
Криптографические серверы (CSP) 224
Программная модель CryptoAPI. 225
Вызов базовых функций CryptoAPI 226
Цифровые подписи в CryptoAPI 229
Поддержка CryptoAPI в программах MFC 229
Код класса CCryptoDoc 230
Последние замечания о CryptoNotes и шифрах 238
Глава 8. Управление исходным кодом 241
Проектирование программы управления кодом 243
Классы лексем 244
Класс сканера CScanner 277
Инициализация сканера для C++ 287
Класс синтаксического разбора CCodeParser 294
Главная программа 317
Компиляция SCodeMnt.exe 324
Запуск SCodeMnt.exe 325
Чем можно заняться 325
Глава 9. C/C++ в Internet 327
Службы Internet 328
WinSockAPI 328
Инициализация WinSock 329
Служба имен 330
Вопросы, касающиеся порядка байтов 330
Коммуникация через сокеты 331
Проблема блокировки и вызов selectQ 331
Содержание 7
Асинхронные вызовы сокетов 332
Синхронные операции и сериализация 333
Проект простого обозревателя 333
Новый Internet-класс CHtmlView 375
Управляющий элемент обозревателя 375
Создание проекта, использующего CHtmlView 376
Перемещение в среде CHtmlView 377
Простая программа-обозреватель на основе класса CHtmlView 379
Обозреватель для просмотра результатов поиска 379
Глава 10. Финансовые расчеты 381
Вычисление амортизации 382
Линейная амортизация 382
Код программы для расчета линейной амортизации 383
Амортизация по сумме цифр лет 385
Код программы для амортизации по сумме цифр лет 387
Амортизация по балансу с двойным наклоном 391
Код программы для амортизации по балансу с двойным наклоном . . . 393
Функции, связанные с рентой 397
Вычисление конечной стоимости при единственном
начальном взносе 398
Код для вычисления конечной стоимости при единственном
начальном взносе 399
Расчет конечной стоимости ряда взносов 401
Код для вычисления конечной стоимости
при последовательных взносах 402
Расчет одиночного взноса, достигающего заданной
конечной стоимости 404
Код для расчета взноса, достигающего заданной стоимости 404
Расчет взносов, необходимых для достижения указанной
конечной стоимости 405
Код для расчета ряда взносов, достигающих целевой суммы 406
Написание простого калькулятора ссуды 407
Код для расчета выплат по ссуде 408
Объединение различных вычислений 409
Код для расчета взносов в пенсионный фонд 410
Глава 11. Статистические расчеты 413
Введение в средние значения 414
Среднее арифметическое, медиана и модус 415
Другие распространенные виды средних 423
Среднее взвешенное 423
Среднее геометрическое 424
Вычисление среднего квадратичного и квадратичной суммы 425
Оценка вероятностей 430
Второй закон вероятности 432
Третий закон вероятности 432
8 C/C++. Архив программ
Четвертый закон вероятности 432
Регрессионный анализ 439
Глава 12. Создание фракталов 445
Введение во фракталы 446
Краткое замечание о графиках 448
Обзор архитектуры документ/вид 449
Глава 13. Объектно-ориентированный анализатор
выражений 483
Основы синтаксического анализа 484
Выражения 484
Анализ выражений: проблема 485
Синтаксический анализ выражения 486
Разбиение выражения на лексемы 487
Простой анализатор выражений 489
Анализатор, воспринимающий переменные 497
Обобщенный анализатор 505
Расширение и улучшение анализатора 512
Глава 14. Реализация языковых
интерпретаторов на C++ 515
Анализатор выражений Small BASIC 517
Выражения Small BASIC 518
Лексемы языка Small BASIC 518
Интерпретатор языка Small BASIC . 529
Ключевые слова 539
Загрузка программы 540
Главный цикл 541
Функция присваивания 542
Команда PRINT 543
Команда INPUT 544
Команда GOTO 545
Оператор IF 547
Цикл FOR 548
GOSUB 551
Работа со Small BASIC 552
Усовершенствование и расширение интерпретатора 554
Создание своего собственного компьютерного языка 554
Глава 15. Исследование библиотеки
стандартных шаблонов 555
Обзор библиотеки стандартных шаблонов 556
Контейнеры 556
Итераторы 557
Алгоритмы 558
Содержание
9
Функциональные объекты, определяемые пользователем 558
Сортировки STL 559
Алгоритм sort() 559
Алгоритм partial_sort() 561
Функция sort_heap 562
Сортировка STL-контейнеров с элементами,
определенными пользователем 564
Определяемый пользователем критерий сортировки 566
Поиск в контейнерах STL 568
Алгоритм find() 569
Алгоритм binary_search() 571
Использование функциональных объектов 572
Контейнеры и потоки 574
Чтение элементов контейнера из файла 576
Сравнение строк 577
Алгоритмы теории множеств 579
Поиск подмножества строк 580
Обслуживание приоритетных сообщений 583
Контейнер двоичного дерева 586
Глава 16. C/C++ в разработках CGI 605
Архитектура CGI 606
FTP и HTTP: сохранение статуса 607
Эффективность коммуникации без сохранения статуса 608
Четыре шага транзакции HTTP 609
1-й шаг: Создание соединения 609
2-й шаг: Запрос клиента 610
3-й шаг: Отклик сервера 611
4-й шаг: Разрыв соединения сервером 611
Подробнее об URI 612
ВнутрииКЬ 612
URL, протоколы и типы файлов 613
Фрагменты URL 613
URLhHTML 614
Абсолютные и относительные URL 614
Место CGI в модели Web 615
Разработка компонента часов 616
Компонент часов 616
Сценарий CGI, использующий метод POST протокола HTTP 622
Использование приложений CGI 636
Взаимодействие сервера с программой CGI 636
Обращение из обозревателя к программе CGI 637
Предисловие
Это книга написана программистами, пишущими на C/C++, — для
программистов, пишущих на C/C++. В 16-ти ее главах вы найдете самый
интересный и полезный код, приложения которого вы можете видеть повсюду.
Программы и подсистемы можно использовать так, как они написаны, или как
основу для ваших собственных разработок. Будете ли вы относительным
новичком в программировании или испытанным профессионалом, я уверен, что
книга вам понравится.
Книга появилась на свет довольно курьезным образом. Скотт Роджерс,
один из издателей и главный редактор Осборна, задумал выпустить серию
книг, которые стали называться Annotated Archives. Эти книги должны были
собрать вместе массу практических программ и библиотек, сопровождаемых
исчерпывающими, углубленными описаниями кода, предложениями по его
развитию и советами относительно его применения. Я сразу понял, что это
замечательная мысль и что из этого может получиться отличная книга по
программированию на C/C++. Единственным препятствием было то, что мой
писательский план и так уже был забит до отказа, и я не мог сам написать всю
книгу. (Из-за быстроты перемен и инноваций нагрузка на мое расписание
всегда очень велика.)
Именно тогда за дело взялся Уэнди Ринальди, мой основной редактор в Ос-
борне. Объединив наши умственные усилия, мы придумали план. Мы решили
собрать нескольких авторов, каждый из которых напишет о том, что его
особенно интересует. Я сам напишу две главы, а потом буду выступать в качестве
консультанта и советчика. При таком подходе мы смогли своевременно
предоставить для книги код самого высокого качества и при этом сохранить ее
связность. Я думаю, вас удовлетворят результаты того, что мы сделали.
Книгу открывают пять глав, написанных Артом Фридманом,
охватывающие наиболее фундаментальные структуры и алгоритмы компьютерной
обработки данных: сортировку, поиск, двоичные деревья, смешанные таблицы,
разреженные массивы и управление памятью. Арт предлагает углубленное
изучение этой тематики и приводит ряд изощренных и элегантных
реализаций. По большей части он спроектировал свой код в виде подсистем, которые
вы можете вставить прямо в свой следующий проект. Арт даже реализовал
набор процедур, поддерживающих балансировку двоичных деревьев. Если вам
когда-нибудь приходилось этим заниматься, вы знаете, что это такое.
Ларе Кландер написал несколько глав, посвященных различным
популярным предметам, которые, я надеюсь, будут вам интересны. Например, Ларе
рассказывает о шифрах, файловых утилитах, финансовых, статистических
расчетах и фракталах. Он числе прочего он приводит код для простого
обозревателя Internet. Так что, если вам потребуются процедуры для расчета выплат
по ссуде, вычисления стандартного отклонения выборки, шифровки файла
и т. д., главах Ларса вы найдете все, что нужно.
Сам я написал две главы. Первая содержит три версии синтаксического
анализатора с рекурсивным спуском для разбора численных выражений,
таких, как (10+2)/3. Такой анализатор в ряде ситуаций бывает весьма полезен.
Другая глава исследует языковые интерпретаторы. В качестве примера анали-
12 Предисловие
затор и интерпретатор комбинируются, образуя интерпретатор для простого
языка, подобного Basic. Однако описанные там методики можно адаптировать
для создания интерпретатора языка любого типа, какого захотите. Если вы
когда-нибудь задумывались над тем, чтобы разработать свой собственный язык
программирования, то эта глава определенно для вас!
Марк Михаэлис представляет одну чрезвычайно увлекательную главу,
глубоко анализирующую вопросы обработки исходного кода. Он предлагает
процедуры синтаксического разбора, выделяющие из файла отдельные лексемы,
форматирующие файл, извлекающие имена заголовков и «раскрашивающие»
текст программы. Еще более ценными эти процедуры становятся благодаря
тому, что они не привязаны к конкрет ному языку и вы можете легко
приспособить и расширить их для обработки текстов на любом компьютерном языке.
И наконец, Эндрю Гейтер написал главу о различных аспектах STL. Если
вы еще недостаточно уверенно чувствуете себя в среде STL, то для вас эта глава
будет особенно полезной.
Одно последнее замечание. Во всех главах книги код следует современному
стилю программирования, согласующемуся со стандартом ANSI/ISO C++.
Поэтому программы будут работать с любым стандартным компилятором. Весь
код протестирован на компиляторе Visual C++ 6 корпорации Microsoft.
Херберт Шильдт
Магомет, Иллинойс
1999
5
ГЛАВА
.-4..
i
/^
? *
GtypjWHMttKj
V:
• l
',^-i,- l!«iiS--=3
bs.cpp
sort.h
ss.cpp
Арт Фридман
^4V
qs_basic.cpp
qs.cpp
ms.cpp
ML* у Г^Э
#./'
; I,
M' *«'S
14
Глава 1
Задачи сортировки данных в восходящем или нисходящем порядке
возникают в прикладном программировании буквально на каждом шагу. Как
только данные отсортированы, они сразу становятся более осмысленными и в
них гораздо легче ориентироваться. Поэтому сортировка, как правило, стоит
затраченного на нее времени, особенно если она выполняется эффективно, и в
этом отношении в программировании подробно исследованы различные
методики сортировки.
Введение в сортировку
В этой главе представлено несколько хорошо известных методов
сортировки данных, находящихся в памяти. Два простых метода, пузырьковая
сортировка и выборочная сортировка, сравнительно неэффективны; их среднее
время выполнения пропорционально квадрату п, числа сортируемых элементов.
Эти методы просты для понимания и надежны в реализации. Когда п
невелико, они могут оказаться вполне приемлемыми.
Два других метода, обычно называемых быстрой сортировкой и
сортировкой слиянием, более сложны, но лучше подходят для больших значений п. Их
среднее время выполнения пропорционально n*lg(n). Когда число
сортируемых элементов велико, эти методы имеют значительное преимущество перед
более простыми. Но хотя среднее время быстрой сортировки весьма неплохое,
в наихудшем случае ее эффективность неудовлетворительна. Поэтому
приводится второй ее вариант, для которого вероятность столкнуться с наихудшим
случаем на практике чрезвычайно низка. Все программы в этой главе
сортируют данные в восходящем порядке.
Целесообразно будет сделать несколько предварительных замечаний
относительно приводимого нами кода. Все функции определены как шаблоны
встроенных функций. Целью использования шаблонов является, как обычно,
стремление обеспечить возможность обработки разнообразных типов данных.
Причина объявления функций в качестве встроенных имеет двоякий
характер. Во-первых, некоторые из них будут более эффективны. Во-вторых, такое
объявление позволяет размещать их, если это желательно, в заголовочных
файлах. Применение ключевого слова inline не является существенным, и его
всегда можно опустить.
Каждая программа представлена своим .срр-файлом, который включает
заголовок sort.h. Этот заголовок содержит шаблоны двух функций:
♦ print<T>(T*, int) печатает значения массива в линейной
последовательности.
♦ swap<T>(T*, int, int) меняет местами два элемента массива.
Эти вспомогательные функции широко используются в наших примерах.
Помимо этого, файл sort.h не имеет существенного значения.
Наконец, два слова о массивах. Массив — это, вероятно, самый ясный и
простой способ представления линейной последовательности элементов. А
доступ к массиву в форме data[n] является простейшим способом ссылки на
n-ный элемент массива data. По этой причине предпочтение отдано индексам,
а не указателям. Читатели, для которых возможное < лижение эффективности
представляется существенным, могут при желании модифицировать обходы
массивов, применив арифметику указателей.
Сортировка
15
Пузырьковая сортировка
Пузырьковая сортировка чрезвычайно проста. Чтобы понять, как она
работает, представьте себе массив из пяти элементов, «поставленный»
вертикально, с нулевым элементом наверху и четвертым — внизу, как показано на
рис. 1.1. Пузырьковая сортировка проходит по массиву снизу вверх. Каждый
элемент массива сравнивается с тем, что находится непосредственно над ним,
и если последний больше, производится перестановка двух этих элементов.
Например, нижний элемент (с индексом 4) на рис. 1.1а оказывается
наименьшим. Алгоритм сортировки сравнивает его со значением элемента 3
(который равен 2) и решает поменять их местами. Затем он сравнивает элемент 3
с элементом 2 и также обменивает их. Процесс продолжается до тех пор, пока
наименьший элемент не «всплывает», подобно пузырьку воздуха, на самый
верх. В данный момент элемент 0 имеет правильное значение, и при
последующих проходах алгоритма его можно игнорировать. Результат первого прохода
показан на рис. 1.1b.
Затем пузырьковая сортировка применяет ту же самую процедуру к подмас-
сиву, состоящему из элементов с индексами от 1 до 4, в результате чего в
позиции 1 также оказывается правильное значение (в данном случае 2). Сортировка
продолжается в том же духе, сканируя все меньшие подмассивы, пока они не
будут исчерпаны. К этому моменту весь массив будет правильно сортирован.
Рис. 1.1.
Пузырьковая сортировка: в массиве
слева значение 1 находится в
исходном, 4-м элементе. Массив
справа показывает результат
«всплытия» этого значения в свое
конечное положение в элементе О
а:
0
1
2
3
4
4
3
5
2
1
Ь:
0
1
2
3
4
1
4
3
5
2
Код
Файл bs.cpp содержит код пузырьковой сортировки и пример его
использования. Вот файл sort.h, содержащий вспомогательные функции для данной и
других процедур сортировки этой главы:
|#include <iostream>
j#include <cassert>
{using namespace std;
// Файл sort.h содержит две полезных вспомогательных функции.
// Swap( ) обменивает элементы в позициях
16
Глава 1
t
i// posl и pos2 в пределах массива.
'J template <class T>
J inline void swap(T array[], int posl, int pos2)
J T temp;
i temp = array[posl];
1 array[posl] = array[pos2];
array[pos2] = temp;
!>
// Print( ) распечатывает элементы массива
■// в линейной последовательности.
template <class T>
'inline void print(T array[], int size)
i<
\ int i;
■J for (i=0; i < size; ++i) {
1 cout « array[i] « "
i >
, cout « endl;
.JJ
А вот сам файл bs.cpp:
#include "sort.h"
j// Файл bs.cpp реализует шаблон функции bubble__sort( ),
\l/ которая сортирует элементы своего входного массива
[// в восходящем порядке. Тип Т должен поддерживать
!// operator-( ) и operator<( ). Для инициализации может
!// потребоваться копирование. Если требуется печать, необходима
!// operator«{ ). Аргумент array[] содержит элементы, требующие
!// сортировки, a size является числом элементов этого массива.
!// Сортировка делается "по месту." Допускаются повторяющиеся
!// элементы.
template <class т>
inline void bubble_sort(T array[], int size)
t
int i, j;
// Верхний предел внешнего цикла равен size-1, а не
// size, так как если все прочие элементы заняли свои места,
// наибольший автоматически оказывается в правильной позиции,
for (i=0; i < size-1; ++i) {
for (j=size-l; j > i; —j ) {
if (array[j-l] >array[j]) swap(array, j-1, j);
}
}
}
Сортировка tT
int main( )
{
int array_l[] = {7, 3, 8, 2, 1, 5, 4);
print(array_l, 7);
bubble_sort(array_l, 7);
print(array_l, 7);
cout « endl;
int array_2[] « {7, 3, 8, 2, 1, 5, 4, 9, 75, -5};
print(array_2, 10);
bubble_sort(array_2, 10);
print(array_2, 10) ;
cout « endl;
int array_3[] = {1, 2, 3);
print(array_3, 3);
bubble_sort(array_3, 3);
print(array_3, 3);
cout « endl;
int array_4[] = {3, 2, 1);
print(array_4, 3);
bubble_sort(array_4, 3);
print(array_4, 3);
cout « endl;
int array_5[] *= {3, 2, 1, 3);
print(array_5, 4);
bubble_sort(array_5, 4);
print(array_5, 4);
cout « endl;
int array_6[] = {3, 3, 3};
print(array_6, 3);
bubble_sort(array_6, 3);
print(array_6, 3);
cout « endl;
return 0;
__}
Ниже приводится вывод bs.cpp. Каждая пара строк показывает целый
массив до и после сортировки.
7 3 8 2 15 4
12 3 4 5 7 8
73821549 75 -5
-5 12345789 75
12 3
12 3
3 2 1
12 3
18
Глава 1
3 2 13
12 3 3
3 3 3
3 3 3
I ПРИМЕЧАНИЯ
Функция bubble_sort(T*, int) принимает параметр-массив и его размер.
Вся функция состоит из внешнего цикла, определяющего, какой из подмасси-
вов обрабатывается в данный момент, и внутреннего цикла, который
сканирует данный подмассив и обменивает при необходимости соседние значения.
Сортировка проводится «по месту». Повторяющиеся значения не вызывают
никаких неприятностей. Заметьте, что внешний цикл выполняется size-1, а не
size раз. Как только все остальные элементы заняли правильные места,
наибольший элемент также должен автоматически занять свое место.
Внутренний цикл for проходит каждый из подмассивов в обратном
порядке, в направлении, противоположном внешнему циклу. Можно сделать
наоборот, при этом не наименьший элемент будет «всплывать», а наибольший —
«тонуть» на «дно» текущего подмассива. Вывод программы показывает, что
повторяющиеся значения обрабатываются правильно.
Выборочная сортировка
Хотя пузырьковая сортировка вполне работоспособна, она производит
гораздо больше действий, чем это необходимо — даже для простого метода.
Слабым местом ее, очевидно, является количество выполняемых перестановок, —
вместо того, чтобы ставить каждый элемент сразу на его конечное место, она
перемещает элементы каждый раз всего на одну позицию. Выборочная
сортировка основана на том же основном принципе, что и пузырьковая, но решает
эту ключевую проблему.
Если подвергнуть выборочной сортировке массив на рис. 1.2а, то алгоритм
сортировки также будет просматривать подмассивы, отыскивая каждый раз
минимальное значение. Однако вместо обмена пар соседних значений,
стоящих не в том порядке, алгоритм просто записывает индекс минимального
значения и затем производит единственный обмен его со значением, которое
занимает в данный момент верхнюю позицию в подмассиве. Первый проход по
Рис. 1.2.
Выборочная сортировка. В массиве слева значение 1 в
исходном состоянии занимает 4-й элемент. Массив справа
показывает результат обмена элемента 0 (т. е. начального
элемента массива) с элементом 4 (наименьшим в
массиве).
а:
0
1
2
3
4
4
3
5
2
1
Ь:
0
1
2
3
4
1
3
5
2
4
Сортировка /9
массиву на рис. 1.2а находит, что минимальное значение, равное 1, занимает
4-й элемент. Как только это определено, элементы нулевой и четвертый
обмениваются значениями. Результат показан на рис. 1.2Ъ. Затем выборочная
сортировка сканирует все меньшие и меньшие подмассивы, аналогично
пузырьковой сортировке.
Код
Код выборочной сортировки находится в файле ss.cpp. Выполнение
программы дает тот же результат, что и bs.cpp.
#include "sort.h"
[// Файл ss.cpp реализует шаблон функции selection_sort( ),
!// которая сортирует элементы своего входного массива
[// в восходящем порядке. Тип Т должен поддерживать
|// operator=( ) и operator<( ). Для инициализации
|// может потребоваться копирование. Если требуется печать,
// необходима operator«( ). Аргумент array содержит подлежащие
[// сортировке элементы, size является числом элементов
!// этого массива. Сортировка производится "по месту." Допускаются
[// повторяющиеся элементы.
!// Get_min_index( ) возвращает индекс массива, соответствующий
!// минимальному значению подмассива, определяемому
|// left и right.
template <class T>
inline int get_min_index(T array [], int left, int right)
f.1
f*^ int min__index = left;
J-„ int i;
t *
for (i = left; i <= right; ++i) {
if (array[i] < array [min__index]) min_index = i;
}
return min_index;
\lI Selection_sort( ) выбирает нужный элемент для каждого
// индекса массива, начиная с нулевого индекса и
!// двигаясь вверх.
[template <class T>
[inline void selection_sort(T array[], int size)
{
int i;
int min index;
20
Глава 1
i"-:
* j // Верхний предел внешнего цикла равен size-1, а не
f| // size, так как если все прочие элементы заняли свои места,
// наибольший автоматически оказывается в нужной позиции.
for (i=0; i < size-1; ++i) {
min_index = get_min_index(array, i, size-1);
if (min_index != i) swap(array, i, min_index);
I'
? i
:«
f
iv
t
int main( )
{
int array_l[] = {7, 3, 8, 2, 1, 5, 4>;
print(array_l, 7);
selection_sort(array_l, 7);
print(array_l, 7);
cout « endl;
int array_2[] - (7, 3, 8, 2, 1, 5, 4, 9, 75, -5};
print(array_2, 10);
selection_sort(array_2, 10);
print(array_2, 10);
cout « endl;
int array_3[] ■ (1, 2, 3} ;
print(array_3, 3);
selection_sort(array_3, 3);
print(array_3, 3);
cout « endl;
int array_4[] = {3, 2, 1};
print(array_4, 3);
selection_sort(array_4, 3);
print(array_4, 3);
cout « endl;
int array_5[] = {3, 2, 1, 3};
" •. print(array_5, 4) ;
.* * selection_sort(array_5, 4) ;
r
h
it Л
i
■*
t
П
I
- 1
»
1
print(array_5, 4);
cout « endl;
int array_6[] ■ {3, 3, 3};
print(array_6, 3);
selection_sort(array_6, 3);
print(array_6, 3);
cout « endl;
return 0;
}
Сортировка
21
| ПРИМЕЧАНИЯ
Код файла ss.cpp похож на код пузырьковой сортировки, за исключением
того, что в нем используется новая функция get_min_index(T*, int, int).
Функция возвращает индекс минимального значения в подмассиве, ограниченном
указанными индексами. «Внутренний цикл» функции selection_sort(T*, int) —
это, на самом деле, просто вызов get_min_index(T*, int, int).
Вызов swap(T*, int, int) производится только в случае, если наименьший
элемент еще не находится на своем месте. Тем самым мы не тратим
понапрасну времени, обменивая элемент сам с собой. Но на практике подобная
стратегия может как улучшить, так и ухудшить производительность. Очевидно,
если входной массив уже отсортирован, устранение таких избыточных
обменов окажется разумным. Кроме того, это целесообразно в ситуациях, когда
элементы данных очень велики и их присваивание обходится дорого. В других
случаях получаемая выгода может и не оправдывать затрат на
дополнительные сравнения индексов.
Базовая быстрая сортировка
Быстрая сортировка принципиально отличается от рассмотренных выше
простых методов. Ее стратегия основана на принципе «разделяй и властвуй»,
что приводит в среднем к значительно более эффективному выполнению
сортировки. Она вполне оправдывает свое название.
Рассмотрите пятиэлементный массив, показанный на рис. 1.3а. Его
элемент 0 расположен слева, элемент 4 — справа. Первое, что делает быстрая
сортировка — это выбор разделяющего значения из значений элементов массива.
В качестве разделяющего может быть выбран совершенно произвольный
элемент, однако стратегия выбора оказывает сильное влияние, на эффективность
алгоритма. Это влияние будет исследовано в следующем разделе, где
представлен усовершенствованный вариант быстрой сортировки. Пока же мы будем
выбирать в качестве разделяющего крайний левый элемент^ массива. В нашем
примере это элемент 0 со значением 3.
Следующим шагом будет разбиение массива на два подмассива с помощью
выбранного разделяющего значения. По завершении этого Шага разделяющее
значение окажется на правильном месте. Все элементы слева от него (т. е.
составляющие левый подмассив) будут меньше; все элементы справа будут
больше него. В этом, базовом, варианте алгоритма дублирование значений не
допускается.
Как достигается такое разбиение? Идея состоит в том, чтобы определить
два указателя — head и tail — которые первоначально ссылаются на первый и
последний элементы массива. По мере выполнения разбиения эти указатели
движутся навстречу друг другу и, наконец, встречаются. В этот момент
разбиение завершено.
Так как разбиение играет в быстрой сортировке столь важную роль, мы шаг
за шагом разберем детальный пример, показанный на рис. 1.3. Первым шагом
будет сравнение хвостового значения (т.е. значения элемента, на который
ссылается указатель tail) с разделяющим значением. Пока хвостовое значение
остается большим, чем разделяющее, мы будем двигать tail влево; другими слова-
22
Глава 1
ми, мы будем его декрементировать. Результат можно видеть на рис. 1.3Ь, где
tail указывает на элемент 2. Затем мы выполняем аналогичную (зеркальную)
процедуру с указателем head. Мы двигаем head вправо (инкрементируем его) до
тех пор, пока он не будет указывать на элемент, больший или равный
разделяющему значению. В нашем примере это произойдет немедленно, так как сам
элемент 0 и является разделяющим. Если head находится все еще левее, чем tail,
мы обмениваем значения, на которые они ссылаются. Результат показан на
рис. 1.3с. Поскольку указатели еще не пересеклись, мы продолжаем двигать их
навстречу друг другу, как и прежде. Конечное состояние массива показано на
рис. 1.3d. Два указателя встретились, и разделяющее значение занимает свое
окончательное положение. Все значения слева меньше, а справа от него —
больше. Теперь, когда разбиение исходного массива произведено, быстрая
сортировка рекурсивно вызывает саму себя для левого и правого подмассивов.
Рис. 1.3.
Разбиение массива
при быстрой сортировке
(разделяющее значение
равно 3)
а:
Ь:
с:
d:
3
Т
Head
0
3
t
Head
0
1
t
Head
0
1
2
1
2
1
2
1
2
1
2
1
t
Tail
2
3
t
Tail
2
3
t
Head
t
Tail
5
3
5
3
5
3
5
4 ч
T
Tail
4
Сдвиг указателей
J
4 *-<^
4
\
Обмен значений
J
4 4--/
4
\
Сдвиг указателей
_ J
4 t-*'
Код
Вот файл qs_basic.cpp:
[#include "sort.h"
i// Файл qs_basic.cpp реализует шаблон функции quick_sort( ),
;// которая сортирует элементы своего входного массива
[// в восходящем порядке. Тип Т должен поддерживать
[// operator=( ) и operator<( ). Для инициализаций может
[// потребоваться копирование. Если требуется печать,
// необходима operator«( }. Аргумент array содержит подлежащие
// сортировке элементы, size является числом элементов этого
// массива. Сортировка производится "по месту." Повторяющиеся
!// элементы *не* допускаются.
// Простой вариант get_pe( ), просто возвращает
[// первый элемент массива.
[template <class T>
[inline int get_pe(T array[], int lower, int upper)
Сортировка
>
г
return lower;
}
// Простой вариант partition( ), не может обрабатывать повторяющиеся
// значения. Разбивает подмассив, ограниченный start и end,
// на два меньших подмассива, таких, что все элементы левого
,// подмассива меньше разделяющего элемента, а все элементы
// правого подмассива больше. Таким образом, разделяющий элемент
// оказывается на своем окончательно» месте в сортированном
,// массиве. Возвращает указатель, ссылающийся на
// разделяющий элемент.
'template <class T>
inline int partition(T array[], int start, int end, int pe_index)
4 // int k = -1; // Только для отладки.
// Инициализация head и tail индексами
// первого и последнего элементов массива.
3 Т ре = array[pe_index];
j int head - start, tail = end;
a while ( head < tail ) {
■ // Декрементировать tail, пока не достигнем элемента,
J // меньшего или равного разделяющему.
\ while ( (array[tail] > ре) ) —tail;
» , assert(array[tail] <= ре);
E
a
f // Инкрементировать head, пока не достигнем элемента,
I // большего или равного разделяющему.
while ( (array[head] < ре) ) ++head;
assert(array[head] >= pe);
// Обменять head и tail, если только они уже не скрестились,
if ( head < tail ) swap(array, head, tail);
i
3 // Только для отладки.
] // for (k=start; k<=head; ++k) (assert(array[k] <= pe);}
// for (k=end; k>=tail; —k) (assert(array[k] >=pe);}
}
assert(head == tail);
assert(array[head] = pe);
return tail;
// Шаблон функции qs_helper( ), которая управляет ходом
_ // рекурсии. После получения разделяющего элемента
24
»
.J
// ока разбивает массив на два подмассива,
// вызывая partition( ). Затем она рекурсивно вызывает
// саму себя для сортировки каждого из этих подмассивов.
template <class T>
inline void qs_helper(T array[], int head, int tail)
{
int diff - tail - head;
// Бессмысленно сортировать один элемент,
if ( diff < 1 ) return;
// Специальный случай 2-элементного массива.
if (diff — 1) {
if (array[head] > array[tail]) (
swap(array, head, tail);
return;
}
}
int pe_index = get_pe(array, head, tail);
int mid = partition(array, head, tail, pe_index);
assert( (mid >= head) && (mid <= tail) );
// Элемент с индексом mid содержит теперь разделяющее значение
// и занимает правильное положение. Сортировать левый
// и правый подмассивы.
qs_helper(array, head, mid-1);
qs helper(array, mid+1, tail);
// Интерфейс quick_sort(T array[], int size) предусмотрен
// для удобства вызывающей программы. Процедура переводит
. // параметр size в форму, более подходящую для
Н qs_helper(T array[], int head, int tail), рекурсивной
// функции, делающей всю действительную работу.
template <class T>
inline void quicksort (T array [] , int size)
{
int head = 0, tail = size-1;
qs_helper(array, head, tail);
>
int main( )
{
int array_l[] = (7, 3, 8, 2, 1, 5, 4);
print(array_l, 7);
quick_sort(array_l, 7);
Сортировка
25
print(array_l, 7);
cout « endl;
int array_2[] = {7, 3, 8, 2, 1, 5, 4, 9, 75, -5};
print{array_2, 10);
quick__sort (array_2, 10) ;
print(array_2, 10);
cout « endl;
int array_3[] - (1, 2, 3};
print(array_3, 3);
quick_sort(array_3, 3);
print{array_3, 3);
cout « endl;
int array_4[] = {3, 2, lb-
print (array_4, 3);
< quick_sort(array_4, 3);
l print(array_4, 3);
J cout « endl;
( I#if 0
'' ' // Простейший вариант быстрой сортировки не обрабатывает эти случаи.
/ ' int array_5[] = {3, 2, 1, 3>;
print(array_5, 4);
quick_sort(array_5, 4);
Mill print (array_5, 4);
. cout « endl;
int array_6[] = {3, 3, 3};
print(array_6, 3);
1 quick_sort(array_6/ 3) ;
print(array_6, 3);
cout « endl;
#endif
return 0;
U>
Ниже показаны результаты, выдаваемые qsjbasic.cpp. Они совпадают с
результатами пузырьковой сортировки, за исключением двух последних
случаев, которые не тестируются, поскольку базовый вариант быстрой сортировки
не допускает повторяющихся значений.
7 3 8 2 15 4
12 3 4 5 7 8
73821549 75 -5
-5 12345789 75
26
Глава 1
12 3
12 3
3 2 1
12 3
1 ПРИМЕЧАНИЯ
Функция quick_sort(T*, int) переводит свой аргумент size в значения для
нижней и верхней границ массива и передает их функции qs_helper(T*, int,
int), которая и выполняет всю действительную работу. Последняя, в свою
очередь, вызывает функции get_pe(T*, int, int) и partition(T*, int, int, int).
Данный вариант get_pe(T*, int, int) тривиально прост — функция просто
возвращает свой второй аргумент, индекс самого левого элемента массива.
Зачем такая элементарная операция удостоена вызова в качестве функции? Мы
сделали это, чтобы подчеркнуть, что стратегия выбора разделяющего элемента
может оказать решающее влияние на эффективность в наиболее
неблагоприятном случае. Выбор разделяющего элемента — важный «кирпичик» быстрой
сортировки, и такое его отделение облегчает экспериментирование и
настройку. То же самое можно сказать и о процедуре разбиения, которая является
настоящим ядром алгоритма. Примеры будут приведены в следующем разделе.
I ЗАМЕЧАНИЕ ПРОГРАММИСТА
Наиболее естественным подходом при реализации быстрой
сортировки является рекурсия, используемая и в наших примерах. Простой и
эффективный способ снижения издержек рекурсии — это применение
элементарных методов к массивам, меньшим некоторого порогового
размера. Заметьте, что наша реализация qs_helper(T*, int, int)
обрабатывает одно- и двухэлементные массивы в качестве особых случаев.
Улучшенная быстрая сортировка
Эффективность быстрой сортировки в наиболее неблагоприятном случае,
грубо говоря, не лучше, чем у более простых методов. На самом деле, она дает
очень плохие результаты, будучи применена к уже сортированному массиву.
Почему это так? Принцип «разделяй и властвуй» подразумевает, что задача на
каждом шаге алгоритма разбивается на две примерно равные части, и так оно
приблизительно и бывает в среднем. Но если применить нашу простейшую
методику выбора разделяющего элемента — выбор крайнего левого элемента — к
уже сортированному массиву, то, как совершенно ясно, задача не делится на
две равные части. Фактически мы остаемся с новой задачей почти того же
объема, что и исходная.
Для получения наилучших результатов нужно было бы взять в качестве
разделяющего элемента медиану массива. Но поиск медианы для каждого под-
массива обойдется недешево. Одним из возможных способов примерной оцен-
Сортировка 27_
ки будет нахождение медианы для первого, последнего и среднего
элементов; именно так и поступает наш усовершенствованный вариант функции
get_pe(T*, int, int). Затраты на такую операцию постоянны и выражаются в не
более чем трех сравнениях, и при этом шансы встретиться с наихудшим
случаем сильно падают. Другим возможным подходом будет выбор в качестве
разделяющего случайного элемента массива.
Другое изменение в этом усовершенствованном варианте сортировки
касается partition(T*, int, int, int); в эту функцию введена возможность обработки
повторяющихся значений. Чтобы понять, почему элементарная версия
быстрой сортировки их не допускает, рассмотрите массив [3, 2, 3]. Разделяющее
значение равно 3, и процедура разбиения сразу же входит в бесконечный
цикл. Усовершенствованная быстрая сортировка обрабатывает массив
правильно.
Код
Вот файл qs.cpp. Программа выдает те же результаты, что и bs.cpp.
«Ml
#include "sort.h"
// Файл qs.cpp реализует шаблон функции quick_sort( ),
// которая сортирует элементы своего входного массива
//в восходящем порядке. Тип Т должен поддерживать
// operator=( ) и operator<( ). Для инициализаций может
// потребоваться копирование. Если требуется печать,
// необходима operator«{ ) . Аргумент array содержит подлежащие
// сортировке элементы, size является числом элементов этого
// массива. Сортировка производится "по месту." Повторяющиеся
// элементы допускаются.
// Этот вариант get_pe( ) пытается избежать наихудшего случая,
// выбирая в качестве разделяющего элемента медиану
// левого, серединного и правого значений массива.
1 template <class T>
(inline int get__pe(T array[], int lower, int upper)
(
S
L
// Как вы помните, qs_helper не вызывает get__pe ( ) для
// массивов иэ одного и двух элементов. Если вы модифицируете
// qs_helper так, что она будет вести себя по-другому,
// можно обрабатывать специальные случаи здесь, например:
//if { (upper - lower) < 2 ) return lower;
assert( (upper - lower) >= 2);
int mid = (lower + upper) / 2;
// Простая последовательность сравнений, проверяющая три
// возможных случая. Требуется не более трех сравнений.
Я if ( (array[lower] <= array[mid]) ) {
28
Глава 1
if (array[mid] <= array[upper]) return mid;
else if ( array[upper] <=; array [lower] ) return lower;
else return upper;
}
else {
assert(array[lower] > array[mid]);
if (array[lower] <= array[upper]) return lower;
else if ( array[upper] <- array[mid] ) return mid;
else return upper;
}
)
; // Этот вариант partition обрабатывает повторяющиеся значения.
// Разбивает подмассив, ограниченный start и end, на два
j// меньших подмассива, таких, что все элементы левого
■// подмассива меньше, а все элементы правого подмассива
'// больше разделяющего значения. Таким образом,
-*// сам разделяющий элемент оказывается на своем окончательном
!// месте в сортированном массиве. Возвращает индекс
// позиции разделяющего элемента.
template <class T>
■ inline int partition(T array[], int start, int end, int pe index)
■ <
// int k = -1; // Только для отладки.
I:
:
// Сохранить разделяющее значение и поместить
// разделяющий элемент в крайнюю правую позицию.
I pe ~ array[pe_index];
swap(array, pe_index, end);
J // Инициализировать head и tail так, чтобы они указывали
// на начальный и предпоследний элементы массива,
int head = start, tail = end-1;
while ( true ) {
// Инкрементировать head, пока не достигнем элемента,
// большего или равного разделяющему,
while ( (array[head] < pe) ) ++head;
assert(array[head] >= pe);
// Декрементировать tail, пока не достигнем элемента,
// меньшего или равного разделяющему.
while ( (array[tail] > pe) £6 (tail > start) ) —tail;
assert(array[tail] <= pe);
// Обменять head и tail, если только они уже не скрестились.
// Принудительный инкремент/декремент индексов head/tail
// после обмена предотвращает бесконечный цикл в случае, если
// оба они ссылаются на разделяющее значение,
if ( head >= tail ) break;
ii swap (array, head++, tail--);
Сортировка
29
II Только для отладки.
// for (k=start; k<-head; ++k) {assert(array[k] <= pe);}
// for (k=end; k>=tail; —k) {assert(array[k] >=pe);}
}
swap(array, head, end) ;
assert(array[head] == pe);
return head;
У
ill Шаблон функции qs_helper( ), управляющей ходом
// рекурсии. После получения разделяющего элемента
III разбивает массив на два подмассива,
// вызывая partition( ). Затем она рекурсивно вызывает
!// саму себя для сортировки каждого из этих подмассивов.
template <class T>
[inline void qs_helper(T array[], int head, int tail)
{
int diff = tail - head;
// Нет смысла сортировать один элемент.
if ( diff < 1 ) return;
// Специальный случай 2-элементного массива,
if (diff == 1) {
if (array[head] > array[tail]) swap(array, head, tail);
return;
}
int pe_index = get_pe(array, head, tail);
int mid = partition(array, head, tail, pe_index);
assert( (mid >= head) && (mid <= tail) );
// Элемент с индексом mid содержит теперь разделяющее значение
// и занимает правильную позицию. Сортировать левый и правый
// подмассивы.
qs_helper(array, head, mid-1);
qs_helper(array, mid+1, tail);
}
// Интерфейс quick_sort(T array[], int size) предусмотрен
III для удобства вызывающей программы. Функция переводит
[// параметр size в форму, более подходящую для
[// qs__helper(T array [] , int head, int tail), рекурсивной
[// функции, выполняющей всю действительную работу.
template <class T>
inline void quick_sort(T array[], int size)
{
int head = 0, tail = size-1;
30
Глава 1
Г qs_helper(array, head, tail);
I
int main( )
{
int array_l[] « {7, 3, 8, 2, 1, 5, 4};
j print(array_l, 7);
!■ J quick_sort (array_l, 7) ;
print(array_l, 7) ;
cout « endl;
int array_2[] = {7, 3, 8, 2, 1, 5, 4, 9, 75, -5} ;
print(array_2, 10);
quick_sort(array_2, 10) ;
print(array_2, 10);
" cout « endl;
3 int array_3[] = {1, 2, 3};
j print(array_3, 3);
I quick_sort(array_3, 3);
print(array_3, 3);
J cout « endl;
1 int array_4[] - {3, 2, 1};
^ print(array_4, 3) ;
fc quick_sort(array_4, 3);
print(array_4, 3) ;
cout « endl;
*
. int array_5[] = {3, 2, 1, 3};
, ■ print(array_5, 4);
^ quick_sort(array_5, 4) ;
■; print (array_5, 4) ;
cout « endl;
1 int array_6[] = (3, 3, 3);
Лщ print(array_6, 3) ;
\ quick_sort(array_6, 3) ;
J. print(array_6, 3) ;
' cout « endl;
return 0;
| X\PV
ПРИМЕЧАНИЯ
Как уже отмечалось, по своей структуре улучшенная быстрая сортировка
не отличается от базового варианта. Изменена лишь реализация процедур
get_pe(T*, int, int) и partition(T*, int, int, int). Поскольку функция,
вызывающая get_pe(T*, int, int), рассматривает массивы размером в 1 и 2 элемента в
качестве специальных случаев, get_pe(T*, int, int) предполагает, что передан-
Сортировка
31
ный ей массив имеет по крайней мере три элемента. Это подтверждается
начальным оператором assert.
Средний индекс массива, mid, вычисляется как среднее аргументов upper и
lower. Для массивов с четным числом элементов результат деления нацело
дает меньшее из двух возможных значений. Стандартным методом
определения медианы массива является его сортировка и затем выбор значения
серединного элемента. Поскольку у нас всего три элемента, целесообразно
выполнять вместо этого фиксированную последовательность сравнений, исключая
тем самым расходы на ненужные перестановки. Функция get_pe(T*, int, int)
производит два или три сравнения в зависимости от входных значений.
Возвращаемое значение представляет собой индекс массива, соответствующий
разделяющему значению. Мы возвращаем индекс, а не само значение, так как
он потребуется partition(T*, int, int, int) для перемещения значения
разделяющего элемента в некоторое особое место. Важно, чтобы время выполнения
get_pe(T*, int, int) не увеличивалось бы с ростом размера массива.
Модификации в partition(T*, int, int, int) невелики, однако они
существенным образом меняют ее поведение. Первым делом эта процедура обменивает
содержимое разделяющего и конечного элементов массива; разделяющее
значение будет сохраняться в конечном элементе на протяжении всего процесса
разбиения и займет свое окончательное (правильное) положение только в
самом его конце. Главный цикл while сделан бесконечным, т. е. условие
завершения проверяется не в заголовке, а в теле цикла.
Циклы, передвигающие head и tail, остались по существу теми же самыми.
Цикл, декрементирующий tail, снабжен дополнительным условием,
обеспечивающим невыход указателя за начало массива. Затем следует проверка
условия завершения главного цикла. Цикл завершается, если head и tail
встретились или скрестились. Следующий оператор обменивает элементы, на которые
ссылаются head и tail, и принудительно инкрементирует/декрементирует их.
Тем самым гарантируется, что цикл завершится, даже если оба указателя
ссылаются на элементы, значения которых равны разделяющему.
На каждом проходе главного цикла гарантируется, что ни одно значение
слева от головного указателя не будет больше, а справа от хвостового —
меньше разделяющего значения. После того, как указатели встретились или
скрестились, мы обмениваем крайний правый элемент с головным. Это не
нарушает вышеупомянутого условия и, кроме того, гарантирует, что левый и правый
подмассивы будут отделены друг от друга по меньшей мере одним
разделяющим элементом. Такие изменения в partition(T*, int, int, int) позволяют ей
корректно обрабатывать повторяющиеся значения.
Сортировка слиянием
Хотя средняя производительность быстрой сортировки производит большое
впечатление, мы видели, что в наихудшем случае дела обстоят неважно. Мы
увидели также, как можно значительно сократить вероятность встречи с
неблагоприятной ситуацией.
Сортировка слиянием применяет похожий подход «разделяй и властвуй» и
в среднем столь же эффективна, но в ней не возникает проблемы наихудшего
случая. Тем не менее, и сортировка слиянием не безупречна. В то время как
32
Глава 1
прочие программы этой главы сортируют массив <tno месту», сортировка
слиянием требует дополнительного массива того же размера для временного
хранения данных. Если памяти у вас в избытке и необходимо защититься от
превратностей, связанных с наихудшим случаем, разумным решением будет
сортировка слиянием.
В отличие от быстрой сортировки, схема разбиения при сортировке
слиянием очень проста. Она просто разрезает массив на две равные части, образуя два
подмассива. Не нужны никакие разделяющие элементы и тонкие механизмы
скрещивания указателей.
Затем сортировка слиянием рекурсивно вызывает себя для каждого из двух
подмассивов. Конечным шагом является слияние двух сортированных подмас-
сивов в один сортированный массив. В этом также нет ничего сложного, но
здесь требуется временный массив для результата. Заметьте, что вся реальная
работа делается при разматывании стека вызовов.
Код
Вот файл ms.cpp. Программа выводит то же самое, что и bs.cpp.
ri
I ]#include "sort.h"
k
l|
w Jf Файл ms.cpp реализует шаблон функции merge_sort( ) ,
V/ сортирующей элементы своего входного массива в
» л// восходящем порядке. Тип Т должен поддерживать
*// operator—( ) and operator<( ). Для инициализации может
'// потребоваться копирование. Если требуется печать,
1 // необходима operator«( ) . Основная процедура поддерживается
г ,// шаблонами ms_helper( ) и merge ( ) . Вся функции объявлены как
. ,// inline, чтобы при желании можно было размещать их в заголовках.
■ I// Допускаются повторяющиеся элементы.
1
// Merge( ) рассматривает array как объединение двух подмассивов,
' •// один из которых соответствует индексам от start до mid,
1 '//а другой элементам с индексами от mid+1 до end. Предполагается,
// что оба эти подмассива сортированы, и функция объединяет их в
// массив temp__array. Затем temp_array копируется обратно в array.
//По завершении функции массив сортирован в восходящем порядке.
4 jtemplate <class T>
' ч inline void merge(T array[] , Т temp_array[] ,
■ 4 int start, int mid, int end)
* {
■ i int i_temp = 0, i_lower = start, i_upper = mid+1;
I II Цикл продолжается, пока ни один из подмассивов
//не пуст.
I i while ( (i_lower <= mid) && (i_upper <~ end) ) {
• ] if (array[i_lower] < array[i_upper])
• , temp_array [i_temp++] = array[i_lower++];
> *i else
Сортировка ЗЦ
^ temp_array[i_temp++] = array [i__upper++] ;
. )
1
.. \ // Если в каком-либо из подмассивов остались элементы,
// они просто копируются в temp_array.
if (i_lower <= mid J (
assert(i__upper > end);
for (; i_lower <= mid;
temp_array [ i__temp++] = array[i_lower++]) ;
}
else {
assert(i_lower > mid);
assert(i_upper <= end);
■*! for {; i_upper <= end;
temp_array[i_temp++] - array[i_upper++]) ;
}
t
// Размер массива равен end - start + 1.
assert{i_temp — end - start + 1);
// Теперь копировать temp_array обратно в array.
int i__array = start;
for (i_temp — 0; i_array <= end;
[ j array[i_array++] = temp_array[i_temp++])
1»
// Ms_helper( ) - рекурсивная функция, управляющая стратегией
// сортировки. Она разбивает исходный массив, просто деля
// его пополам, и вызывает себя рекурсивно для каждой
V/ из половин, а затем объединяет сортированные поднассивы
i// в один массив. Вся работа по сортировке делается merge( )
// при разматывании стека вызовов.
Jtemplate <class T>
1inline void ms_helper(T array[], T temp_array[],
int head, int tail)
!<
r ] // Бессмысленно сортировать один элемент.
L J if (head == tail) return;
| . assert(tail > head);
// Найти среднюю точку массива.
int mid = (head + tail) / 2;
assert( (mid >= head) £& mid <- tail);
// Рекурсивно сортировать поднассивы.
ms_helper(array, temp_array, head, mid);
ms_helper(array, temp_array, mid+1, tail);
• ■ // Объединить результаты.
J ( merge(array, temp_array, head, mid, tail);
2 Зак.1208
34
Глава 1
;// Интерфейс merge_sort(T array[], int size) предусмотрен
;// для удобства вызывающей программы. Функция переводит
// параметр size в форму, более подходящую для
// ms_helper(T array[], int head, int tail), рекурсивной
А// функции, выполняющей действительную работу. Чтобы не
1 // выделять и не освобождать временные массивы при каждом
\// вызове merge( ), это делается здесь, всего один раз.
Ц
"i template <class T>
* inline void merge_aort(T array[], int size)
' -M
int head = 0, tail = size-1;
\ T *temp_array = new T[size];
ms_helper(array, temp_array, head, tail);
delete[] temp_array;
>
// Следующая программа инициализирует несколько массивов,
// печатает их, сортирует и печатает их снова.
// Допускаются повторяющиеся значения.
*
int main( )
К
int array_l[] - {7, 3, 8, 2, 1, 5, 4);
print(array_l, 7);
J merge_sort(array_l, 7);
print(array_l, 7);
cout « endl;
fe '
int array_2[] = (7, 3, 8, 2, 1, 5, 4, 9, 75, -5};
print(array_2, 10);
merge_sort(array_2, 10) ;
- I print(array_2, 10);
^ cout « endl;
n ■
, 4 int array_3[] ■ {1, 2, 3};
! J print(array_3, 3);
J merge_sort(array_3, 3);
print(array_3, 3);
cout « endl;
f.
i
int array_4[] = {3, 2, 1};
print(array_4, 3);
merge_sort(array_4, 3);
print(array_4, 3);
cout « endl;
; int array_5[] = {3, 2, 1, 3);
« print(array_5, 4);
.t merge_sort(array_5, 4);
print(array_5, 4);
cout « endl;
.1
Сортировка
35
int array_6 [] = {3, 3, 3};
print(array_6, 3);
merge_sort (array__6, 3) ;
print(array_6, 3) ;
cout « endl;
return 0;
}
| ПРИМЕЧАНИЯ
Общая структура этого кода схожа со структурой быстрой сортировки.
Функция merge_sort(T*, int) вызывает ms_helper(T*, T*, int, int), процедуру,
управляющую рекурсией и слиянием. Помимо того, что merge_sort(T*f int)
переводит свои аргументы в более удобную форму, она еще выделяет
временный массив, который потребуется для работы процедуры merge(T*, T*, int,
int, int). Выделение памяти делается здесь, потому что merge_sort(T*, int)
знает размер исходного массива и может с самого начала выделить
единственный временный массив. Передача этого массива другим функциям в качестве
аргумента выглядит несколько неуклюже, однако это предпочтительнее, чем
альтернативное его выделение и освобождение в merge(T*, T*, int, int, int) при
каждом вызове этой процедуры.
Настоящая «рабочая лошадка* этого алгоритма — это merge(T*, T*, int,
int, int). Она рассматривает свой первый аргумент array как два подмассива,
ограниченных индексами start, mid и end. Функция предполагает, что эти
подмассивы уже отсортированы, и объединяет их в temp_array, проходя по
ним параллельно и выбирая на каждом шаге меньший из двух элементов,
который и копируется в temp_array. Это происходит в начальном цикле while.
Обратите внимание, что для каждого из трех участвующих в процессе
массивов отслеживается отдельный индекс. Индекс i_lower указывает на
текущий элемент нижнего, т. е. левого подмассива. Индекс i_upper играет
аналогичную роль для верхнего (правого) подмассива. Наконец, i_temp отслеживает
текущий элемент массива temp_array, который служит для временного
хранения результата в процессе слияния. Каждый из этих индексов проходит по
своему массиву в восходящем порядке. i__lower (i_upper) получает
приращение только в том случае, если для копирования в temp_array выбран элемент
левого (правого) подмассива. i_temp инкрементируется безотносительно к
тому, какой из подмассивов выбран.
Когда все элементы одного из подмассивов исчерпаны, не имеет смысла
делать дальнейшие сравнения. Оставшиеся элементы непустого подмассива
просто копируются в оставшиеся позиции временного массива. Теперь temp_ar-
гау содержит все сортированные элементы. Завершающим шагом является
копирование его обратно в array.
#d ЬУ*
<s,
лО>
у
ГЛАВА
36
Глава 2
Связанные списки входят в число самых основных и важнейших структур
данных. Большинство программ так или иначе применяют списки. Так
или иначе все с ними знакомы, однако им присущи некоторые тонкости,
которые делают связанные списки более интересным предметом, чем может
показаться на первый взгляд. В этой главе мы рассмотрим некоторые детали их
реализации и приведем в порядке иллюстрации несколько примеров.
В настоящее время все большую популярность приобретает термин
контейнер. Контейнер — это структура данных, главной задачей которой является
хранение других объектов, причем способ их хранения четко определен.
Примером контейнера может служить массив, структура, встроенная в язык.
Списки представляют собой пример контейнерных классов. В этой главе мы
рассмотрим важнейшие аспекты реализации контейнеров, опираясь на списки
как базовую модель.
Имейте в виду, что существует великое множество способов реализации
списков. Код, представленный здесь, предназначен только для того, чтобы
показать некоторые возможные пути и предложить другие альтернативы. Он не
более чем отправная точка и не претендует на то, чтобы быть «лучшим»
способом реализации в любом разумном смысле этого слова. Наверняка вы сможете
улучшить наш код и приспособить его к вашим конкретным обстоятельствам.
Но в любом случае не стоит изобретать велосипеды.
Введение в связанные списки
Связанные списки — это сравнительно простые структуры данных, часто
используемые в качестве «строительных блоков» для стеков, очередей и
других более сложных объектов. Вряд ли найдется много программистов (если
такие вообще есть), которые в своей практике не сталкивались бы со списками в
той или иной форме. Говоря абстрактно, список представляет собой, подобно
массиву, последовательность объектов или значений. Однако, в отличие от
массива, список не имеет фиксированного размера — он может расти или
сокращаться про мере необходимости. Эта его особенность становится
существенной, если число объектов в списке неизвестно до времени выполнения.
Рис. 2.1 показывает, как выглядит в памяти односвязный список. Каждый
элемент такого списка может быть представлен в виде двух составляющих:
реальных данных, которые вы хотите запомнить, и указателя на следующий
элемент списка, если таковой имеется. Элемент списка динамически
размещается в памяти только при необходимости и привязывается к остальной части
списка посредством указателей. Если элемент списка больше не нужен, его
память освобождается. Правда, за такую гибкость приходится платить более
сложным способом доступа к элементам, чем в массивах, и некоторыми
дополнительными расходами памяти. Но часто эти издержки вполне окупаются.
Рис. 2.1.
Расположение
в памяти односвязного
списка, содержащего
числа 3, 4 и 2
3 ». >. 4 • ► 2 • ► У^\/
А А
Head
Tail
(запредельный указатель)
Связанные списки
39
Несмотря на простоту структуры связанного списка, при его конкретной
реализации нужно принять немало решений относительно ряда вопросов. Вот
некоторые из них:
♦ Объекты какого типа будут содержаться в списке?
♦ Как определить, что список пуст?
♦ Как должны обрабатываться ошибки вроде попыток удалить объект из
пустого списка?
♦ Какие операции над списком должны поддерживаться?
♦ Как часто придется удалять или вставлять элементы в середину списка?
♦ Нужно ли будет проходить по списку в обоих направлениях, или только
в одном?
♦ Нужно ли знать число элементов в списке в любой момент времени? И
если это так, нужно ли нам следить за этим непрерывно или просто
подсчитывать элементы списка при необходимости?
В этой главе демонстрируется ряд примеров связанных списков, а также
предложения относительно того, как их можно усовершенствовать. Первые
примеры весьма просты, а в каждом следующем вводится одно новое свойство,
устраняющую ту или иную проблему или ограничение, присущее
предшественнику.
Если говорить о сложности или совершенстве, эти примеры гораздо
примитивнее связанных списков, реализованных в библиотеке стандартных
шаблонов C++ (STL). Большинству программистов в настоящее время легко
доступны мощные возможности STL. Примеры этой главы предлагают
альтернативы, так сказать, более легкой «весовой категории».
Реализация списков на С: Заметьте, что связанные списки часто реализуют
на языке С. Чтобы написать С-версии первых двух примеров, выведите
функции-элементы за пределы классов и замените ключевое слово class на struct.
Нужно такэке убрать другие ключевые слова, специфические для C++, такие,
как private. Мы использовали C++, поскольку этот язык позволяет написать
списки как абстрактные типы данных, — т. е. предусмотреть необходимый
интерфейс, не раскрывая полностью лежащей в его основе реализации.
Структуры данных такого рода с большой долей вероятности будут более надежны.
Шаблоны списков: Наконец, C++ позволяет писать шаблоны списков, так
что один раз написанный код может применяться для самых разнообразных
типов данных, а не только для того, который мы имели в виду при
проектирования списка.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Перед тем, как вы приступите к работе, имейте в виду, что
приводимые примеры широко используют закрытые вложенные классы,
дружественные классы и т. п. Для этого есть весьма весомая причина:
пользователю предоставляется достаточный контроль, но
пользовательскому коду не разрешено слишком полагаться на особенности
реализации класса. Однако такой подход содержит потенциальную проблему,
так как существуют различия в том, как разные компиляторы C++
40
Глава 2
интерпретируют правила доступа. В результате ваш конкретный
компилятор может не воспринимать данный код в том виде, как он
написан. Если такое происходит, вы можете обойти эту проблему,
выборочно изменив некоторые из закрытых объявлений на открытые. Но
все-таки постарайтесь, если возможно, избегать этого.
Односвязные списки
Как явствует из рис. 2.1, элементы односвязного списка имеют всего один
указатель — на следующий элемент. Это означает, что по списку можно
проходить только в одном направлении, начиная от «головы» и двигаясь по
указателям в сторону «хвоста». Поиск элемента с некоторым конкретным значением
требует времени, пропорционального длине списка, так как нужно проверить
каждый элемент, пока не будет найден нужный. Операции вроде удаления
имеют ограниченные возможности, как вы увидите из примеров. С другой
стороны, реализация односвязного списка несложна и потому весьма эффективна
при вставке или удалении элементов из его головы. Такие структуры данных
идеальны для реализации стеков.
Все списки в этой главе имеют дополнительный элемент, находящийся
непосредственно за действительным концом списка. Этот «запредельный» элемент
имитирует структуру контейнеров STL, которые всегда определяют подобные
элементы. Наличие такого элемента, на который постоянно ссылается указатель
«хвоста», делает очень простой проверку того, является ли список пустым.
Кроме того, он делает более быстрыми некоторые операции в хвосте списка.
Простой односвязный список
Первый пример, который будет называться slist_l, самый простой. Класс
Single_list реализует список целых чисел. Заметьте, что возвращаемые рядом
функций-элементов значения являются целыми, а не узлами, их содержащими.
Код
Код организован в виде двух файлов: slist_l.h — это сам список, а
slist__l.cpp демонстрирует пример его использования.
// Файл slist_l.h
■// Простой не-шаблонный односвязный список целых чисел.
#include <cassert>
class Single_list {
private:
class Single_node {
// Назначение Single_node - поддержка класса Single_list.
// Поэтому все его элементы закрыты, а класс Single_list -
// дружественный.
Связанные списки
41
friend class Single_list;
i
> // Создать Single__node с осмысленным значением.
Single_node(int node__val) : val(node_val) { }
// Создать пустой Single_node.
Single_node( ) { }
i '
*Single_node( ) { }
Single_node *next; // Указывает на следующий Single__node.
t int val; // Данные.
};
Single__node *head; // Указывает на начало списка.
Single_jnode *tail; // Указывает на узел за концом списка.
к. I // Объявленные закрытыми и не определенные операции копирования
[ //и присваивания, что делает их недоступными.
. 1
I F Single_list & operator=(const Single_list &);
i Single_list(const Single_list &) ;
4
! |public:
» J
// Создать пустой список.
Single_list( ) {
// Пустой "запредельный" узел.
1 head = tail = new Single_node;
i tail->next =0;
}
■ i
1
// Создать список, содержащий один элемент.
Single_list(int node_val) {
// Пустой "запредельный" узел.
head = tail = new Single_node;
tail->next = 0;
add_front(node_val);
}
// Пройти список от головы к хвосту, удаляя его элементы.
~Single_list( ) {
Single_node *node_to_delete — head;
for (Single_node *sn = head; sn != tail;) {
sn = sn->next;
delete node_to_delete;
node_to_delete = sn;
)
// assert( node_to_delete -= tail );
delete node to delete;
42
Глава 2
i
i
ь
1
i '
I
L
bool is_empty( ) const {return head == tail;}
// Ввести новый элемент в начало списка.
void add_front(int node_val) {
Single_node *node__to_add = new Single__node(node__val) ;
node_to_add->next = head;
head = node_to_add;
}
// Удалить головной элемент.
J // Заметьте, что remove_front( ) освобождает память, занятую
f // удаляемым элементом, поэтому нужно предусмотреть вариант
// вызова функции для пустого списка. В противном случае функция
// может попытаться удалить Single_node, который не должен
// удаляться.
int remove_front( ) {
if ( is_einpty( ) ) throw "tried to remove from an empty list";
i
j, T Single_node *node_to_remove — head;
int return_val = node_tc_remove->val;
head — node__to_remove->next;
delete node_to_remove;
return return val;
5 >
// Возвращает true, если список содержит node_val,
// и false в противном случае.
bool find(const int node_val) const {
for (Single_node *sn = head; sn != tail; sn = sn->next) {
if (sn->val == node_val) return true;
}
return false;
}
// Возвращает значение п-го элемента списка. Пользователь должен
// быть уверен, что element_num не меньше единицы, и не больше
// размера списка. Позволяет пользователю распечатать содержимое
// списка, хотя и не эффективно.
int get_nth(const int element_num) const {
if (element_num < 1) throw "get_nth argument less than one";
- * int count = 1;
for (Single_node *sn - head; sn != tail; sn = sn->next) {
if (count++ == element_num) return sn->val;
}
throw "element_num exceeds list size";
Связанные списки
43
II Возвращает размер списка, подсчитывая его элементы.
// Неэффективна, поскольку предполагается, что такая операция
// не будет частой (для тестирования), и поэтому лучше затратить
// здесь дополнительное время, чем терять его на модификацию
// переменной размера списка при каждом включении/удалении элемента.
int size{ ) const {
int count = 0;
for {Single_node *sn - head; sn \= tail; sn = sn->next)
++count;
return count;
}
>;
Вот файл slist_l.cpp:
// Файл slist_l.cpp
extern "C" printf(char *, ...);
ttinclude "slist_l.h"
!// Print_slist печатает все значения из Single__list.
[// Вызывает функции-элементыэ size( ) и get_nth(int)
// из Single__list. Такая реализация неэффективна, так как
// get_nth(int) проходит по всему списку вплоть до
// п-го элемента при каждом вызове. Это исправляется
// в последующих примерах.
//
[void print_sXist(const Single_list & si) {
int list_size = si.size( );
if (list_size -= 0)
printf("empty list\n");
else {
int elt - 1;
while ( elt <= list_size ) {
printf("%d ", sl.get_nth(elt++) );
printf("\nM);
}
}
Single_list my_list; // Создать пустой Single_list.
[int xnain( )
(
44
Глава 2
fc-.i // Добавить несколько элементов в начало.
>" -. for ( int val - 0; val < 5; ++val ) {
i i my__list.add_front(val) ;
- -Я print_slist(my_list);
j i
i ] // Теперь удалить их по одному. Чтобы очистить список,
л // не требуется знать его длину,
while ( ! my_list.is_empty( ) ) {
■Т\ | print_slist(my_list) ;
l,'.j my_list.remove_front( ) ;
Г Ъ }
. jA return 0;
Программа slist_l выводит значения, показанные ниже. Строки
показывают текущее содержимое my_list по мере его роста и сокращения.
0
1 0
2 10
3 2 10
4 3 2 10
4 3 2 10
3 2 10
2 10
1 0
о
| ПР1/
ПРИМЕЧАНИЯ
Класс Single_list использует специализированные узлы для связки между
собой целых значений. Открытие внутренней структуры такого узла
предоставило бы пользователю более полный контроль (увеличив при этом сложность
работы с классом). Это также ограничило бы нас в плане изменений
реализации класса в будущем.
Для этого первого примера мы сделали выбор в пользу простоты и
надежности- Поэтому специальные узлы со связками выполнены в виде закрытого
вложенного класса Single_list::Single_node. Пользователям Single_list нет
нужды знать внутреннюю структуру и даже знать о самом существовании Sing-
le__list::Single_node, и, таким образом, они могут сосредоточиться на тех
целых значениях, которыми им нужно манипулировать. Другие закрытые
элементы Single_list (head и tail) указывают на узлы, представляющие голову и
хвост списка. Заметьте, что хвостовой элемент является на самом деле
элементом, следующим за конечным.
У Single_list два конструктора. Конструктор по умолчанию создает пустой
список, a Single_list(int) создает список с одним элементом. Деструктор
просто проходит по списку, удаляя каждый из его элементов.
Связанные списки 45^
Функции is_empty() и find(const int) возвращают значения типа bool.
Функция is_empty() возвращает true, если head и tail указывают на один и тот же
узел; другими словами, если в списке присутствует только «запредельный» узел.
Имя find в этом простейшем примере может вводить в заблуждение, потому
что функция не возвращает местоположение найденного узла, которое можно
было бы использовать в дальнейшем. Вместо этого она просто сообщает,
содержится ее параметр в некотором элементе списка. Функция find(int) проходит
по списку, пока не найдет узел с указанным значением (возвращается true)
или не будет достигнут конец списка (возвращается false). Позже в этой главе,
в шаблонном варианте класса Double_list будет показана функция поиска,
которая возвращает итератор и тем самым устраняет данный недостаток Sing-
le_list. Ту же методику можно применить и в Single_list.
Важнейшие действия выполняются функциями add_front(int) и remo-
ve_front(). Первая из них создает новый элемент и присоединяет его в начало
списка. Память под новый элемент выделяется динамически с помощью
операции new. Давайте разберем эту короткую функцию строка за строкой:
// Ввести новый элемент в начало списка.
void add_front(int node_yal) {
Single_node *node_to_add = new Single_node{node_val);
node__to_add->next = head;
head = node_to_add;
}
Первая строка показывает, что функция принимает целый параметр и
возвращает void, поскольку нет никакого полезного значения, которое можно
было бы возвратить. Однако приложение могло бы-использовать возвращаемое
такой функцией значение для сообщений об ошибках. Например, возможен
отказ операции new при нехватке динамической памяти. Следующая строка
выделяет память под Singlenode, который будет содержать новое целое
значение. Значение инициализируется параметром конструктора Single_no-
de(int), который появляется в выражении с new. Третья строчка
инициализирует указатель next нового узла, присваивая ему текущее значение head.
Наконец, в четвертой строке мы присваиваем head указатель на новый узел.
Подобная «хирургия указателей» может быть опасной. Реализация ее кода в виде
функции-элемента помогает предотвратить возникновение широкой
категории программных дефектов.
Функция remove_front() выполняет действие, противоположное add_front(int):
она удаляет первый элемент списка и освобождает занимаемую им память.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
В функции remove_front() мы должны предусмотреть обработку
ошибочных ситуаций — поскольку попытка удаления элемента из пустого
списка является довольно распространенной ошибкой, результаты
которой могут быть катастрофическими. В частности, мы должны
быть уверены, что не удаляем узел, который удалять нельзя; это
может нарушить структуру динамической памяти, что скажется
только впоследствии. Ошибки такого рода отлаживать очень трудно.
46
Глава 2
Single_list — это готовый стек, если понимать функции add_front(int) и ге-
move_front() как соответственно операции «push» и «pop*.
Файл slist_l.cpp демонстрирует, как применять класс Single_list в
работающей программе. Прежде всего заметьте, что пользователю нужно написать
функцию print_slist(). Отказ от реализации этой функции в самом классе
может быть оправдан, если она вызывается только от случая к случаю в целях
отладки. Тогда пользовательское определение ее (пусть неэффективное) будет
вполне адекватным. (Позже в этой главе мы реализуем функцию-элемент
print(), которая и более эффективна, и более удобна.) Главная программа
создает пустой список, заносит в него несколько элементов, а затем по одному
удаляет их, пока список не станет снова пустым. Функция print_list()
показывает результат каждого изменения.
Шаблон односвязного списка
Второй пример, slist_2, значительно расширяет возможности простого
связанного списка. Вот его главные отличия от первого примера:
♦ Пример slist_2 является шаблоном. Это означает, что он позволяет
создавать списки, содержащие объекты самых различных типов, а не
только целые числа. Чтобы проиллюстрировать этот момент, наш пример
кода показывает список значений типа float. Существуют некоторые
ограничения на тип содержащихся в списке объектов, но большинство
интересующих нас типов оказываются вполне приемлемыми.
♦ Пример slist_2 реализует то, что мы будем называть перечислителем
(enumerator). Каждый создаваемый нами список будет иметь «текущий»
элемент, на который мы сможем ссылаться или манипулировать им с
помощью предназначенных для этого функций-элементов класса. Тем
самым обеспечивается большая гибкость и эффективность списка, причем
пользователю не требуется доступ к внутреннему устройству класса.
♦ Поле next хвостового узла используется более осмысленно, поскольку
оно теперь указывает на последний элемент списка. Как вы помните, в
slist_l это поле всегда содержало нулевой указатель. Это простое
усовершенствование позволяет реализовать функцию-элемент add_rear(),
которая эффективно присоединяет элемент в конец списка. Возможность
вставлять элементы в конец и извлекать их из начала составляет
отличительную черту того, что называют очередью.
♦ Предусмотрена функция-элемент print().
I ЗАЛ
ЗАМЕЧАНИЕ ПРОГРАММИСТА
У нас все еще отсутствует возможность эффективного удаления
элемента из конца списка. Чтобы это было возможным, нам нужно иметь
указатель на предпоследний элемент, чтобы мы могли присвоить его
полю next значение tail. Одним из путей является переопределение
хвостового элемента таким образом, чтобы он содержал два указателя, а
не одно поле указателя и (неиспользуемое) поле данных. Но вы выбрали
Связанные списки
47
более простой подход. Двусвязные списки, описанные далее в этой
главе, имеют структуру, более подходящую для вставок и удалений
элементов в любой позиции.
Код
Код примера также содержится в двух файлах. Файл slist_2.h — это сам
список, a slist_2.cpp показывает пример его применения.
// Файл slist__2.h
!// Шаблон односвязного списка.
//
!// Главное усовершенствование slist_l: перечислитель.
//
// Модификации slist_l: add_rear(T) и print( ).
//
// Тип Т должен определять operator==( ) для поддержки find(T),
// а также operator«( ) для поддержки print ( ) .
//
[#include <cassert>
j#include <iostream>
using namespace std;
| template <class T>
class Single_list {
{private:
class Single_node {
4
I
l
friend class Single_list<T>;
// Создать Single__node с осмысленным значением.
Single_node (T node_yal) : val (node__val) { }
// Создать пустой Single_node.
Single_node( ) { }
~Single_node( ) { }
// Напечатать значение.
void print_val( ) const {cout « val « " ";}
Single_node *next; // Указывает на следующий Single_node.
T val; // Данные.
Л
£/~4 Single__node *head; // Указывает на начало списка.
,_ J Single_node *tail; // Указывает на запредельный узел.
i A Single_node *current; // Указывает на текущий узел.
// Объявленные закрытыми и не определенные операции копирования
// и присваивания, что делает их недоступными.
Single_list & operator^(const Single_list &);
Single_list(const Single_list &) ;
// Вспомогательная функция, вызьюаемая из Single_node(T),
// add_front(T) и add_rear(T).
void add_to_empty(T node_val) {
Single_node *node_to_add = new Single__node(node_yal) ;
node_to_add->next = head;
head = node__to_add;
tail->next = head;
current = head;
}
public:
// Создать пустой список.
Single_list( ) {
head — tail = new Single_node;
tail->next = 0;
current = tail;
}
// Создать список, содержащий один элемент.
Single_list(T node_val) {
head = tail = new Single_node;
tail->next = 0;
add_to_empty(node_val);
}
// Пройти по списку от начала к концу, удаляя каждый элемент.
~Single_list( ) {
Single_node *node_to_delete = head;
for (Single_node *sn = head; sn !— tail;) {
sn = sn->next;
delete node_to__delete ;
node_to_delete = sn;
}
// assert( node_to_delete == tail ) ;
delete node_to_delete;
}
bool is_empty( ) const {return head == tail;}
// Ввести новый элемент в начало списка.
// Отметьте особый случай пустого списка, когда
// tail->next равно 0 и потому tail->next->next
// не имеет смысла.
void add_front(T node_val) {
if ( is_empty( ) )
add_to_empty(node_val);
else {
Single_node *node_to_add = new Single_node (node val) ;
Связанные списки
49
X
node to add->next = head;
p head = node_to_add;
fi
'■I
U
M
}
I
l // Добавить новый элемент в конец списка.
// Отметьте специальный случай пустого списка, когда
// tail->next равно 0 и потому tail->next->next
| 1 //не имеет смысла.
| 1
[, void add_rear(T node_val) {
, if ( is_empty( ) )
I i add_to_empty(node_val);
4 else {
I i Single_node *node_to__add — new Single_node(node_val);
iJ node_to_add->next = tail;
' ^ tail->next->next = node_to_add;
•j tail->next = node_to_add;
! }
)
1// Удалить начальный элемент списка.
// Заметьте, что remove_front( ) освобождает занимаемую
// удаляемым элементом память, и потому должна правильно
// обрабатьтать пустые списки. В противном случае она может
// удалить Single_node, который удаляться не должен.
Т remove_front( ) (
if ( is_empty( ) ) throw "tried to remove from an empty list";
Single_node *node__to__remove = head;
r~ \ T return_val = node__to_remove->val;
head * node_to_remove->next;
. // Помимо конструкторов и вставки в пустой список,
j // это единственное место, где изменяется current (в качестве
ft // побочного эффекта) .
if (current == node to remove) current = node to remove->next;
delete node_to_remove;
return return val;
}
// Возвращает true, если список содержит node_val,
// и false в противном случае,
bool find(T node_val) const {
for ( Single_node *sn = head; sn != tail; sn = sn->next ) {
if { sn->val = node_val ) return true;
}
return false;
}
50
Глава 2
It Возвращает значение n-го элемента списка. Пользователь должен
// быть уверен, что element_num не меньше единицы и не больше,
// чем размер списка.
Т get_nth(const int element_num) const (
if ( element_num < 1 ) throw "get_nth argument less than one";
int count =1;
for ( Single_node *sn « head; sn != tail; sn = sn->next ) {
if ( count++ — element_num ) return sn->val;
}
throw "element num exceeds list size";
}
// Возвращает размер списка, подсчитывая его элементы.
// Неэффективна, поскольку предполагается, что такая операция
// не будет частой (для тестирования), и поэтому лучше затратить
// здесь дополнительное время, чем терять его на модификацию
// переменной размера списка при каждом включении/удалении элемента.
int size( ) const {
if ( is_empty( ) ) return 0;
int count * 0;
for ( Single_node *sn = head; sn != tail; sn - sn->next ) ++count;
return count;
}
// Печатает список более удобно и эффективно, чем это
// возможно с slist_l.
void print( ) const (
for ( Single_node *sn » head; sn != tail; sn = sn->next ) {
sn->print_val( );
}
cout « endl;
}
//
// Здесь начинаются функции, поддерживающие перечислитель.
//
void reset_current( ) { current = head; }
bool increment_current( ) {
if ( current ! = tail ) {
current = current->next;
return true;
}
1 return false;
Связанные списки
51
i
ft
»-
i I
i i>i *
// Вызов get_current в случае, если current -= tail не определено
// Пользователь должен проверить.
Т get_current( ) const { return current->val; }
bool current_is__tail( ) const { return current. = tail; }
// Заметьте, что current не изменяется,
bool insert_after_current( const T node_val ) {
if ( current_is_tail( ) ) return false;
Single_node *node_to_add — new Single_node(node_val) ;
node_to_add->next = current->next;
current->next = node to add;
Ниже показан файл slist_2.cpp.
// Файл slist_2.cpp
#include "slist_2.h"
// Следующая главная программа показывает простые примеры того,
[// как используется slist_2; параметр шаблона имеет тип float.
[// Программа выдает результат, показанный ниже. Каждая строка
// вывода показывает текущее содержимое my_list по мере того, как
[// он растет и сокращается. При наращивании списка мы вводим в него
[// по два элемента перед каждым вызовом print( ). При сокращении мы
[// печатаем содержимое после каждого удаления элемента. Список
// максимального размера печатается дважды - с помощью print( )
|// и с использованием функции перечислителя. ^
//
//0 0
// 0.1 0 0 0.1
// 0.2 0.1 0 0 0.1 0.2
// 0.3 0.2 0.1 0 0 0.1 0.2 0.3
// 0.4 0.3 0.2 0.1 0 0 0.1 0.2
// 0.4 0.3 0.2 0.1 0 0 0.1 0.2
// 0.3 0.2 0.1 0 0 0.1 0.2 0.3
// 0.2 0.1 0 0 0.1 0.2 0.3 0.4
// 0.1 0 0 0.1 0.2 0.3 0.4
//0 0 0.1 0.2 0.3 0.4
// 0 0.1 0.2 0.3 0.4
// 0.1 0.2 0.3 0.4
// 0.2 0.3 0.4
// 0.3 0.4
// 0.4
0.3 0.4
0.3 0.4
0.4
1
Single_list<float> my_list; // Создать пустой Single_list
int main()
{
52
Глава 2
?\
I.
i
г
• 1
// Ввести в список несколько значений.
float f = 0.0;
for { int val - 0; val < 5; ++val ) {
my_list.add_front(f);
my_list.add_rear(f);
my_list.print( );
f +« 0.1F;
}
// Распечатать список (с неплохой эффективностью)
// с помощью функций перечислителя.
my__list.reset_current( > ;
while ( ! my_list.current_is_tail( ) ) {
cout « my_list.get_current( ) « " ";
my_list.increment_current( ) ;
)
cout « endl;
// Теперь удалить элементы по одному. Чтобы очистить список,
// не требуется знать его длину.
■,.1
while ( ! my_list.is_empty{ ) ) {
my_list.remove_front( ) ;
' my_list.print{ );
4 }
•I
return 0;
Ниже показан результат работы программы (его можно видеть и в листинге):
0 0
0.1 0 0 0.1
0.2 0.1 0 0 0.1 0.2
0.3 0.2 0.1 0 0 0.1 0.2 0.3
0.4 0.3 0.2 0.1 0 0 0.1 0.2 0.3 0.4
0.4 0.3 0.2 0.1 0 0 0.1 0.2 0.3 0.4
0.3 0.2 0.1 0 0 0.1 0.2 0.3 0.4
0.2 0.1 0 0 0.1 0.2 0.3 0.4
0.1 0 0 0.1 0.2 0.3 0.4
0 0 0.1 0.2 0.3 0.4
0 0.1 0.2 0.3 0.4
0.1 0.2 0.3 0.4
0.2 0.3 0.4
0.3 0.4
0.4
ПРИМЕЧАНИЯ
Как и в случае slist_l, код slist_2 организован в виде двух файлов. Мы
сосредоточим внимание на различиях между этими примерами.
Связанные списки
53
Самой первой строкой определения шаблона является
template <class T>
Это означает, что мы можем строить списки из значений «произвольного»
типа Т — в кавычках, поскольку имеются определенные ограничения. Вообще
говоря, автор шаблона должен принять некоторые предпосылки относительно
того, какие операции поддерживаются типами, передаваемыми пользователем
в качестве параметров шаблона. Важно уделить пристальное внимание этим
предпосылкам; они должны быть разумны и четко сформулированы.
В slist_2 функция-элемент find(T) использует операцию равенства орега-
tor==(), поэтому тип Т обязан ее поддерживать. Подобным же образом print()
предполагает наличие для типа Т операции operator«(). Других ограничений
нет. Но не забывайте, что если вы модифицируете slist_2, вам, возможно,
придется ввести еще и свои собственные ограничения. Например, вы можете
решить, что вашему списку необходима сортировка в виде функции-элемента
sort(). В этом случае тип Т должен поддерживать еще и операцию сравнения,
такую, как operator<().
Поддержка перечисления поддерживается элементом данных current типа
Single_list::Sinlgle_node* и несколькими функциями-элементами:
♦ reset_current() возвращает указатель текущего элемента в начало
списка.
♦ increnient_current() сдвигает текущий указатель на следующий элемент
списка, если таковой имеется. В этом случае функция возвращает true.
Если current указывает на хвостовой элемент, increment_current() не
производит никаких изменений и возвращает false.
♦ get_current() возвращает значение текущего элемента. Если current
указывает на хвост списка, возвращаемое значение не определено; за
обнаружение такого состояния ответственна вызывающая функция. Для этой
цели предусмотрена функция current_is_tail(). Она возвращает true,
если current указывает на хвост списка.
♦ insert_after_current(const T) вставляет новый узел после текущего, если
только current не указывает на хвост списка. В последнем случае
функция возвращает false. Заметьте, что insert_after_current(const T) не
изменяет самого указателя current.
В классе нет функции вроде decrement_current(), поскольку довольно
бессмысленно двигаться по односвязному списку в обратном направлении.
«Обратных» указателей здесь просто нет. (Однако такая функция была бы важна
для поддержки перечислителей в двусвязном списке. Мы не приводим такого
примера, но нетрудно понять, как это можно сделать. Последний пример этой
главы позволяет двигаться по списку в обратном направлении, но не с
помощью перечислителя. Мы пользуемся другой, более мощной методикой.)
Как уже отмечалось в начале этого раздела, новая функция-элемент add_re-
аг(Т) позволяет реализовать на основе slist_2 очередь. Поскольку указатель
next хвостового узла ссылается на последний элемент списка, мы можем
выполнять вставку в конец списка весьма эффективно, не проходя для этого по
всему списку. Новая организация списка требует внесения небольших
изменений в конструктор Single_list(T) и функцию add„front(T). Эти функции, как
и add_rear(T), должны обрабатывать пустой список как специальный случай.
54
Глава 2
Это поясняется в комментариях исходного кода. Чтобы упростить
сопровождение кода, эта специальная обработка инкапсулирована в новой закрытой
функции-элементе add_to_empty(T).
Двусвязные списки
По двусвязным спискам можно эффективно перемещаться в обоих
направлениях. Элементы могут добавляться или удаляться с любого конца, что
делает возможным организацию структуры данных, известной как deque
(сокращение выражения очередь с двумя концами). Двусвязный список также
оказывается удобен для различных операций разбивки и слияния списков.
Мы не будем вводить новые функции для «хирургии указателей», а
сосредоточимся в первом примере на базовых операциях и затем во втором, более
сложном из примеров покажем, как реализовать простой итератор. Итераторы
широко используются в стандартной библиотеке шаблонов (STL), и потому стоит
затратить некоторые усилия на то, чтобы разобраться, как они устроены.
Рис. 2.2 показывает, как выглядит двусвязный список в памяти.
Рис. 2.2.
Расположение в памяти
двусвяэного списка,
содержащего числа 3, 4 и 2
Head Tail
(запредельный указатель)
Простой двусвязный список
Первый из примеров, dlist_l, во многих отношениях похож на slist_l и
slist_2. Мы возьмем эти два примера в качестве отправной точки и
сконцентрируем внимание на тех чертах dlist_l, которые его отличают.
Наиболее существенное отличие касается вложенного класса Dlist__node,
который заменяет класс Single_node первых двух примеров. Кроме указателя
next, в DUst_node имеется также указатель prev. Он-то и делает список
двусвязным. Любая функция-элемент, модифицирующая список, должна иметь в
виду наличие этого указателя.
Код
Код примера организован в виде двух файлов: dlist_l.h реализует
собственно список, dlist_l.cpp показывает пример работы с ним. Вот текст dlist_l.h.
// Файл dlist_l.h
// Простой не-шаблонный список целых.
#include <iostream>
#include <cassert>
using namespace std;
class Double list {
и
Ш
Связанные списки
55
[ jprivate:
> : class Double_node {
к \
\* ■ // Единственное назначение Double_node - поддержка класса
L // Double list. Поэтому все его элементы объявлены как
'. i // private, a Double_list является его другом.
к ■ friend class Double_list;
t // Создать Double_node с осмысленным значением.
£ ; Double node(int node val) : val(nodeval) { }
// Создать пустой Double__node.
Double_node( ) { }
~Double_node( ) { }
// Распечатать значение.
л void print val( ) const { cout « val « " "; }
Г 1 Double_node *next; // Указывает на следующий узел списка.
r *] Double__node *prev; // Указывает на предыдущий элемент.
int val; // Данные.
t-
i 1
Г,'
К
[ '
i ■
rib
i
r public:
t
r
L
);
Double_node *head; // Указывает на начало списка.
Double_node *tail; // Указывает на запредельный элемент.
// Объявленные закрытыми и не определенные операции копирования
// и присваивания, что делает их недоступными.
Double_list & operators(const Double_list £);
Double_list(const Dpuble__list &) ;
// Создать пустой список.
Double_list( ) {
// Пустой "запредельный" узел.
head = tail = new Double_node;
tail->next = 0;
tail->prev = 0;
}
// Создать список, содержащий единственный элемент.
Double_list(int node_val) {
// Пустой "запредельный" узел.
head = tail = new Double_node;
tail->next = 0;
tail->prev = 0;
add_front(node_val);
}
// Пройти по списку от головы к хвосту, удаляя каждый элемент.
-Double list( ) {
56
Глава 2
Double_node *node_to_delete = head;
'-* for (Double__node *sn = head; sn != tail;)
"J sn = sn->next;
* < delete node_to_delete;
node_to_delete = sn;
}
i ■! // assert( node to delete == tail ) ;
: 1
к
s
Г
•i
-1
I
; \
H *
. I
1
■A
delete node to delete;
i
1 bool is_empty( ) const {return head — tail;}
- i // Ввести новый элемент в начало списка.
i^ , void add_front(int node_val) {
£ Double_node *node_to_add = new Double_node(node_val) ;
node_to_add->next = head;
node_to_add->prev = 0;
head->prev = node_to_add;
f head = node to add;
}
void add_rear(int node_val) {
if ( is_empty( ) )
// Предложение "else" не выполняется для пустого списка,
// так как tail->prev равно нулю и, таким образом,
// tail->prev->next будет бессмасленным.
add_front(node_val);
else {
Double_node *node__to_add = new Double_node(node_val) ;
node_to_add->next - tail;
node_to_add->prev - tail->prev;
tail->prev->next = node_to_add;
tail->prev = node_to_add;
)
}
// Вставляет в список node_val сразу после
// первого вхождения key, если key в данный момент есть
// в списке. Если key *не* входит в список,
t*"lj| // ничего не делает и возвращает false. Заметьте, что
\\ * // такая функция могла бы быть реализована и в классе
h ■ // Single_list.
bool insert_after(int node_val, const int key) {
for ( Double_node *dn = head; dn != tail; dn = dn->next ) {
// Входит ли ключ в список?
if ( dn->val = key ) {
f ""J // Да! dn теперь указывает на ключ. Создать новый
!* j // Double_node для node_val и вставить его после ключа.
с- <
Double__node *node_to_add = new Double_node (node_val) ;
node_to_add->prev — dn;
node to add->next — dn->next;
Связанные списки
57
dn->next->prev - node_to_add;
}
» dn->next = node to add;
return true;
)
return false;
}
// Заметьте, что remove_front( ) и remove_rear( ) освобождают
// память, занятую удаляемым элементом, поэтому они должны
\ * // правильым образом обрабатывать пустые списки. В противном случае
// они могли бы удалить Double__node, который нельзя удалять.
* int remove_front( ) {
■ if ( is_empty( ) ) throw "tried to remove from an empty list";
i
I Double_node *node_to_remove = head;
i I int return_val = node_to_remove->val;
head = node_to_remove->next;
head->prev = 0;
de 1 e te node_to_remove;
return return_val;
}
int remove_rear( ) {
if ( is_empty( ) ) throw "tried to remove from an empty list";
Double_node *node_to_remove = tail->prev;
if { node_to_remove->prev == 0 ) {
//Остался только один узел, вызвать remove_front( ).
return remove_front( ) ;
}
else {
int return_val = node_to_remove->val;
node_to_remove->prev->next = tail;
tail->prev = node_to_remove->prev;
delete node_to_remove;
return return_val;
>
)
// Удаляет из списка первое вхождение node_val.
// Если значения в списке нет, возвращает false.
bool remove_val(int node_val) {
for ( Double_node *dn = head; dn != tail; dn - dn->next ) {
// Входит ли node_val в список?
if ( dn->val == node_val ) {
[ // Да! dn теперь указывает на ключ. Удалить узел.
dn->prev->next = dn->next;
dn->next->prev - dn->prev;
delete dn;
58
Глава 2
return true;
}
>
return false;
}
// Возвращает true, если node_val в списке,
// false в противном случае,
bool find(int node_val) const {
for { Double_node *dn = head; dn != tail; dn - dn->next ) {
if ( dri->val == node val ) return true;
} ■ ■,'"
return false;
}
// Возвращает значение элемента списка. Пользователь должен быть
// уверен, что element_num не меньше единицы и не больше
// размера списка.
int get_nth(const int elementjnum) const {
if ( element_num < 1 ) throw "get_nth argument less than one";
int count - 1;
. for ( Double_node *dn = head; dn != tail; dn = dn->next ) {
if ( count++ = element_num J return dn->val;
}
throw "element num exceeds list size";
}
// Возвращает размер списка, подсчитывая его элементы.
// Неэффективна, поскольку предполагается, что такая операция
// не будет частой (для тестирования), и поэтому лучше затратить
// здесь дополнительное время, чем терять его на модификацию
// переменной размера списка при каждом включении/удалении элемента.
int size ( ) const {
int count ~ 0;
- for ( Double_node *dn = head; dn != tail; dn » dn->next ) ++count;
1 return count;
i
l
}
1 // Распечатывает список.
1 void print( ) const {
_ for ( Double__node *dn = head; dn != tail; dn = dn->next ) {
J dn->print_val( );
1 1
cout « endl;
1 )
Связанные списки
59
А вот файл dlist_l.cpp:
// Файл dlist—l.cpp
#include "dlist_l.h"
// Следующая главная программа показывает простые примеры того,
// как используется dlist_l; параметр шаблона имеет тип float.
// Программа выдает результат, показанный ниже. Каждая строка вывода
// показывает текущее содержимое my_list no мере того, как он растет
//и сокращается. При наращивании списка мы вводим в него по два
// элемента перед каждым вызовом print( ). При сокращении мы
// печатаем содержимое после каждого удаления элемента.
//
//0 0
//1001
//210012
//32100123
//4321001234
//4 3 -999 21001234
// 4 -999 21001234
// -999 21001234
//21001234
//1001234
//001234
'//01234
//1234
//234
//3 4
' <// 4
\ .
[
i чDouble list my list; // Создать пустой Double list.
I ~ ~ ~
int main()
!<
// Ввести в список несколько элементов,
for ( int val = 0; val < 5; ++val ) {
my_list.add_front(val);
my_list.add_rear(val);
my_list.print( );
}
// Вставить в список -999 после первого узла,
// значение которого равно 3.
my_list.insert_after(-999, 3);
my__list.print{ );
// Удалить первый узел, значение которого равно 3.
my_list.remove_val(3);
my_list.print( );
60
Глава 2
// Удалить элементы по одному. Чтобы очистить список,
//не требуется знать его длину,
while ( ! my_list.is_empty( ) ) {
my_list. remove__f ront ( ) ;
my__list. print ( ) ;
)
return 0;
Ниже показан результат работы программы (его можно видеть также и в
комментарии листинга).
0 0
10 0 1
2 10 0 12
32100123
4321001234
4 3 -999 21001234
4 -999 21001234
-999 21001234
21001234
10 0 12 3 4
0 0 12 3 4
0 12 3 4
12 3 4
2 3 4
3 4
4
ПРИМЕЧАНИЯ
( ПРУ
В этом примере двусвязного списка имеются некоторые новые
функции-элементы. Наиболее важной из них является remove_rear(), поскольку
именно она делает возможной организацию на основе dlist_l двунаправленной
очереди, т. е. deque.
Функция msert_arter(int, const int) вставляет в список новый узел после
первого вхождения узла с указанным значением. Функция remove_val(int)
удаляет из списка первое вхождение узла с указанным ключом.
Шаблон двусвязного списка
В dlist_l функции insert_after(int, const int), remove_val(int) и find(int)
имеют ограниченные возможности, поскольку списки часто содержат
повторяющиеся значения. Если вас интересует не первый из входящих в список
узлов с данным ключом, эти функции вам ничем не помогут.
Так зачем вообще включать их в dlist_l? Они введены в класс списка,
поскольку полезны для иллюстрации важного момента в его проектировании.
Функция inscrt_after(int, const int), например, была бы куда более полезна,
если позволяла включать в список элемент после некоторого узла, идентифи-
Связанные списки
61
цированного пользователем. Мы должны, таким образом, предусмотреть
способ, посредством которого пользователь мог бы манипулировать узлами
списка как таковыми. Однако непосредственная реализация подобной
возможности открывала бы немалую часть деталей реализации, которые лучше было бы
сохранить закрытыми.
Программа slist_2 демонстрирует один из способов решения данной
проблемы — реализацию перечислителя. Здесь мы покажем иной подход к решению:
использование итератора. Итератор будет являться «дескриптором» узлов
типа Doublenode. Это означает, что он будет обеспечивать доступ и
манипуляцию с узлами некоторым специфическим образом, не требующим знания
деталей внутреннего устройства Double_node. Насколько это возможно, мы
хотели бы, чтобы итератор выглядел и вел себя подобно указателю.
(Проектирование итераторов — многосторонний и сложный предмет, и мы, в интересах
ясности и простоты, не затрагиваем многие его аспекты.)
Код
Код примера содержится в двух файлах: dlist_2.h реализует собственно
список, a dlist_2.cpp показывает пример работы с ним. Вот файл dlist_2.h.
И
// Файл dlist_2.h
// Шаблон двусвязного списка. Функции find( ), remove_front( )
//и remove_rear ( ) для сообщения об ошибке возвращают сигнальное
// значение. Ошибки итератора генерируют исключения.
#include <iostream>
#include <cassert>
using namespace std;
template <class T>
class Double_list {
public:
* !
* ,
\
4
i
II Здесь требуется опережающее объявление
class iterator;
friend class iterator;
a
private:
class Double_node;
friend class Double_node;
class Double_node {
public:
friend class Double_list;
friend class iterator;
// Создать Double_node с осмысленный значением
Double node(T node__val) : val (node_val) { }
62
Глава 2
t // Создать пустой Double_node.
Double_node( ) { }
--Double_node ( ) { }
9 ;j // Распечатать значение узла. Тип Т должен перегружать
" I // operator«( ) .
; i void print_val( ) { cout « val « " "; }
"4
■' 1 Double__node *next; // Указывает на следующий Double_node.
'* * ,Double_node *prev; // Указывает на предыдущий Double__node.
1 T val; // Данные:
P: I );
■1
■ 1public:
E 1 class iterator {
' -I // Этот класс является "дескриптором", или псевдоуказателем,
Р "J // для объектов Double_node.
;-j //
| | // Операции:
П |
Ы
| ; // ++ переводит итератор на следующий Double_node.
; i1 // Неопределенна, если итератор уже указывает на
к.
1.4
С.
г
1
- 1
I
Hi
// запредельный узел.
// -- Переводит итератор на предыдущий Double_node.
L // Неопределенна, если итератор указывает на Головиной узел,
г ■!
// == означает, что два итератора указывают на
// один и тот же Double_node.
// != является отрицанием -~.
■ ? // * возвращает значение данных Double_node.
'J
: 1 public:
friend class Double list<T>;
// Нулевой конструктор,
j iterator( ) : the_node(0) { }
I
fc-i j iterator (Double_node * dn) : the_node (dn) { )
. ■-] // Создать итератор из указателя на узел Double_node.
// Для использования итераторов в качестве аргументов или
// возвращаемых значений требуется конструктор копии.
// Копирует узел,
iterator(const iterator & it) : the node(it.the node) { }
iterators operator=(const iterators it) {
\ _ J the_node ~ it.the_node;
'• l return *this;
«J }
Связанные списки
63
г ■ bool operator»(const iterators it) const {
i * return (the_node *= it. the_node) ;
}
i
i
I
r
I
r'
4
i
r
I
I
bool operator!=(const iterators it) const {
return !(it == *this);
)
iterators operator++( ) {
if ( the_node == 0 )
throw "incremented an empty iterator";
if ( the_node->next = 0 )
throw "tried to increment too far past the end";
the_node = the_node->next;
return *this;
}
iterators operator—{ ) {
if ( the_node = 0 )
throw "decremented an empty iterator";
if ( the_node->prev = 0 )
throw "tried to decrement past the beginning";
the_node = the_node->prev;
return *this;
>
TS operator*( ) const (
|i if ( the_node = 0 )
| i throw "tried to dereference an empty iterator";
return the_node->val;
}
] private:
Double_node * the_node;
};
private:
Double_node *head; // Указывает на начало списка.
Double_node *tail; // Указывает на запредельный элемент.
J // Объявленные закрытыми и не определенные операции копирования
//и присваивания, что делает их недоступными.
Double__list S operator= (const Double_list S) ;
j Double_list (const Double^list 6);
i
, iterator head_iterator;
iterator tail__iterator;
public:
._] // Создать пустой список.
64
Глава 2
U
-*-j
*
r
r-
' \
I
: t
;!
r
1 J
- i
и
Double_list( ) {
head = tail = new Double_node;
tail->next = 0;
tail->prev = 0;
// Должны быть инициализированы *после* head и tail, поэтому
// этого нельзя сделать в списке инициализации Double_list.
head_iterator = iterator(head);
tail_iterator = iterator(tail);
}
// Создать список, содержащий единственный элемент.
Double_list(T node__val) {
head = tail = new Double_node;
tail->next = 0;
I tail->prev = 0;
1 ] // add__front( ) тоже должка их настраивать.
■ 1 head__iterator « iterator(head);
i 1 tail iterator = iterator(tail);
к j addfront(nodeval);
"■1 }
i
// Пройти по списку от головы к хвосту, удаляя каждый элемент.
-Double list( ) {
Double__node *node_to_delete = head;
! for (Double__node *sn = head; sn != tail;) {
sn = sn->next;
delete node_to_delete;
* j node_to_delete = sn;
}
f" \ // assert ( node_to__delete = tail );
| j delete node_to_delete;
"i
■. •! bool is_empty( ) {return head — tail;}
iterator front( ) { return head_iterator; }
■* i iterator rear( ) { return tail_iterator; }
void add_front(T node_val) {
Double_node *node_to_add ■ new Double_node(node_val);
node to add->next = head;
i!
. j node_to_add->prev = 0;
- head->p rev = node__to_add ;
i j head = node_to_add;
j ] ■* head_iterator = iterator(head);
}
// Ввести новый элемент в начало списка.
'; void add_rear(T node_val) {
j if ( is_empty( } )
Связанные списки
65
ж
I"1
• I // Предложение "else" не выполняется для пустого списка,
| j // так как tail->prev равно нулю и, таким образом,
■ // tail->prev->next будет бессмасленкым.
I ". add_front(node_val);
, J else {
1 Double_node *node__to_add = new Double_node (node_val) ;
■ node__to_add->next = tail;
i . node__to_add->prev = tail->prev;
tail->prev->next = node_to_add;
tail->prev — node_to_add;
tail_iterator - iterator(tail);
)
// Вставляет в список node__val сразу после итератора key_i.
// Если key_i *не* входит в список, возвращает false.
bool insert_after(T node_val, const iterator & key_i) {
for ( Double__node *dn = head; dn !- tail; dn = dn->next ) {
// Найден ли узел для заданного итератора?
if ( dn == key_i.the_node ) {
// Да! dn теперь на него указывает. Создать новый
// Double_node для node_yal и вставить его вслед за
// найденным узлом.
Double_node *node__to__add = new Double_node(node_val) ;
node_to_add->prev =* dn;
node_to_add->next — dn->next;
dn->next->prev = node_to_add;
dn->next — node_to_add;
return true;
)
}
return false;
// Удалить головкой элемент списка.
// Заметьте, что remove_front( ) и remove_rear( ) освобождают
// память, занятую удаляемым элементом, поэтому они должны
// правильным образом обрабатывать пустые списки. В противном случае
// они могли бы удалить Double_node, который нельзя удалять.
Т remove_front( ) {
if ( is_empty{ ) ) throw "tried to remove from an empty list";
Double__node *node__to_remove = head;
T return_val = node_to_remove->val;
head = node_to_remove->next;
head->prev = 0;
head iterator = iterator(head);
delete node_to_remove;
return return__val;
}
T remove_rear( ) {
if ( is_empty( ) ) throw "tried to remove from an empty list";
Double_node *node__to_remove = tail->prev;
if (node_to_remove->prev == 0) {
// Остался всего один элемент, вызвать reraove_front( ).
return remove__f ront ( );
}
else {
T return_val = node_to_remove->val;
node_to_remove->prev->next = tail;
tail->prev = node_to_remove->prev;
delete node_to_remove;
return return_val;
}
}
// Удаляет узел, на который ссылается итератор key_i.
// Если key_i не найден, возвращает false.
bool remove_it(iterator & key_i) (
for ( Double_node *dn = head; dn != tail; dn = dn->next ) {
// Найден ли узел для заданного итератора?
if ( dn == key_i.the_node ) {
//Да! dn теперь на него указывает. Удалить узел
dn->prev->next = dn->next;
dn->next->prev = dn->prev;
delete dn;
key_i.the_node = 0;
return true;
}
}
return false;
}
// Find( ) возвращает первый итератор, ссылающийся на node_val.
// Если node_val нет в списке, возвращает tail_iterator.
// Чтобы это работало, в классе Т необходима корректная
// operator==( ).
iterator find(T node_val) const {
for ( Double_node *dn - head; dn != tail; dn = dn->next ) {
if ( dn->val == node_val ) return iterator(dn);
}
return tail_iterator;
}
// Возвращает итератор, ссылающийся на n-ый элемент списка.
Связанные списки
67
, I
// Возвращает tail__iterator, если elementjnum меньше единицы
// или больше числа элементов в списке.
iterator get_nth(const int element_num) const {
if ( element_num < 1 ) return tail_iterator;
1 I int count = 1;
for ( Double_node *dn = head; dn != tail; dn = dn->next ) {
if ( count++ == element_num ) return iterator(dn);
}
II element_num слишком велик.
| return tail_iterator;
ь\ >
> 1
// Возвращает размер списка, подсчитывая его элементы.
// Неэффективна, поскольку предполагается, что такая операция
// не будет частой (для тестирования), и поэтому лучше затратить
// здесь дополнительное время, чем терять его на модификацию
// переменной размера списка при каждом включении/удалении элемента.
s ' int size( ) const {
1 int count = 0;
i • for ( Double_node *dn = head; dn != tail; dn = dn->next ) ++count;
- return count;
- s )
// Распечатать список,
void print( ) const {
for ( Double_node *dn = head; dn != tail; dn — dn->next ) {
dn->print_val( );
)
cout « endl;
}
LJ);
А вот dlist_2.cpp:
// Файл dlist_2.cpp
#include "dlist_2.h"
// Следующая программа показывает несколько примеров работы с dlist_2
// Результаты ее работы:
//
//43210
//43210
//01234
3*
68
Глава 2
i \ Double_list<int> the__list; // Пустой список.
I ■
■ ь int main()
f";1 Double_list<int>: :iterator list_iter;
int ret = 0;
E-i
i
If Ввести несколько значений в начало списка.
-j for (int j = 0; j < 5; ++j) {
' the_list.add_front(j);
\\ '
// Печать.
the_list.print( );
// Распечатать снова, используя итератор (почти так же эффективно).
* for ( list_iter = the_list.front( ) ;
list_iter != the_list.rear( ) ;
++list_iter ) {
1
\ \ cout « *list_iter « " ";
}
cout « endl;
M
r"
И
**4 II Теперь, с помощью итератора, распечатать в обратном порядке.
for ( list__iter = the_list.rear( ) ;
list_iter ! = the__list.front( )
; ) {
--list_iter;
cout « *list_iter « "
}
cout « endl;
return 0;
( ПРЬ
ПРИМЕЧАНИЯ
Итератор реализован как открытый вложенный класс с именем Double_
list::iterator. Поскольку класс открытый, пользователи могут создавать его
объекты. А поскольку он вложенный, имя его (iterator) должно
квалифицироваться именем класса, который его определяет. Это уменьшает вероятность
конфликтов имен.
Класс iterator должен знать о некоторых закрытых элементах класса
Double! 1st, и потому последний должен объявить итератор дружественным
классом. Подобным же образом iterator объявляет своим другом Double_list.
Обратите внимание на необходимые при этом опережающие (предварительные)
объявления.
Связанные списки
69
В дополнение к стандартным указателям на голову и хвост списка данный
вариант Doublelist объявляет head_iterator и tail_iterator, также
ссылающиеся соответственно на голову и хвост (повторим, что хвост — это
«запредельный» узел списка).
Давайте взглянем теперь на внутреннее устройство класса Double_list::ite-
rator. Его единственный (закрытый) элемент данных определяется так:
Double_node * the_node;
Именно это итератор и должен скрывать. Операции, объявляемые в классе
iterator, позволяют пользователям манипулировать этим узлом строго
определенным образом. Поведение этих операций специфицированы в следующей
таблице:
Операция
класса iterator
operator=()
operator==()
operator!=()
operator++()
operator--!)
operator*!)
Действие
Присваивает thejwde тоже значение, что и thejiode в правой части
присваивания.
Возвращает true, если два итератора ссылаются на один и тот же
узел. Некоторые предпочитают сравнивать адреса итераторов, а не
узлов, что будет более эффективной реализацией, когда требуется
выполнять большое число сравнений.
Отрицание операции ==.
Перемещает итератор на следующий узел списка.
Перемещает итератор на предыдущий узел списка.
Возвращает не сам узел Double_node, а значение его элемента node_val.
Для поддержки использования итераторов в качестве аргументов или
возвращаемых значений определяется конструктор копии.
Теперь, когда у нас есть итератор, давайте посмотрим на функции доступа и
модификации. Здесь мы сразу видим получаемую от итераторов выгоду.
Функции find(T) и get_nth(const int) возвращают iterator, а не bool, как их
предыдущий вариант. Когда требуемый итератор получен, пользователь
может передавать его другим функциям-элементам, обращаться к данным и
проходить по списку в обоих направлениях.
Пересмотренные варианты функций insert_after(T, const iterator &) и re-
move_it(iterator &) принимают параметры-итераторы. Пользователь может
теперь манипулировать списком как ему угодно (в разумных пределах), хотя
детали его реализации остаются надежно скрытыми.
#d ЬУ*
<s,
лО>
у
ГЛАВА
JjHQTtrflTH 1СМ1ЧР№ШГ>
*J* « *.
:*$},}
Арт Фридман
Ч ^ '
bt.h
btcpp
avi.h
avi.cpp
•4 jsiJS'ggfc-v W, ^~,
72
Глава 3
В этой главе мы сконцентрируемся в основном на двоичном дереве поиска,
которое является довольно элегантным средством организации
упорядоченных данных в виде, приспособленном для эффективного поиска. Двоичные
деревья полезны в самых разнообразных ситуациях. Например, двоичное
дерево может использоваться при синтаксическом разборе выражений,
составленных из двухместных операций. Программа инвентаря может применить
дерево для хранения информации о наличных компонентах.
Данная глава посвящена изучению преимуществ применения двоичных
деревьев и показывает, как можно преодолеть возникающие при этом проблемы.
Как всегда, вы должны помнить, что существуют разные пути реализации
той или иной структуры. Те из них, что приводятся здесь, служат только
примерами.
Введение в двоичные деревья
Деревья и, в частности, двоичные деревья являются структурами
несколько более сложными, чем списки. Списки представляются обычно как нечто
линейное, в то время как деревья в естественном представлении имеют более
одного измерения.
Деревья обычно изображают «растущими» сверху вниз, с корнем наверху,
как на рис. 3.1. Отдельные ячейки, из которых составляется дерево, называют
узлами. Связки, направленные от некоторого узла вниз, соединяют его с
дочерними узлами (или непосредственными потомками). Естественным образом
узел, имеющий дочерние узлы, называется их родительским узлом. Аналогия
с генеалогическим деревом позволяет ввести термины прародитель, предок и
потомок с очевидными значениями. Узел, не имеющий дочерних узлов,
называют листом. Хотя узел может иметь более одного дочернего, родительский
узел может быть у него только один. Структура данных, в которой узлы имеют
более одного родителя, не может считаться деревом. Единственным узлом, не
имеющим родителя, является корневой узел.
Рис. 3.1.
Расположение двоичного
дерева поиска в памяти
5
И
7±7
<
"
[ 9 N
Л 7 Л
7*7
010 ША
7*7
Выражение «двоичное дерево» является одновременно и осмысленным, и
сбивающим с толку. На первый взгляд, легко предположить, что двоичное
дерево — это просто частный случай дерева вообще, но это не так. Хотя оно
родственно другим деревьям в разных отношениях, двоичное дерево рассматривается
как совершенно особый вид. В обычном дереве узел может иметь произвольное
Двоичные деревья
73
число дочерних узлов. В двоичном дереве узел должен не иметь ни одного,
иметь один или иметь два дочерних узла. Дочерний узел, расположенный на
рисунке дерева левее родительского, называется левым потомком (левым
дочерним). Соответственно дочерний узел правее родителя называют правым
потомком. В общем, помните, что когда мы в этой главе неформальным образом
употребляем термин «дерево», то на самом деле имеем в виду двоичное дерево.
Существуют три стандартных процедуры перебора узлов двоичного дерева:
это обход с предварительной, порядковой и отложенной выборкой.
Определяются они рекурсивно.
♦ Чтобы обойти дерево с предварительной выборкой его узлов, нужно
сначала выбрать корневой узел, затем обойти левое поддерево (опять же с
предварительной выборкой), и, наконец, обойти правое поддерево.
♦ Обход с порядковой выборкой состоит в том, что сначала производится
порядковый обход левого поддерева, затем выбирается корневой узел,
после чего совершается обход правого поддерева.
♦ При обходе с отложенной выборкой производится сначала обход (также с
отложенной выборкой) левого, затем правого поддерева и только в конце
происходит выборка корневого узла.
В наших примерах эти схемы обхода являются основой различных
функций распечатки. Для функций, печатающих дерево в виде линейной
последовательности значений, термин «выборка узла* означает просто распечатку его
значения. Деструкторы в примерах применяют обход с отложенной выборкой,
которая гарантирует, что узел не будет удален до тех пор, пока не будут
удалены оба его дочерних узла. Здесь термин «выборка* означает «удаление».
Предыдущий рисунок (3.1) показывает, как выглядит структура двоичного
дерева поиска в памяти. Каждый узел состоит из трех независимых элементов:
действительных данных, которые в нем хранятся, указателя на левого
потомка и указателя на правого потомка. Ради удобства наши реализации будут
определять дополнительные поля, но существенными являются только эти три.
Хотя возможны реализации, основанные на массивах, мы будем следовать
более распространенному подходу, использующему динамическое распределение
памяти. Наши деревья будут написаны в форме шаблонов C++. Замечания,
сделанные в предыдущей главе относительно закрытых вложенных классов,
остаются в силе и здесь.
Двоичные деревья поиска
Чем отличается двоичное дерево поиска от любого другого двоичного
дерева? Для данных, сохраняемых в двоичном дереве поиска, должно быть
определено отношение упорядочения, такое, как «меньше». Простейшим примером
данных, которые можно упорядочить, являются целые числа, и мы будем
широко их использовать. Существенное свойство двоичного дерева — это то, что
для каждого данного узла все узлы левее его (т. е. левый дочерний и все его
потомки) содержат значения, меньшие значения самого узла. Аналогичным
образом все узлы правее данного содержат большие значения. Деревья, которые
мы будем обсуждать, не допускают повторяющихся значений, поэтому нам не
придется беспокоиться о том, что делать с узлами с равными значениями.
74
Глава 3
Рассмотрите, например, узел на рис. 3.1, содержащий значение 9. Все узлы
левее его имеют значения, меньшие 9, а все те узлы, что находятся правее —
большие 9. Это иногда называют критерием двоичного дерева поиска. Все
операции, которые мы будем производить над нашими деревьями, сохраняют этот
основной критерий.
Чем хороши двоичные деревья поиска? Это проиллюстрирует пример.
Допустим, мы хотим определить, содержит ли дерево значение 7. Мы начинаем с
корня и видим, что его значение равно 5. Это говорит нам, что мы можем
игнорировать все левое поддерево, потому что искомое значение — больше 5. Если
дерево более или менее сбалансировано, простое сравнение сразу отбрасывает
примерно половину данных, — это значительная выгода, особенно если дерево
велико. Повторяя ту же процедуру для правого поддерева, мы видим, что 7
меньше 9 и переходим к левому потомку. Он действительно содержит
значение 7, так что поиск завершен. В среднем число сравнений, необходимых для
нахождения значения в дереве (или констатации того факта, что его там нет),
пропорционально двоичному логарифму числа его узлов. Как мы увидим,
возможно значительное расхождение между средней оценкой и поведением
алгоритма в наихудшем возможном случае.
Помимо поиска, мы хотим иметь возможность вставки и удаления узлов.
Вставка значений в простое дерево поиска достаточно прямолинейна, но
удаляются они довольно хитрым образом. Как будет показано в разделе о
сбалансированных деревьях поиска, как вставка, так и удаление труднее
производить именно на сбалансированных деревьях. Мы также реализуем функции,
возвращающие n-й наименьший элемент дерева, и функции распечатки
деревьев в различных удобных формах.
Простой шаблон двоичного дерева поиска
Первый пример, шаблон класса Binary_tree, показывает простое дерево
поиска. Заметьте, что параметры и возвращаемые значения открытых
функций-элементов относятся к типу Т, а не к специальному классу узлов.
Код
Код примера организован в виде двух файлов. Bt.h является собственно
шаблоном дерева, a bt.cpp демонстрирует его работу. Вот bt.h:
#include <xostream>
#include <cassert>
using namespace std;
///////////////////////////////////////////////////////////////////
//
//
// Файл bt.h.
// Простой шаблон класса Binary_tree. Реализует двоичное дерево
// поиска без балансировки. Повторение значений недопустимо. Тип Т
// должен поддерживать копирование и следующие операции:
Двоичные деревья
75
//
//
//
|//
!//
к'
//
[// Открытые функции-элементы:
//
К/
К/
//
//
// bool add( const T insert_value );
|// void remove( T value );
T get_nth(const int element_num) const
operator^( );
operator« ( )
operator==( )
operator!=( )
operator<( ) ;
Binary__tree( >;
Binary_tree(const
~Binary_tree( ) ;
T root val );
Создать пустое дерево.
Создать дерево с одним эл.
Освободить память.
к/
//
К/
//
V*
//
//
I//
!//
//
//
//
// Замечания по реализации:
I//
Узлы содержат поле nodecount для упрощения реализации
get_nth. содержат также связки с родителем. Рекурсивные
функции отмечены специально.
int size( ) const;
bool find( T find value );
void print ( int level = 0 ) const;
void print_pre_order( ) const;
void print_in__order ( ) const;
void print_j>ost__order( ) const;
Вставить элемент.
Удалить элемент.
Значение п-го
по порядку эл-та.
Число эл-тов.
True если дерево
содержит find_value.
Напечатать как дерево.
Напечатать как список.
Напечатать как список.
Напечатать как список.
К'
//
//
//
к/
\///////////////////////////////////////////////////////////////////
//
template <class T>
class Binary_tree {
[private:
struct Tree_node {
friend class Binary_tree,
T val;
Tree_node *left_child;
Tree_node *right_child;
Tree_node *parent;
int nodecount;
// Данные узла.
// Число узлов в поддереве с корнем в
// данном узле, включая сам узел.
76
Глава 3
■ч
■ i
г I
* ш
ж
I «
ь
I I
1
J
Tree_node( );
Tree_node( const T node_val ) : val(node_val) { )
-Tree_node( ) { }
// Isa_right_child( ) и isa_left_child( ) возвращают true если
// вызывающий узел является правым/левым потомком, и false
// в противном случае. Для корневого узла возвращается false.
bool isa__right_child( ) const {
if ( (parent ™ 0) || ( parent->right_child •= this) )
return false;
else
return true;
)
bool isa_left_child( ) const {
if ( (parent ==0) || ( parent->left_child != this) )
return false;
else
return true;
}
// Print( ) печатает данные в виде дерева "на боку" с корнем
// слева. "Обратный" рекурсивный обход (т.е. справа налево) -
// иначе мы напечатали бы зеркальный образ. Nodecount показан
■ 1
'г // в скобках. Нулевые потомки показаны как "@".
void print ( const int level = 0 ) const {
■ I // Инициализация указателем this вместо корня делает возможным
j // печатать поддеревья, а не только все дерево целиком.
"* const Tree node *tn = this;
if ( tn !« 0 ) tn->right_child->print( level + 1 );
for (int spaces = 0; spaces < level; ++spaces)
cout « "
. I if ( tn != 0 )
i * cout « tn->val « ' (' « tn->nodecount « ') ' « endl ;
j else
; j cout « "@" « endl;
: ч
Г J if ( tn != 0 ) tn->left_child->print( level + 1 );
■ \
i
л
Г
}
>; // Конец определения Tree_node.
private:
J// Закрытые данные Binary_tree:
деревья
Tree__node ,*root;
Tree_node *zero_node; // По сути const, используется для
// возврата значения из find_node(const T).
// Закрытые функции Binary_tree:
// Запретить копирование и присваивание.
Binary_tree(const Binary_tree &);
Binary_tree & operator^( const Binary_tree fi );
// Создать корневой узел, инициализировать val значением root_val,
// обнулить дочерние указатели, nodecount установить в 1.
void maJce_new_root ( const T root_val ) {
root = new Tree_node(root_val);
root->left_child = 0;
root->right_child = 0;
root->parent = 0;
root->nodecount = 1;
}
// Find_node(T £ind_value) возвращает ссылку на указатель,
// чтобы упростить реализацию remove(T).
Tree_node * & find__node( Т find_value ) {
Tree_node *tn — root;
while ( (tn != 0) && (tn->val != find_value) ) {
if ( find_value < tn->val )
tn = tn->left_child;
else
tn = tn->right_child;
}
// Вместо того, чтобы просто возвратить tn, мы выполняем эти
// сложные действия, чтобы убедиться, что возвращается *ссылка*
//на искомый узел в пределах дерева.
if ( tn as о )
// Find_value нет в дереве.
return zero_node;
else if ( tn->isa_left__child( ) )
// Tn - левый потомок родителя.
return tn->parent->left_child;
else if ( tn->isa_right_child< ) )
// Tn - правфй потомок poAHrenat.
return tn->parent->right__child;
else if { tn -— root )
// Специальный случай - нет родителя.
return root;
// Управление не должно достигать этой точки. Возвращается нуль,
// чтобы подавить предупреждение компилятора.
78
Глава 3
■t
assert(false);
return zero node;
// Insert_node( const T, Tree_node * ) присоединяет новое значение
// к соответствующему листу, если егъ еще нет в дереве. Возвращает
// указатель но новый узел, если он создан, нуль в противном случае.
* // Нерекурсивна. Увеличивает nodecount для каждого пройденного узла.
* ■ // Это нужно сделать *после* вставки узла, чтобы в случае
* г // дублирования (новый узел не создан) не получился ложный nodecount.
J
Tree_node *
-i insert__node ( const T insert_value, Tree_node * start_node = 0 )
■ *
J if ( root == 0 ) {
й \ II Специальный случай пустого дерева.
j make_new_root( insert_value );
* return root;
>
* \ if ( start node == 0 ) start node = root;
Tree_node *tn = start__node;
ь
while ( (tn != 0) US (tn->val != insert_value) ) {
i if ( insert_value < tn->val ) {
, J // Проверить левого потомка,
if ( tn->left_child == 0 ) {
II Вставить новый узел как левый дочерний для tn.
attach_node( tn, tn->left_child, insert_value );
II Прибавить 1 к nodecount для tn и всех его предков.
i adjust_nodecount_to_root(tn, l) ;
■
* return tn->left^child;
}
f else {
1 tn = tn->left_child;
': >'
l , else {
■ ■ // Проверить правого потомка.
j if ( tn->right_child == 0 ) {
fc // Вставить новый узел как правый дочерний для tn.
у attach_node( tn, tn~>right_child, insert_value );
i
1 II Прибавить 1 к nodecount для tn и всех его предков,
i adjust nodecount to root(tn, 1);
Двоичные деревья
79
fl
■Ч
а
[* j return tn->right__child;
}
else {
tn = tn->right_child;
)
}
}
// Insert__value уже в дереве.
assert { tn != 0 );
return 0;
}
// Attach_node( Tree_node *, Tree_node * &, T ) - вспомогательная
// функция для insert_node( const T insert_value, Tree_node * ).
void attach_node( Tree_node * new_parent,
Tree_node * & new_child, T insert_value ) {
// Вставить новый узел как дочерний для tn.
new_child = new Tree_node( insert_value );
new_child->left_child = 0;
new_child->right_child = 0;
new_child->parent - new_j>arent;
new_child->nodecount = 1;
}
// Adjust_nodecount_to_root( Tree_node *, int ) прибавляет incr к
// полю nodecount узла tn к всех предков включая корневой узел.
// Заметьтеу что родительская связка корня равна нулю.
void adjust_nodecount_to_root( Tree_node * tn, int incr ) {
while ( tn != 0 ) {
tn->nodecount += incr;
tn = tn->parent;
}
}
\ !
P>.
// Get_nth_node( Tree_node *, const int ) возвращает узел,
// соответствующий n-му порядковому значению в дереве.
// Рекурсивна. Опирается на поле nodecount, которое должно быть
// корректным.
Tree_node * get_nth_node(Tree_node * tn, const int nth) const {
// Специальный случай пустого корня.
ff if ( tn = 0 ) return 0;
I // Запомнить nodecount левого потомка tn.
int lc_count
= (tn->left_child != 0) ? tn->left__child->nodecount : 0 ;
if ( (lc count + 1) == nth ) {
80
Глава 3
II Готово - tn сам является n-м значением.
return tn;
}
else if ( lc__count >= nth ) {
J ! // Искать nth в левом потомке.
return get_nth_node(tn->left_child, nth);
}
else {
// Искать (nth - lc_count - 1) в правом потомке.
return get_nth_node(tn->right_child, nth - lc_count -1);
м >
i' 4
: ]
L ■
1
* I
h
I
// Cleanup ( Tree_node * ) удаляет в<5е Tree_nodes в обходе с
// отложенной выборкой. Реальные действия -Binary_tree( ).
j I void cleanup (Tree_node *tn) {
i 4 // Специальный случай пустого корня.
1
i
l
4 , , r
L if ( tn -= 0 ) return;
f
if ( tn->left_child != 0 ) {
cleanup(tn->left_child);
t t tn->left child = 0;
)
if ( tn->right_child ! = 0 ) {
' - cleanup(tn->right_child);
tn->right_child = 0;
■ i J
. i delete tn;
К >
I // Print_pre( const Tree_node * ) рекурсивно печатает значения в
. } // поддереве с корнем в tn в обходе с предварительной выборкой.
\ I
Г 1 void printjpr*(const Tree_node * tn) const {
f i // Специальный случаи пустого корня,
f J if ( tn = 0 ) return;
cout « tn->val « '*
\ j if ( tn->left_child != 0 ) {
I -I print_pre( tn->left_child );
« ! >
t \ if ( tn->right_child != 0 ) {
1 *" print_pre( tn->right_child );
\*\ >
i **
\ i II Print_in( const Tree_node * ) рекурсивно печатает значения в
i. _j // поддереве с корнем в tn в порядковом обходе (как сортированные)
Двоичные деревья „______„_ __ §j_
j.ji void print_in(const Tree_node * tn) const {
// Специальный случаи пустого корня.
if ( tn и о ) return;
м
I
r .1
\ 1
i ■
и-1
t
*■ A
**.
r ,
i
t
I
H
I.'
I
i
t
И
k
1 J
fc_J
if ( tn->left_child != 0 ) {
print_in( tn->left_child );
}
cout « tn->val « " ";
if ( tn->right_child != 0 ) {
print_in( tn->right_child );
}
}
// Print_post( const Tree_node * ) рекурсивно печатает значения в
// поддереве с корнем в tn в обходе с отложенной выборкой.
void print_post(const Tree_node * tn) const {
// Специальный случай пустого корня.
if ( tn == 0 ) return;
if ( tn->left_child != 0 ) {
print_post( tn->left_child >;
)
if ( tn->right_child !- 0 ) {
print_post ( tn->right__child ) ;
}
cout « tn->val « " ";
)
// Конец закрытых функций Binary_tree.
public:
// Открытые функции-элементы Binary_tree:
Binary_tree( ) : zero_node(0) { root = 0; }
Binary_tree(const T root_val ) : zero_node(0) {
make_new_root( root_val );
}
// Освобождает память всех Tree__node, с отложенной выборкой.
// Реальную работу делает закрытая cleanup( Tree_jiode * ).
-Binary_tree( ) {
cleanup( root );
}
82
Глава 3
|! // Add( const T ) добавляет к дереву значение, если его тан еще
// нет. Возвращает true, если insert_value действительно добавлено.
// Реальную работу делает закрытая insert_node(T).
i
I
" J
1
ь i
j
V
I I
}
i
bool add( const T insert__value ) {
Tree_node *ret = insert_node(insert_value);
if (ret) return true;
else return false;
}
|- // Remove ( T ) удаляет узел дерева, содержащий value.
| j // Эту функцию реализовать корректно труднее всего, поскольку
tr I // удаляемый узел может иметь два дочерних. Они должны быть
i // воссоединены с деревом, но удаляемый узел имеет только одного
f // родителя, поэтому требуются некоторые ухищрения. Эта операция
Г // производится так, чтобы дерево сохранило свой критерий,
j* //и чтобы высота его *не* увеличилась.
// Объявление nodento__remove как ссылки на указатель делает
( // remove ( Т ) проще, чем это могло бы быть.
9
* void remove{ T value ) {
\ ■ Tree node * & node to remove = find node( value );
Tree_node * predecessor — 0;
Tree_jiode * temp — 0;
if ( node_to_remove == zero__node ) return;
J assert ( node__to__remove->val == value );
< // Сначала обработаем простые случаи, когда node_to_remove
1 // имеет не более одного дочернего узла.
■ if ( node_to_remove->left_child = 0 ) {
щ // Node to remove не имеет левого потомка.
temp = node__to__remove ;
if ( node_to_remove->right_child != 0 ) {
// Node_to_remove имеет правого потомка.
node_to_remove->right_child->parent = node_to_remove->parent;
)
v // Заменить node_to_remove его правым потомком. Неважно, если
//он нулевой. После данного оператора переменная
// node_to__remove больше не ссылается на узел, который мы
$ // удаляем. Но temp все еще ссылается на него.
node_to_remove = node_to_remove->right_child;
f !
* \ If Правый потомок node_to__remove теперь занимает в дереве
- I // его позицию. Его nodecount не изменился.
// Но nodecount его родителя и остальных предков нужно
// уменьшить на единицу, чтобы учесть удаление узла.
Двоичные деревья
83
i adjust nodecount to root(temp->parent, -1);
4
: j
1 i
i
■
•
<
■]
II Освободить память только что удаленного узла,
delete temp;
return;
}
else if ( node_to_remove->right_child = 0 ) {
, // Node_to_remove не имеет правого потомка,
temp — node_to_remove;
//Мы знаем, что левый потомок ненулевой, так как мы сейчас
// в "блоке else". Значит, не нужно это проверять
// перед исполнением следующего оператора.
1 node_to_remove->left_child->parent = node_to_remove->parent;
■ j
t * // См. комментарии выше в "блоке if".
| | node_to_remove ~ n.ode__to_remove->left_child;
1 j adjust_nodecount_to_root(temp->parent, -1);
- ! delete temp;
! return;
// Если мы достигли данной точки, то знаем, что node_to_remove
// имеет оба дочерних узла. Найти его предшествующий узел, т.е
// самого правого потомка его левого дочернего узла.
predecessor — node_to_remove->left_child;
while ( predecessor->right_child !— О )
1 ] predecessor = predecessor->right_child;
f i // Заменить значение node to remove значением его
// предшественника.
node_to_remove->val = predecessor->val;
i
* | // Теперь, когда значение predecessor'а переместилось вверх по
а •& II дереву, мы должны прикрепить левого потомка predecessor'а
^ II к его родителю. Он станет правым потомком последнего,
i I // Вспомните, что predecessor не имеет правого потомка,
// поскольку мы нашли его именно по этому признаку.
Tree_node * рр = predecessor->parent;
if ( рр =- node__to_remove ) {
// Специальный случай, когда предшественник - левый потомок
// node_jto_remove (т.е. последний не имеет правого потомка).
pp->left_child - predecessor->left_child;
if ( predecessor->left child !- 0 )
j predecessor->left_child->parent - pp;
. * )
! '• else if ( predecessor->left_child != 0 ) {
t i II У predecessor'а есть левый потомок, и мы не должны
[ ' // оставлять его "висящим". Он становится правым потомком
84
Глава 3
// predecessor'a.
j pp->right_child = predecessor->left_child;
■ predecessor->le£t child->parent = pp;
! }
, ■ else {
, j // Левый потомок predecessor'а нулевой, так что
] // полю right_child его родителя указывать не на что.
j assert( pp->right_child == predecessor );
pp->right_child = 0;
\ )
.1
I // Теперь обновить nodecount для predecessor->parent и всех
// его предков. Родительская связка корня равна нулю.
adjust_nodecount__to_jroot(pp, -1) ;
i
1 // Значение predecessor'а переместилось в другой узел, а его
// потомки, если есть, присоединены к его родителю.
1 // Исходно занимаемая им память больше нам не нужна,
delete predecessor;
* "".j // Get_nth( const int ) возвращает n-oe порядковое значение
1 //в дереве.
Т get_nth(const int element_num) const {
Tree_node * tn = get_nth_node(root, element_num) ;
I return tn->val;
* i >
«
// Size{ ) возвращает число узлов в дереве.
int size( ) const { return root ? root->nodecount : 0; }
I '
4 // Find( T ) возвращает true, если findjvalue есть в дереве,
//и false в противном случае.
! bool find( T find_value ) {
: Tree_node *tn = find^node( find_value );
if ( tn != 0 )
return true;
else
return false;
}
] // Print( ) производит обратную порядковую выборку и печатает
j // дерево "на боку".
*
void print ( ) const {
cout « "\n" « "===============!==SB==" « »\n"
« endl;
Двоичные деревья S5
// Это вызов Binary_tree::Tree_node::print( ),
// а *не* рекурсивный вызов Binary_tree::print{ ).
root->print( );
*
}
i i
// Следующие функции печатают дерево в линейной последовательности
// при обходе с предварительной, порядковой и отложенной выборкой.
// Print_inorder( ) эквивалентна сортировке. Эти функции не имеют
// параметров, но вызывают рекурсивные закрытые функции, принимающие
// параметры типа Tree_node *, чтобы те выполняли всю работу.
void print__pre_order ( ) const {
Г j print_pre (root) ;
cout « endl;
}
void print_in_order( ) const {
print_in(root) ;
cout « endl;
>
void print^jost^order( ) const {
print_post(root);
cout « endl;
)
»
■i
};
После заполнения дерева десятью элементами bt.cpp для проверки печатает
дерево в псевдографическом виде «на боку», с корнем слева. Для каждого узла
она печатает его значение вместе со значением поля nodecount в скобках.
Нулевые дочерние указатели обозначаются символом «@». После распечатки
дерева его значения выводятся также в линейной последовательности для
различных алгоритмов обхода. После этого из дерева удаляются листовой и
корневой узлы, после каждого удаления дерево снова распечатывается. Для
каждого узла его левый дочерний узел печатается ниже и правее, правый —
правее и выше.
и дту———™———————" —-——— и I I
Ё!" // Файл bt.cpp: Примеры работы с двоичным деревом
#include "bt.h"
// Создать двоичное дерево поиска, содержащее целые,
// Корневой узел содержит значение 7.
Binary_tree<int> my_bt{ 7 );
// Заполнить дерево целыми значениями,
void populate{ ) {
my_bt.add( 5 )
my_bt.add( 9 J
my_bt.add< 6 J
my_bt.add( 4 )
ST
86
Глава 3
S 3 my_bt.add( 11 ) ;
' my_bt.add( 8 ) ;
[ my_bt.add( 2 ) ;
myjat.add( 10 ) ;
my_bt.add( 19 );
I>
int main( )
,' populate( ) ;
// Распечатать все дерево.
my_bt.print ( ) ;
j // Рапечатать my_bt в различных линейных последовательностях.
j cout « endl;
'L * cout « "Pre-order: " ;
I I my_bt.print_pre_order ( );
; i
t
■■
■
f.4
h'«
cout « "Post-order: " ;
my_bt.print_post_order ( );
cout « "In-order: " ;
my_bt.print_in_order ( );
cout « "In-order, using get_nth( int ):" « endl;;
cout « " " ;
int i;
for (i = 1; i <= my_bt.size{ ); ++i) {
cout « my_bt.get_nth(i) « " ";
}
cout « endl;
cout « endl;
// Удалить некоторые значения, распечатывая получившееся дерево.
my_bt.remove { 2 );
cout « "«w—««™-»™—■" « endl;
cout « " removed 2" ;
my_bt.print ( );
my_bt.remove ( 7 );
I , cout « "===--=ss==s--======-==--==sa==" « endl;
cout « " removed 7" ;
i ' my_bt.print ( );
,*J return 0;
Двоичные деревья 87
А вот вывод программы bt.cpp:
@
19(1)
б
11(3)
@
10(1)
е
9(5)
@
8(1)
@
7(10)
@
6(1)
@
5(4)
@
4(2)
в
2(1)
@
Pre-order: 7542 698 11 10 19
Post-order: 24658 10 19 11 97
In-order: 2 4 5 6 7 8 9 10 11 19
In-order, using get_nth( int ):
2 4 5 6 7 8 9 10 11 19
removed 2
@
19(1)
в
11(3)
@
10(1)
e
9(5)
@
8(1)
@
7(9)
@
6(1)
@
5(3)
@
4(1)
@
88
Глава 3
removed 7
@
19(1)
@
11(3)
е
10(1)
е
9(5)
@
8(1)
@
@
6(8)
@
5(2)
@
4(1)
@
I ПРИМЕЧАНИЯ
Как показано в блоке комментария к классу Binary_tree, тип параметра
шаблона Т должен определять несколько операций. Хотя данная реализация
требует, чтобы все они были определены, единственными операциями,
обязательными для всех реализаций двоичного дерева поиска, являются орега-
tor==() и operator<(). Без этих операций отношения бессмысленно и думать о
какой-то организации объектов типа Т в виде двоичного дерева поиска.
Binary_tree определяет вложенный закрытый класс Tree_node для
представления внутренней структуры двоичного дерева. Следуя тому же подходу,
что применялся в случае связанных списков, структура его скрыта от
пользователя. Давайте посмотрим на эту внутреннюю структуру Tree__node.
Поле val содержит данные, хранящиеся в узле. Left_child и right_child
являются указателями на левого и правого потомков узла. Нулевое значение
любого из них означает, что соответствующий дочерний узел отсутствует. Это
необходимые поля структуры узла двоичного дерева.
В дополнение к ним мы, в основном ради удобства, определяем еще два
поля. Поле parent указывает на родительский узел. Поле nodecount содержит
число узлов поддерева с корнем в данном узле. Это поле позволяет эффективно
реализовать функцию Binary_tree::get_nth(const int). Заметьте, что эти
«удобные» поля не обходятся даром. Не говоря о дополнительном расходе
памяти, они должны обновляться при каждой модификации дерева. Стоят они
того или нет, зависит от требований проектируемого вами приложения.
Функциями-элементами Tree_node являются isa_right_child(), isa_left_child()
и print(). Первые две возвращают true, если узел является правым (левым)
потомком своего родителя и false в противном случае. Функция print() распеча-
Двоичные деревья
89
тывает поддерево «на боку» с корнем в крайней левой колонке. Она
предназначена в основном для отладки.
У Binary_tree имеется ряд открытых и закрытых элементов-функций, но
только два закрытых элемента данных. Элемент данных root — это просто
указатель на корневой узел дерева. Zero_node — указатель на Tree_node, чье
единственное назначение — помочь в реализации закрытой функции find_no-
de(const T), которую мы вскоре обсудим. Теперь мы готовы перейти к
рассмотрению закрытых и открытых функций-элементов Binary_tree.
У Binary_tree два открытых конструктора. Конструктор по умолчанию
создает пустое дерево, a Binary_tree(const T) создает дерево с одним элементом
типа Т. Деструктор удаляет каждый Tree_node, совершая обход дерева с
отложенной выборкой. Отложенная выборка гарантирует, что никакой узел не
будет удален до тех пор, пока не будут удалены все его потомки.
( ЗАМЕЧАНИЕ ПРОГРАММИСТА
Ради простоты мы решили не поддерживать копирование и
присваивание. Поэтому конструктор копии и операция присваивания объявлены
закрытыми элементами и не определяются. Это позволяет
компилятору сообщить об ошибке при попытке выполнить эти операции. Если
вам они нужны, то их следует сделать открытыми и написать
соответствующие определения.
Открытая функция-элемент add(const T) выделяет новый Tree_node со
значением, которое передано пользователем, и прикрепляет его к дереву в
качестве листа. Действительная работа делается закрытой функцией insert_no-
de(const T), которая, в свою очередь, вызывает три других закрытых
функции — make_new_root(const T), attach_node(Tree_node*, Tree__node*, T) и
adjust_nodecount_to_root(Tree__node*, int).
Функция insert_node(const T), по сути, пробует найти новое значение,
походя по дереву таким образом, как это описано во вводном разделе о деревьях
поиска. Функция сравнивает новое значение со значением, хранящимся в
корневом узле. Если они равны, ничего делать не нужно, так как наше дерево не
допускает повторяющихся значений. Если новое значение больше, insert_no-
de(const T) переключает свое внимание на правого потомка корневого узла.
В противном случае она обращается к левому потомку. Этот процесс
повторяется для выбранного потомка, пока значение не будет найдено в дереве (в этом
случае ничего больше не требуется) или же в фокусе нашего внимания не
окажется нулевой узел. То есть мы выбираем, например, поле left_child текущего
узла и обнаруживаем, что оно равно 0. Тогда мы прикрепляем новый узел к
текущему в качестве левого потомка. Это делает attach_node(Tree_node*,
Tree_node, T), выделяя новый Tree_node, инициализируя его поля и
производя простую «хирургическую операцию» над указателями, чтобы «привить»
узел к дереву.
Еще не все закончено, поскольку прикрепление нового узла сделало
необходимым обновление полей node count для всех его предков. Это и производит
adjust_nodecount_to_root(Tree_node*, int), проходя по дереву вверх от нового
узла до корня и увеличивая счетчик каждого встреченного по пути узла. Пол*1
90
Глава 3
Tree_node::parent делает это совсем простым. Make_new__root(const T)
обрабатывает специальный случай, когда дерево изначально пустое.
Функцию remove(T) реализовать и понять наиболее трудно. Чтобы увидеть,
почему это так, давайте посмотрим сначала на простые случаи. Если узел,
который требуется удалить, является листом, нам нужно только освободить его
память, установить в 0 указатель левого или правого дочернего узла родителя
и уменьшить на единицу поля nodecount всех его предков. Когда удаляемый
узел имеет единственный дочерний узел, задача не намного сложнее. Вместо
обнуления дочернего указателя родительского узла мы просто присваиваем
ему значение дочернего указателя удаляемого.
Remove(T) сначала обрабатывает эти специальные случаи. Она начинает с
того, что вызывает find_node(T), которая возвращает ссылку на указатель на
удаляемый узел. Указатель возвращается по ссылке, чтобы remove(T) могла
легко модифицировать поле родителя удаляемого узла, которое указывает на
последний. Не считая этой особенности, find_node(T) ничем не отличается от
процедур поиска, обсуждавшихся ранее.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
В функции find^jiode(T) есть еще два момента, о которых стоит
упомянуть. И тот, и другой являются следствием того факта, что
функция возвращает ссылку на указатель Tree.jwde. Во-первых, функции
нужен указатель, на который она может сослаться, даже если это
нулевой указатель. Это единственная причина того, что мы объявляем поле
zero_node. Во-вторых, вместо простого возврата найденного указателя
на искомый узел, функция идет на дополнительные ухищрения, чтобы
возвратить ссылку на соответствующий дочерний указатель
родительского (по отношению к искомому) узла* Не эффективнее ли было бы
иметь отдельный указатель, ссылающийся на родителя узла в то
время, как мы идем вниз по дереву? Как и для большинства вопросов,
касающихся эффективности, ответом будет «это зависит...» Подход,
которому следует findjnode(T), довольно дорогостоящ, но связанные с ним
расходы не увеличиваются с ростом глубины дерева. Цена, которую
придется платить за отдельный родительский указатель — увеличивает-
* ся. Если производительность для вашего приложения очень важна,
может быть, стоит затратить некоторые усилия и написать тестовую
программу, определяющую, при какой глубине дерева «счет сравняется».
Что произойдет, если мы захотим удалить узел, имеющий два
непосредственных потомка? Каждый из них должен быть вновь прикреплен к дереву, но
освобождается всего одна точка закрепления. Не забывайте также, что любая
реорганизация узлов, которую мы предпримем, должна сохранять критерий
двоичного дерева поиска. Способ, который мы изберем — это оставить на
месте узел, подлежащий удалению, но заменить значение этого узла на значение
его непосредственного предшественника в ряду значений дерева.
Непосредственный предшественник, оказывается, имеет не более одного дочернего узла,
поэтому произвести «дендрохирургию» на нем будет проще, чем на узле,
который мы хотим удалить.
Двоичные деревья
91
Откуда известно, что предшественник узла с двумя потомками имеет не
более одного дочернего узла? Непосредственный предшественник — узел
с-наибольшим значением в левом поддереве. Мы ищем его, переходя в левое
поддерево удаляемого узла и двигаясь по нему вниз, выбирая каждый раз правое
поддерево, пока не окажется, что дальше двигаться некуда. Найденный таким
образом узел не имеет правого потомка. Выбор в качестве замещающего
значения непосредственно ему предшествующего сохраняет, кроме того, критерий
дерева поиска.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Когда удаляемый узел имеет два дочерних узла, remove(T) может с
тем же успехом заменить его значение на то, которое следует в дереве
непосредственно после него, а не на предшествующее. Исключительное
использование для замены предшествующего значения может сильно
разбалансировать дерево, если remove(T) вызывается часто и никакой
схемы балансировки не имеется. Поэтому, если вас это беспокоит,
можно модифицировать remove(T) таким образом, чтобы она
поочередно выбирала предшествующее и последующее значения для замены
удаляемого узла.
Функция get_nth(int) возвращает n-ое наименьшее значение в дереве. Si-
ze() возвращает число элементов дерева. Эта функция может работать очень
эффективно благодаря наличию в каждом узле поля nodecount. Если n-е
значения вам ни к чему или эффективность не играет решающей роли, поле node-
count и всю машинерию, связанную с его обновлением, можно убрать.
Функция print() печатает значения узлов вместе с их nodecount, располагая корень
в левой части страницы и разворачивая дерево вправо. Этот вид дерева,
положенного «на бок*, несложно реализовать и он очень полезен при отладке.
Рекурсивные функции print_pre_order(), print_in_order() и print_post_or-
k der() печатают элементы дерева в линейной последовательности в соответст-
! вии со стандартными схемами обхода двоичного дерева. Print__pre_order() пе-
| чатает значение узла до того, как будет напечатан любой из его потомков.
i Print__post_order() делает прямо противоположное, печатая значение узла
| только после того, как будут распечатаны все его потомки. В случае двоичного
| дерева поиска print_in_order() печатает элементы дерева в восходящем
порядке. Find(T) возвращает true, если его параметр находится среди значений
дерева, и false в противном случае.
Ценным дополнением к bt.h был бы набор итераторов, схожий с тем, что
был определен для связанных списков в главе 2. Как и в случае списков,
итераторы могли бы предоставить пользователю изящный способ более полного
контроля над узлами, не обнаруживая их внутреннего устройства.
Определение итераторов для обходов с предварительной, порядковой и отложенной
выборкой сделало бы соответствующие процедуры печати ненужными. Типичная
реализация использовала бы стек. В главе 15 дается пример порядкового
итератора двоичного дерева, не требующего стека.
92
Глава 3
Сбалансированное двоичное дерево поиска
Предположим, мы хотим организовать двоичное дерево поиска, которое
содержит целые от 1 до 7. Рис. 3.2 показывает два из возможных способов
сделать это. Рис. 3.2а соответствует «идеальному» дереву для данного набора
значений. Так как оно отлично сбалансировано, для поиска любого значения
необходимо не более трех сравнений. С другой стороны, хотя рис. 3.2Ь имеет все
признаки двоичного дерева поиска, оно настолько перекошено, что время
поиска в нем такое же, как у линейного списка. Чтобы достичь значения 7, нам
придется перебрать все его узлы.
Рис. 3.2.
Сбалансированное
и несбалансированное
двоичные деревья поиска
Чем определяется, будет ли простое дерево поиска сбалансированным или
нет? Это целиком зависит от порядка, в котором вводятся его элементы. Ввод в
строго восходящем или строго нисходящем порядке дает наихудший
результат из всех возможных. К несчастью, разбалансированные деревья слишком
нередки, чтобы их игнорировать.
Несмотря на простоту понятия, лежащего в их основе, сбалансированные
деревья чрезвычайно трудно реализовать. Поскольку прежде всего
рассматривать сбалансированные деревья нас побуждает вопрос об эффективности,
бессмысленно разрабатывать решение, которое основывалось бы на полном
реструктурировании всего дерева всякий раз, когда в него вводится или удаляется
один элемент (если только мы не рассчитываем построить дерево всего один
раз, никогда его не модифицировать, но обращаться к нему много раз).
Есть несколько подходов к балансировке деревьев. Здесь мы реализуем
дерево с балансировкой по высоте. Сбалансированные по высоте деревья обычно
называют AVL-деревьями по инициалам их разработчиков — Адельсона,
Вельского и Ландиса. Идея состоит в том, чтобы поддерживать дерево в
приблизительно сбалансированном состоянии, потребовав, чтобы каждый узел
удовлетворял следующему условию: высота его левого поддерева должна
отличаться от высоты правого не более чем на единицу. Это требование приводит к
нескольким дополнительным моментам реализации, которые не нужны в
случае простых деревьев поиска.
Каждому узлу потребуется новое поле для хранения его фактора баланса,
который равен высоте правого поддерева минус высота левого. Допустимыми
значениями фактора баланса являются, таким образом, -1, 0 и +1. Любое
другое значение означает, что узел разбалансирован. Поскольку простой способ
вставки лпи удаления узла может вывести дерево из равновесия,
функции-элементы add(T) и remove(T) должны включать дополнительный код,
а:
2 6
/\ /\
1 3 5 7
Ь:1
\.
\
\
\
\
\
Двоичные деревья
93
приводящий дерево обратно в сбалансированное состояние. Чтобы это
осуществить, им придется вызывать новые закрытые функции-элементы rotate_left
(Tree_node *&) и rotate_right(Tree_node *&).
Ротация является методикой такой реорганизации узлов дерева, которая
удовлетворяет критерию двоичного дерева поиска. Использование ротаций для
балансировки поддеревьев детально поясняется в комментариях к исходному
коду. Чтобы понять эти комментарии, необходимо разобраться в том, что собой
представляют ротации как таковые. Рис. 3.3а показывает простое двоичное
дерево с целыми значениями. На рис. З.ЗЬ можно видеть то же дерево после
ротации узла со значением 6 вправо. Наконец, рис. 3.3с показывает дерево после
ротации того же узла влево (из состояния на рис. З.ЗЬ). Это примеры одиночных
ротаций. Заметьте, что ротации не нарушают критерия дерева поиска.
а:
А
4 8
/\ /\
3 5 7 9
Рис. 3.3. Ротации
а: перед выполнением ротаций
Ь: после ротации узла 6 вправо
с: после ротации узла 6 влево
Двойная ротация состоит во вращении узла сначала влево, а затем вправо,
или наоборот. Две одиночных ротации на рис. 3.3 узла, содержащего 6,
эквивалентны двойной ротации.
Следующий код реализует класс сбалансированного двоичного дерева. Он
построен на основе предыдущего класса двоичного дерева из этой главы, но
включает дополнительные механизмы для поддержания баланса дерева.
Код
Код программы состоит из двух файлов. В avl.h представлен собственно
класс дерева, a avl.cpp содержит пример его использования. Вот код из avl.h.
#include <iostream>
#include <cassert>
using namespace std;
\UIIU1 ////////////////////////// //////////////////////////////////
[// Файл avl.h.
// Шаблон класса avl_tree. Реализует двоичное дерево поиска с
[// балансировкой по высоте. Локальная балансировка выполняется add(T) и
// remove(Т), поддерживая сбалансированность. Повторения недопустимы.
[// Тип Т должен поддерживать копирование и следующие операции:
94
Глава 3
tr
>
1 ■.
V?4
V
<■ .j
f-;
t -
Ь -.
Г
I ,
Г ,
Г ■'
*
ir
V
i.
в Ь
Г ■ ■*
i ■
//
//
//
//
//
//
//
// Открытые функции-элементы:
//
Avl_tree( );
Avl_tree(T root_val );
~Avl tree( );
operator^( );
operator« ( ) ;
operator==( );
operator!=( );
operator<( );
Создать пустое дерево.
Создать дерево с одним эл-том.
Освободить память всех узлов.
)
F 1
1 ■
t A .
■ I
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// Замечания по реализации:
//
Узлы имеют поле nodecount для упрощения реализации
get_nth. Имеются также связки с родительским узлом.
Рекурсивные функции отмечаются особо.
bool add( T insert_value
void remove( T value );
T get_nth (const int element__num) const;
int size( ) const;
bool find( T find value );
void print ( int level = 0 ) const;
void print_pre__order( ) const;
void print_in_order( ) const;
void print_post_order( ) const;
Добавить элемент.
Удалить элемент.
Значение n-ro эл-та
отсортированных данных.
Число элементов.
True если find_value
имеется в дереве.
Напечатать "на боку".
Напечатать как список.
Напечатать как список.
Напечатать как список.
//
//
//
//
//
///////////////////////////////////////////////////////////////////
template <class T>
class Avl_tree {
private:
struct Tree_node {
friend class Avl_tree;
T val;
Tree_node *left_child;
Tree_node *right_child;
Tree_node *parent;
int bal;
J
// Данные, хранящиеся в узле.
// Фактор баланса =
// (высота правого поддерева)
// - (высота левого поддерева)
Двоичные деревья 95
int nodecount; // Число узлов поддерева с корнем
//в данном узле, включая его
// самого.
Treejiode ( ) ;
Tree_node( const T node_val ) : val(node_yal) { }
~Tree__node( ) { }
// Isa_right_child( ) и isa_left__child( ) воэврашают true, если
// вызывающий узел является правым/левым потомком родительского
// узла, и false в противном случае. Возвращает false для корня.
bool isa_right_child( ) const (
if ( (parent ==0) j| ( parent->right_child != this) )
return false;
else
return true;
}
bool isa__left_child( ) const {
if ( (parent = 0) | | ( parent->left__child != this) )
return false;
else
return true;
}
bool isa_leaf( ) const {
return ( (right_child — 0) && (left__child == 0) ) ;
>
// Print( ) печатает дерево "на боку", корнем влево.
// "Обратный" рекурсивный порядковый обход (т.е. справа налево)
// — иначе был бы напечатан зеркальный образ. Nodecount/bal
// показаны в скобках. Нулевые потомки показаны как "@".
void print ( const int level - 0 ) const {
// Инициализация значением this вместо корня делает возможной
// печать поддеревьев,
const Tree_node *tn = this;
if ( tn !■ 0 | tn->right_child->print( level + 1 );
for (int spaces = 0; spaces < level; -H-spaces)
cout « "
if ( tn != 0 )
cout « tn->val « * (' « tn->nodecount « ' /' « bal «
« endl;
else
cout « "@" « endl;
if ( tn != 0 ) tn->left_child->print( level + 1 );
')
96
Глава 3
t
fcr-
г:
}
}; // Конец объявления Tree_node
private:
// Закрытые данные Avl_tree:
ЗД1 Tree_node *root;
Tree_node *zero_node; // По сути константа, служит для возврата
// нулевого значения из find node(T).
// Закрытые функции Avl_tree:
jr»" // Запретить копирование и присваивание.
V.
ft
У
Ife
Avl_tree(const Avl^tree &);
Avl__tree & operator» { const Avl__tree 6 ) ;
f^'l // Создать коневой узел, инициализировать val значением root__val,
N11 дочерние узлы нулем, bal нулем, nodecount единицей.
void raake_new_root( const T root_val ) (
root = new Tree_node(root_val);
"J root->left_child = 0,
+.-J root->right_child = 0
\У\ root->parent = 0
root->bal = 0
j: 3 root->nodecount = 1
// Find_node(T find_yalue) возвращает ссыпку на указатель,
// чтобы упростить реализацию remove(T).
Tree_node * & find_node( Т find_value ) {
Tree_node *tn = root;
while ( (tn •= 0) && (tn->val != find_value) )
t. 2 if ( find_value < tn->val )
tn = tn->left_child;
else
tn = tn->right_child;
}
н
К Z II Вместо того, чтобы просто возвратить tn, мы несколько
* // усложняем процедуру, гарантируя, что возвращаем * ссылку*
'Л II в пределах дерева на узел, который ищем.
!;Э if < tn = 0 )
// Find_value нет в дереве.
"\ , return zerojnode;
else if ( tn->isa_left_child( ) )
// Tn - левый потомок своего родителя.
return tn->parent->left__child;
Двоичные деревья
97
else if ( tn->isa_right_child( ) )
// Tn - правый потомок своего родителя.
return tn->parent->right_child;
else if ( tn = root )
// Специальный случай - родитель отсутствует.
return root;
// Управление не достигает этой точки. Возвратить фиктивное
// значение, чтобы предотвратить предупреждение компилятора,
assert(false);
return zero node;
// Insert_node( const T, Tree^node * ) присоединяет новое значение
// к соответствующему листу, если значения еще нет в дереве.
// Возвращает указатель на новый узел, если он создан, иначе 0.
// Нерекурсивна. Увеличивает nodecount каждого пройденного узла.
// Это делается *после* вставки узла, чтобы не получить неверных
// nodecount в случае уже имеющегося в дереве значения.
Tree_node * insert_node
( const T insert_yalue, Tree_node * start_node « 0 )
{
if ( root — 0 ) {
// Специальный случай пустого дерева.
make_new_root( insert_value );
return root;
}
if ( start__node = 0 ) start__node = root;
Tree_node *tn = start_node;
while ( (tn != 0) fi& (tn->val != insert_value) ) {
if ( insert_value < tn->val ) {
// Проверить левого потомка.
if ( tn->left_child = 0 ) {
// Присоединить новый узел в качестве левого потомка tn.
attach_node( tn, tn->left__child, insert_value );
// Модифицировать nodecounts и факторы баланса для всех
// предков. Если найден предок с новым фактором баланса +2
// или -2, дерево в данной точке нуждается в балансировке
// путем соответствующих ротаций. Заметьте, что это нужно
// сделать всего один раз. После того, как такой узел
// сбалансирован, его предки автоматически окажутся
// также сбалансированными.
adjust_for_add( tn->left_child );
4 Зшс.1208
98
Глава 3
return tn->left__child;
}
else {
tn = tn->left_child;
}
}
else {
// Проверить правого потомка.
if ( tn->right_child == 0 ) {
// Присоединить новый узел в качестве правого потомка tn.
attach_node( tn, tn->right_child, insert_value );
adjust_for_add( tn->right_child ) ;
return tn->right_child;
}
else {
tn = tn->right_child;
}
}
// Insert_value уже имеется в дереве.
assert ( tn 1=0 );
return 0;
// Attach_node( Tree_node *, Tree_node * &, T ) - вспомогательная
// функция для insert_node ( const T insert_value, Tree__node * ).
void attach_node( Tree_node * new_parent,
Tree node * & new child, T insert value ) {
t. л — "~ — -
$. i // Присоединить новое значение в качестве левого потомка tn.
Jj 'l new_child = new Tree_node ( insert_value ) ;
new_child->left_child = 0;
new_child->right_child = 0;
new_child->parent = new_parent;
new_child->nodecount = 1;
new_child->bal = 0;
// Adjust_nodecount_to_root( Tree_node *, int ) прибавляет incr
// к полю nodecount узла tn всех его предков включая корень.
// Заметьте, что родительская связка корня равна 0.
void adjust_nodecount_to__root( Tree_node * tn, int incr ) {
while ( tn != 0 ) {
'.pT tn~>node count += incr;
4 tn = tn->parent;
Ы >
Двоичные деревья 99
// Adjust_for_add( Tree_jiode *, Tree_node * ) изменяет факторы
' i // баланса и nodecount для вновь созданного узла и его предков.
j // При необходимости выполняются ротации. Если мы найдем
\ // несбалансированный узел {с фактором баланса 2 или -2)
I //и сбалансируем его поддерево, то можем на этом закончить,
. ■ // поскольку методика балансировки сохраняет исходную высоту
, ' // данного поддерева.
1
£. 1 void adjust_for_add( Tree_node * new child ) {
i t
к i
t j // New_parent может быть корнем, но new_child - нет.
assert( new_child •= root );
bool rotate_flag = false;
bool bal_was__changed = false;
Tree_node * new_parent = new_ehild->parent;
Tree_node * new_grandparent = new_parent->parent;
// Родитель вновь созданного узла не может быть выведен из
// равновесия этой вставкой. Только его *прародитель* может
// быть первым узлом, где баланс нарушается. Поэтому мы делаем
// первую "итерацию" вне цикла и затем инициализируем
// трехуровневую структуру для собственно цикла.
| у ++(new_parent->nodecount);
j
1
ч
if ( new_child->isa_right_child( ) ) {
// New_parent был сбалансирован и не имел правого потомка.
// Поэтому высота его левого поддерева должна быть 0 или 1.
++(new__parent->bal) ;
J bal_was_changed = true;
И )
else {
// New_child должен быть левым потомком, так как он не корень
— (new_parent->bal);
k l<( bal_was_changed — true;
}
6»
I"
\
Р
** .
\
assert { (new__parent->bal > -2) ££ (new_parent->bal < 2) ) ;
while ( new_grandparent != 0 ) {
// В цикле мы полагаем, что new__child и new_parent обновлены.
//Мы обновляем new_grandparent. Сначала нужно обновить
// его nodecount и фактор баланса.
++(new_grandparent->nodecount);
if ( new_parent->isa_right_child( ) ) {
// Если новый фактор баланса new_parent 1 или -1, и он
// изменился, то перед этим он был нулем. Следовательно,
// высота поддерева с корнем в new_parent увеличилась на 1.
// Соответственно высота правого поддерева new__grandparent
// также увеличилась на единицу. С другой стороны, если
100
Глава 3
'" -я // new_parent->bal стал теперь нулем, он должен был быть
Г _ // равен 1 или -1, так что новый узел выровнял поддерево
ч■_ //с корнем в new_parent, и его выысота и, следовательно,
- ' // фактор баланса для new_grandparent не изменились.
if ( new_parent->bal 1= 0 && bal_was_changed )
: // Незачем трогать bal_was_changed. Нам нужно, чтобы он
'• // оставался равен true при следующей итерации.
* ■ ++{new_grandparent->bal);
f else
\* // new_grandparent->bal не был изменен, так что мы хотим,
1 // чтобы bal_was_changed был false при следующей итерации.
1" ■ bal was changed = false;
*:i >
[ 4 else if ( new_parent->isa_left_child( ) .) {
- | // Предыдущие комментарии годятся и здесь, ко теперь мы
, J // уменьшаем new_grandparent->bal, так как рассматриваем
\ // его левое поддерево.
if ( new_jparent->bal != 0 Sfi bal_was_changed )
—(new_grandparent->bal);
else
bal_was_changed « false;
>
r i
t-
4.
j.
* я
t
I
.1
и
if ( (new_grandparent->bal < -1)
|| (new_grandparent->bal > 1) ) {
rotate_flag — true;
break;
}
В // Подняться по дереву на шаг.
new_child = new_parent;
new_parent = new_grandparent;
"l new_grandparent = new_grandparent->parent;
• *\
£ if ( rotate_flag ) {
, " // New_grandparent несбалансирован, и мы должны сбалансировать
j // его путем одной или двух ротаций. Хотя есть четыре случая,
I I // два из них - зеркальные отражения. Помните, мы только что
* *"• // обновили nodecount вплоть до узла, который несбалансирован.
ь .' // Когда мы завершим ротации, нужно будет обновить nodecount
*■ ! // между данной точкой и корнем.
I *
•-■-ч // Некоторые операции обновления баланса для new__child зависят
! "• //от исходного фактора баланса, поэтому запишем их здесь,
г i // перед тем как начать ротацию.
t "i int new_child_orig_bal = new_child->bal;
t j
fc I // Простой случай (всего одна ротация) и его отражение.
J I if ( new_parent->isa_left_child( )
f j && new child->isa left child ( ) ) {
i_Ml // Ротация new_parent направо вокруг new__grandparent.
Двоичные деревья 101
I ■
i I // new_grandparent передается по ссылке.
Е J if ( new_grandparent->isa_left_child{ ) }
* ' rotate_right< new__grandparent->parent->left_child ) ;
else if ( new_grandparent->isa_right__child( ) )
rotate_right( new_grandparent->parent->right_child );
else {
assert( new_grandparent = root );
rotate_right( root );
}
// Обновить факторы баланса и nodecount начиная от
// new_parent->parent, который теперь не то же самое, что
// new_grandparent - это результат проведенной ротации.
■• \ new_parent->bal = 0 ;
new_parent->right__child->bal = 0;
■» adjust__nodecount_to_root( new_jparent->parent, 1 );
}
else if ( new_parent->isa_right_child{ )
&& new_child->isa_right_child{ ) ) (
// Ротация new_parent налево вокруг new_grandparent
)
// new__grandparent передается по ссылке,
if ( new_grandparent->isa_lef t__child( ) )
rotate_left( new_grandparent->parent->left_child );
else if ( new_grandparent->isa_right_child( ) )
rotate_left( new_grandparent->parent->right_child )
else {
assert( new_grandparent == root };
rotate left{ root );
}
// Обновить факторы баланса и nodecount.
new_parent->bal = 0;
new_j>arent->left_child->bal = 0;
adjust_nodecount_to__root ( new_parent->parent, 1 );
// Теперь тяжелые случаи, требующие двух ротаций,
else if ( new_parent->isa_left_child( )
&£ new_child->isa_right_child{ ) )
{
// Ротация new_child. new_parent передается по ссыпке.
// Мы уже внаем, что new_j?arent - левый потомок,
// так что проверять не стоит.
rotateJLeft( new_grandparent->left_child );
// new__child передается по ссылке. Здесь нам уже нужно
// проверять, так как потомок был повернут, и прежние
// предположения не годятся. Так как new__child поднялся
// на шар, нам нужно проверить new_grandparent, а не
// new__parent.
if ( new_grandparent->isa_left_child{ ) )
Глава 3
rotate_right( new_grandparent->parent->left_child );
else if ( new_grandparent->isa_right_child( ) )
rotate__right < new_grandparent->parent->right_child );
else {
assert( new_grandparent == root );
rotate__right ( root ) ;
)
// New_grandparent теперь правый потомок вновь
// сбалансированного поддерева. Обновить факторы баланса и
// nodecount начиная с родителя этого поддерева.
new_grandparent->parent->bal = 0;
// Другие модификации зависят от исходного фактора баланса
// new_child.
if (new_child_orig_bal = 0) {
new_grandparent->bal — 0;
new_grandparent->parent->left_child->bal = 0;
)
else if (new_child_orig_bal == -1) {
new_grandparent->bal - 1;
new_grandparent->parent->left_child->bal = 0;
}
else if (new_child_orig_bal = 1) {
new_grandparent->bal = 0;
new_grandparent->parent->left_child->bal = -1;
}
adjust_^nodecount_to_root
( new_grandparent->parent->parent , 1 );
}
else if ( new_parent->isa__right_child( )
&& new_child->isa_left_child( ) ) (
// Ротация new_child. new_parent передается по ссылке.
// мы уже внаем, что new_parent - правый потомок,
// проверять нет нужды.
rotate_right( new_grandparent->right_child );
// new_child передается по ссылке. На этот раз требуется
// проверка, поскольку потомок был повернут и прежние
// предположения не годятся. Так как new_child поднялся
// на шаг, нам нужно проверять new_grandparent, а не
// new_parent.
if { new_grandparent->isa_left_child( ) )
rotate_left( new_grandparent->parent->left_child );
else if ( new_grandparent->isa__right__child( ) )
rotate_left( new_grandparent->parent->right_child );
else {
assert{ new_grandparent = root );
rotate_left( root );
)
Двоичные деревья
103
// New_grandparent теперь левый потомок вновь
// сбалансированного поддерева. Обновить факторы баланса и
// nodecounts начиная с родителя этого поддерева.
new_grandparent->parent~>bal - 0;
** // Другие модификации зависят от исходного фактора баланса
' // new__child.
' if (new_child_orig_bal = 0) {
, new_grandparent->bal =0;
new_grandparent->parent->right_child->bal — 0;
}
else if (new_child_orig__bal = -1) {
new_grandparent->bal =0;
new_grandparent->parent->right child->bal = 1;
)
else if (new_child_orig_bal = 1) {
new_grandparent->bal = -1;
new_grandparent->parent->right_child->bal — 0;
)
adjust_nodecount__to_root
( new_grandparent->parent->parent , 1 );
fe:
// Get_nth_node( Tree_node *, const int ) возвращает узел,
// соответствующий n-му (в отсортированном дереве) значению.
// Рекурсивна. Требует, чтобы поле nodecount было корректным.
Tree_node * get_nth_jiode(Tree_node * tn, const int nth) const {
// Специальный случай пустого корня.
if ( tn = 0 ) return 0;
// Запомнить nodecount левого потомка tn.
int lc_count
= (tn->left_child •= 0) ? tn->left_chiId->nodecount : 0 ;
if ( (lc_count + 1) «- nth ) {
// Готово, так как tn сам и является n-ым значением.
return tn;
>
else if ( lc_count >= nth ) {
// Поиск nth в левом потомке.
return get_nth__node(tn->left_child, nth);
}
else {
// Поиск (nth - lc_count - 1) в правом потомке,
return get_nth node(tn->right_child, nth - lc_count -1);
}
// Функции ротации статические, поскольку им не нужен указатель
// this. Заметьте, что параметр является ссылкой на указатель
// Tree_node.
static void rotate_right(Tree_node * & node) {
Tree_node *tn = node->left_child;
node->left_child = tn->right_child;
if (tn->right__child) tn->right__child->parent = node;
tn->right_child = node;
tn->parent = node->parent;
node->parent = tn;
// Обновить nodecount перед обновлением узла.
// Это нужно сделать именно в таком порядке, т.е. начиная
// с нижнего узла.
int leftcount — node->left_child ?
node->left_child->nodecount : 0;
int right count = node->right__child ?
node->right_child->nodecount : 0;
node->nodecount = leftcount + rightcount + 1;
leftcount m tn->left_child ?
tn->left_child->nodecount : 0;
rightcount — tn->right_child ?
tn->right_child->nodecount : 0;
tn->nodecount = leftcount + rightcount + 1;
node = tn;
)
static void rotate_left(Tree_node * & node) {
Tree_node *tn — node->right_child;
node->right_child = tn->left_child;
if (tn->left_child) tn->left_child->parent = node;
tn->left_child = node;
tn->parent = node-parent;
node->parent = tn;
// Обновить nodecount перед обновлением узла.
// Это нужно сделать именно в таком порядке, т.е. начиная
//с нижнего узла.
int leftcount — node->left_child ?
node->left_jchild->nodecount : 0 ;
int rightcount = node->right_child ?
node->right_child->nodecount : 0;
Двоичные деревья 105
i
i
node->nodecount = leftcount + rightcount + 1;
leftcount = tn->left_child ?
tn->left_child->nodecount : 0;
rightcount = tn->right_child ?
tn->right_child->nodecount : 0;
tn->nodecount = leftcount + rightcount + 1;
node = tn;
i I // Cleanup ( Tree_node * ) удаляет все Tree__nodes в обходе с
■l // отложенной выборкой. Делает реальную работу для -Avl_tree( ).
| void cleanup (Tree_node *tn) {
Г
i
p
f.,
// Специальный случай пустого корня.
if ( tn == 0 ) return;
if ( tn->left_child != 0 ) {
cleanup(tn->left_child);
tn->left_child = 0;
}
J 1 if ( tn->right_ehild != 0 ) {
I , cleanup(tn->right_child);
tn->right_child = 0;
f delete tn;
l> >
i
f // Print_pre( const Tree_node * ) рекурсивно печатает значения
// поддерева с корнем в tn с предварительной выборкой.
void print_pre(const Tree_node * tn) const {
// Специальный случай пустого корня.
if ( tn == 0 ) return;
cout « tn->val « " ";
if ( tn->left_child != 0 ) {
print_pre( tn->left_child );
}
if ( tn->right_child != 0 ) {
print__pre( tn->right_child );
}
)
// Print_in( const Tree_node * ) рекурсивно печатает значения
// поддерева с корнем в tn с порядковой выборкой (сортированные).
void print_in(const Tree_node * tn) const {
706
Глава 3
i
■
с
t
JE
// Специальный случай пустого корня,
if { tn — 0 ) return;
if ( tn->left_child != О ) {
print_in{ tn->left_child );
)
cout « tn->val « " ";
if ( tn->right_child != 0 ) {
print_in( tn->right_child );
}
}
r-
i t // Print_post( const Tree_node * ) рекурсивно печатает значения
i tj // поддерева с корнем в tn с отложенной выборкой.
? ■'
Г 1 void print_post(const Tree_node * tn) const (
// Специальный случай пустого корня,
if ( tn — 0 ) return;
if ( tn->left_child >= 0 ) {
print_jpost( tn->left_child ) ;
}
if ( tn->right_child != 0 ) {
print__post( tn->right_child );
}
cout « tn->val « " ";
}
// Конец закрытых функций Avl_tree.
I 1
t
I
r
V
I
f-.
F
F
i J
""Я
public:
// Открытые функции-элементы Avl_tree:
Avl_tree( ) : zero_node(0) { root = 0; }
Avl_tree(const T root_val ) : zero_node(0) {
maJce_new_root ( root_val ) ;
)
// Освободить память, обойдя все Tree_node с отложенной выборкой.
// Всю работу делает закрытая cleanup( Tree_node * ).
-Avl_tree( ) {
cleanup( root );
}
// Add{ const T ) вставляет в дерево значение, если его еще там
// нет. Возвращает true, если insert_value было действительно
// добавлено. Всю реальную работу делает insert_node(T).
Двоичные деревья
107
к bool add( const T insert value ) {
i
Tree_node *ret = insert_node(insert_yalue);
if (ret) return true;
else return false;
}
// Remove( T ) удаляет из дерева узел со значением value.
// Эту функцию реализовать труднее всего, так как удаляемый
// узел может иметь два дочерних узла. Эти его потомки должны
// быть вновь прикреплены к дереву, но удаляемый узел имеет всего
J ' // одного родителя, так что требуется некоторая "хирургия". Эта
// операция выполняется таким образом, что сохраняется критерий
// двоичного дерева поиска и высота дерева *не* увеличивается.
// Объявление node_to_remove как ссылки на указатель позволяет
// сделать remove( Т ) менее сложной, чем это могло бы быть.
Г
I
h
И
il
. I
!
void remove( Т value ) {
Tree_node * & node_to_remove - find_node( value );
I Tree_node * predecessor = 0;
Tree_node * temp = 0;
int delta balance =0;
I
if ( node_to_remove -- zero_node ) return;
assert( node_to__remove->val == value ) ;
// Сначала обработаем более простые случаи, где node_to_remove
// имеет не более одного потомка.
if ( node to remove->left child " 0 ) {
j | // Node_to_remove не имеет левого потомка,
temp = node_to_remove;
* // Обновить поля nodecount и bal родителя перед тем,
// как начать удаление. В противном случае функции "isa"
//не смогут правильно работать,
if (temp->parent) {
f --(temp->parent->nodecount);
if (temp->isa_left_child( )> {
++(temp->parent->bal);
delta balance = 1;
else if (temp->isa_right_child( )) {
j —(temp->parent->bal);
delta__balance — -1;
[ }
if ( node_to_remove->right_child != 0 ) {
// Node_to__remove имеет правого потомка.
108
Глава 3
f. node_to_remove->right_child->parent = node_to_remove->parent;
>
// Заменить node_to_remove его правым потомком. Если он
// нулевой - неважно. После данного оператора переменная
j_" // node__to__remove больше не ссылается на удаляемый узел.
// На него все еще ссылается temp.
node_to_remove = node_to_remove->right_child;
// Правый потомок node_to_remove теперь занимает его прежнхно
! J // позицию в дереве, nodecount потомка не изменился.
; 1 //Но nodecount его родителя и всех остальных предков
j j // нужно уменьшить на 1, чтобы учесть удаленный узел. Мы
■ // обновим поля nodecount and bal родителя прямо здесь,
* // предоставив adjust_for_remove( ) сделать это для всех
\ // узлов от parent до корня.
' I
ш.
м
I .
i
// После обновления освободить память только что удаленного
//из дерева узла.
adjust_for_remove(temp-->parent, delta_balance) ;
delete temp;
return;
}
else if ( node_to_remove->right_child — 0 ) {
// Node_to_remove не имеет правого потомха.
temp = node_to_remove;
if (temp->parent) {
\ ' —(temp->parent->nodecount);
, j if (temp->isa_left_child( )) {
++(temp->parent->bal);
delta_balance = 1;
}
\ - \ else if (temp->isa_right_child( )) {
, ; --(temp->parent->bal);
,%\l delta balance - -1;
}
}
| // Мы знаем, что левый потомох ненулевой, поскольку находимся
vi //в "блоке else". Следовательно, нет нужды делать проверку
н // перед тем, как выполнить следующий оператор.
node_to_remove->left_child->parent = node_to_remove->parent;
// См. выше замечание о "блохе else".
node_to_remove = node_to_remove->left_child;
J ".j adjust_for_remove(temp->parent, delta_balance) ;
delete temp;
return;
}
Двоичные деревья 109
* •] // Если мы дошли до этой точки, мы знаем, то node_to_remove
ь* • // имеет два дочерних узла. Найти его непосредственного
// предшественника, т.е. самого правого потомка его левого
// дочернего узла.
predecessor = node_to_remove->left_child;
while ( predecessor->right_child != О )
predecessor = predecessor->right_child;
// Заменить значение node_to_remove значением его
// предшественника.
node_to_remove->val = predecessor->val;
// Теперь, когда предшествукяцее значение сдвинулось вверх по
// дереву, мы должны прикрепить левого потомка предшественника к
// его родителю. Он станет правым потомком родителя.
// Вспомните, что predecessor не имеет правого потомка,
// поскольку мы нашли его именно по этому признаку.
I.V
i
Tree_node * рр = predecessor->parent;
if ( рр = node_to_remove ) {
// Специальный случай, когда predecessor является левым
// потомком node_to_remove (т.е. у него нет правого потомка)
pp->left_child = predecessor->left_child;
++(pp->bal);
delta_balance = 1;
if ( predecessor->left_child != 0 )
predecessor->left_child->parent = pp;
}
else if ( predecessor->left_child != 0 ) {
// Predecessor имеет левого потомка, и мы не можем оставить
// его "висящим". Сделать его правым потомком родителя
// predecessor'а.
pp->right_child — predecessor->left_child;
predecessor->left_child->parent = рр;
—(pp->bal);
delta_balance = -1;
}
else {
// Левый потомок предшественника нулевой, поэтому полю его
// родителя right_child не на что укаэыввать.
assert( pp->right_child == predecessor );
pp->right_child = 0;
~{pp->bal);
delta_balance = -1;
)
// Обновить nodecount для predecessor->parent и всех его
// предков. Родительская связка корня равна нулю. Второй
// аргумент adjust_for_remove равен 1, так как это -
// *абсолютное значение* изменения фактора баланса узла рр
// для каждой проведенной выше модификации.
110
Глава 3
г 4
— (pp->nodecount) ;
adjust_forjcemove (pp, delta_balance) ;
// Предшествующее удаленному значение переместилось в другой
// узел, а потомки узла predecessor, если они были, прикреплены
// к его родителю. Нам больше не нужна память, которую исходно
// занимал predecessor.
delete predecessor;
>
// Пройти обратно от start_node до корня, обновляя по пути
// поля nodecount и bal каждого узла начиная с родителя
// start_node. Когда узел оказывается разбалансированным,
// произвести его ротацию.
// Delta_balance - это величина, на которую изменилось поле bal
// узла start__node непосредственно перед вызовом. Не забывайте,
// что родитель корня - нулевой.
void adjust_for_remove ( Tree__node * start_node, int delta_balance ) {
Tree_node * tn • start_node;
int tnJbal_orig = tn->bal;
if { (tn->bal = -2) || (tn->bal ™ 2) )
tn = rotate_for_remove (tn, delta_balance);
delta_balance = tn_bal_orig - tn->bal;
int absdelta = (delta_balance > 0) ? delta_balance : -delta_balance;
Tree__node * tnp = tn->parent;
while ( tnp != 0 ) {
// Обновить поля bal и nodecount узла tnp.
// Если tnp разбалансирован, выполнить соответствующие
// ротации. Функции ротации написаны так, что попутно
Ч // выполняют и эти обновления.
// Обновить nodecount
—(tnp->nodecount);
// Обновить фактор баланса. Сам tn уже обновлен, и мы знаем,
// как изменился его фактор баланса. Если tn - правый потомок
//и его текущий фактор баланса кулевой, то мы уменьшим
// фактор баланса tnp на абсолютное значение изменения
// фактора баланса tn.
// Если tn - левый потомок и его текущий фактор баланса нуль,
//мы увеличим фактор баланса tnp на это значение.
// Заметьте, что если только фактор баланса tn не изменился
//на *нуль*, фактор баланса tnp остается неизменным.
// Если же фактор баланса tnp остался неизменным,
// то же можно сказать и о всех его предках.
//С этого момента мы можем больше не проверять факторы
// баланса и просто уменьшать nodecounts.
1-
f
(
Двоичные деревья 111
П Заметьте, что нам нужно помнить значение tnp->bal
// в случае, если потребуются ротации, чтобы мы могли
// определить новое значение absdelta для следующей итерации.
assert(tn ! = root);
if ( tn->bal = 0 ) {
if ( tn->isa_right_child( )) {
--(tnp->bal);
delta__balance = -1;
}
else {
++(tnp->bal);
delta_balance = 1;
)
}
else {
adjust_nodecount_to_root( tnp->parent, -1 );
return;
}
// Переместиться на ступень вверх для следующей итерации
if ( (tnp->bal > -2) && (tnp->bal < 2) ) {
tn = tnp;
tnp = tn->parent;
■ continue;
■I }
Г // Самая тяжелая работа возложена на этот вызов.
[ i tn = rotate_for__remove (tnp, delta_balance) ;
tnp = tn->parent;
L }
Г
4
У
}
t ] Tree_node * rotate_for_remove( Tree_node * tn, int deltajbalance ) {
; * // Теперь, наконец, самое трудное. Если tnp стал
// разбалансированным, мы должны вновь сбалансировать дерево
// посредством ротаций.
Tree_node * resume_iteration = 0;
Tree node * tnp — tn->parent;
\ \ Hif ( tn->isa_left_child( ) ) (
if ( delta_balance > 0 ) {
// Tn lost a left descendant.
// Запомнить, как выйти на текущего правого потомка tn,
// так как в первом случае, после ротации, он будет тем
// узлом, с которого продолжится восхождение. Во втором
// случае отправной точкой будет левый потомок этого узла.
resume_iteration = tn->right__child;
if ( (tn->bal = 2) &&
112
Глава 3
S:j
ь~
Л 1
Г-i
"А
( (tn->right_child->bal =1) II
(tn->right_child->bal — 0) )
) {
// Обновить факторы баланса значениями после ротации.
,'[_ if (tn->right_child->bal = 1) {
J \ tn->bal = 0;
fc* tn->right_child->bal = 0;
}
$ else {
assert {tn->right__child->bal == 0) ;
tn->bal =1; ~
tn->right_child->bal = -1;
}
if // Одиночная ротация.
Ё • // Хитрая передача tnp no ссылке.
5 ■..■ if ( tn->isa_left_child ( ) )
" "j rotate_left(tn->parent->left__child);
К "j else if ( tn->isa_right_child( ) )
tJ rotate_left(tn->parent->right child);
S".4 else {
it .:!
y- J assert (tn = root);
V* Л rotate_left (root) ;
■"■ (j
fci'J return resume_iteration ;
)
else if ( (tn->bal == 2) &&
(tn->right_child->bal == -1) ) {
// Обновить факторы баланса значениями после ротации.
if (tn->right_child->left_child->bal = 1) {
ъ tn->bal = -1;
!"- • tn->right_child->bal - 0;
"■?! tn->right_child->left_child->bal = 0;
}
else if. (tn->right_child->left_child->bal «* -1) {
f.ifi tn->bal = 0;
* tn->right_child->bal = 1;
[„.j tn->right_child->left_child->bal = 0;
}
else {
assert (tn->right_child->left_child->bal — 0);
tn->bal = 0;
tn->right_child->bal - 0;
tn->right_child->left_child->bal = 0;
}
t // Двойная ротация,
'^j resume__iteration = resume_iteration->left_child;
,i| // Первая ротация.
Чя
rotate_right(tn->right_child) ;
Двоичные деревья £/3
» 1
■
* 1
t:
»
k
\
i '
i 1
// Вторая ротация.
if ( tn->isa_left_child( ) )
rotate_left(tn->parent->left_child);
else if ( tn->isa_right_child( ) )
rotate_left(tn->parent->right_child);
else {
assert(tn = root);
rotate_left(root);
}
return resume iteration;
}
}
// else if ( tn->isa_right_child( ) ) {
else {
assert ( delta_balance < 0 );
// Отражение предыдущих.
// Tn потерял правого потомка.
// Запомнить, как выйти на текущего правого потомка tn,
// так как в первом случае, после ротации, он будет тем
// узлом, с которого'продолжится восхождение. Во втором
// случае отправной точкой будет правый потомок этого узла,
resume_iteration = tn->left_child;
if ( (tn->bal == -2) &£
( <tn->left_child->bal == -1) ||
(tn->left_child->bal -■ 0) )
) {
// Обновить факторы баланса значениями после ротации,
if (tn->l«ft_child->bal — -1) {
tn->bal = 0;
tn->left_child->bal = 0;
}
else (
assert(tn->right_child->bal = 0);
tn->bal к -1;
tn->left_child->bal = 1;
}
// Одиночная ротация.
if ( tn->isa_left_child( ) }
rotate_right(tn->parent->left_child);
else if ( tn->isa_right_child( ) )
rotate_right(tn->parent->right_child);
else {
assert(tn == root);
rotate_right(root);
}
return resume_iteration;
else if ( (tn->bal = -2) ££
114
Глава 3
i
г U
(tn->left child->bal == 1) ) {
!■■-"
^s-' // Обновить факторы баланса значениями после ротации.
| ^ if (tn->left_child->right_child->bal = -1) {
tn->bal = 1;
tn->left_child->bal = 0;
tn->left_child->left_child->bal = 0;
)
else if (tn->left_child->right_child->bal = 1) (
tn->bal - 0;
tn->left_child->bal = -1;
tn->left_child->right_child->bal = 0;
Ц else <
assert(tn->left_child->right_child->bal == 0) ;
tn->bal - 0;
tn->left_child->bal = 0;
tn->left_child->right_child->bal = 0;
>
// Двойная ротация.
resume__iteration = resume__iteration->right_child;
// Первая ротация,
rotate left(tn->left child);
S
=i^ // Вторая ротация.
if ( tn->isa_left_child( ) )
rotate_right(tnp->parent->left_child);
else if ( tn->isa_right_child( ) )
rotate__right(tn->parent->right_child);
else {
assert(tn = root);
rotate_right(root);
}
}
return resume iteration;
}
)
// Управление не должно сюда попадать. Возвратить фиктивное
// значение, чтобы подавить предупреждение компилятора,
assert(false);
return zero node;
// Get_nth( const int ) возвращает п-ое порядковое значение
// дерева.
T get__nth (const int element_num) const {
Tree__node * tn ~ get_nth_node(root, element_num) ;
return tn->val;
}
t
Двоичные деревья 115
II Sizef ) возвращает число узлов дерева.
int size( ) const { return root ? root->nodecount : 0; }
// Find( const T ) returns true if find__value is in the tree,
[ // false otherwise,
j bool find{ T find_value ) {
i j Tree_node *tn = find__node( find_value );
i. I if ( tn != 0 )
return true;
else
return false;
// Print( ) производит обратную порядковую выборку и печатает
// дерево "на боку".
void print ( ) const {
cout « "\n" « "======i===========^====s=====i" « "\n"
1 ' « endl;
E ■
9 II Это вызов Avl_tree: :Tree__node: : print ( ) , a
| * // *не* рекурсивный вызов Avl__tree: : print ( ) .
■ i root->print( ) ;
\ >
\ II Следующие функции печатают дерево в линейной последовательности
, i // при обходе с предварительной, порядковой и отложенной выборкой.
* ■ // Print_inorder( ) эквивалентна сортировке. Эти функции не имеют
■ // параметров, но вызывают рекурсивные закрытые функции, принимающие
■ I // параметры типа Tree^node *, чтобы те выполняли всю работу.
void print_pre_order( ) const {
I print__pre (root) ;
j cout « endl;
I
void print_in_order{ ) const {
print_in(root);
cout « endl;
)
void print_j?ost_order ( ) const {
print_post(root);
cout « endl;
}
.Jil
Avl.cpp заполняет дерево целыми от 1 до 7 в восходящем порядке. В
обычном дереве поиска это дало бы перекошенное дерево, показанное на рис. 3.2Ь.
Однако механизмы балансировки avl.h генерируют вместо этого дерево, пока-
116
Глава 3
занное на рис. 3.2а. Затем элементы 1, 2 и 3 удаляются. И снова механизмы
avl.h поддерживают дерево в сбалансированном состоянии, в то время как
простой алгоритм дал бы перекошенный результат. В скобках после счетчика
узлов печатается также фактор баланса.
// Файл avl.cpp: Примеры использования для avl.h
«include "avl.h"
// Создать сбалансированное двоичное дерево поиска с целыми числами.
// Изначально дерево пустое.
Avl_tree<int> my_avlt;
// Заполнить my_avlt целыми значениями. Ввести их
//в восходящем порядке, что является наихудшей из возможных
// последовательность для простого двоичного дерева поиска.
jvoid populate( ) {
my_avlt.add( 1 )
my_avlt.add( 2 )
5 my_avlt.add( 3 )
j my_avlt.add( 4 )
i my_avlt.add( 5 )
J my_avlt.add( 6 )
my_avlt.add( 7 )
}
|int main( )
_{
4 populate( ) ;
i
I ] // Напечатать все дерево. Оно должно быть хорошо сбалансировано,
// несмотря на попытку populate( ) сделать его перекошенным.
my_avlt.print ( );
// Уделить значения толко из левого поддерева - еще одна жалкая
// попытка перекосить дерево.
my_avlt.remove ( 1 )
my_avlt.remove ( 2 )
my_avlt.remove ( 3 )
4 j // Снова напечатать все дерево,
ь my_avlt.print ( );
г
i return 0;
>- ')
Вывод программы avl.cpp:
8
7(1/0)
@
6(3/0)
Двоичные деревья 117
8
5(1/0)
@
4(7/0)
@
3(1/0)
е "~~
2(3/0)
в
1(1/0)
0
е
7(1/0)
@
6(4/-1)
@
5(1/0)
в
4(2/1)
@
ПРИМЕЧАНИЯ
\n?v
Организация avl.h очень похожа на организацию bt.h. Но в дополнение к
имеющимся там функциям здесь введены закрытые статические элементы го-
tate_left(Tree_node *&) и rotate_right(Tree_node *&). Эти вспомогательные
функции реализуют операции ротаций, описанные выше. Они вызываются из
функций adjust__for_add(Tree_node *) и adjust_for_remove(Tree_node *).
(ЗАЬ
ЗАМЕЧАНИЕ ПРОГРАММИСТА
В классе нет явной функции для двойных ротаций, которая выполняла
бы их за один шаг. Для простоты двойные ротации осуществляются
путем композиции двух одиночных. Специальные функции для двух
видов двойных ротаций улучшили бы производительность благодаря
сокращению числа вызовов и присваиваний, необходимых для
реструктурирования дерева.
Add(T) и remove(T) модифицированы весьма незначительно. Они теперь
вызывают adjust_for_add(Tree_node *) и adjust_for_remove(Tree_node *) для
балансировки дерева. Эти две процедуры обновления узлов делают основную
работу по балансировке. Принцип их работы состоит в движении вверх по
дереву к корню с корректировкой фактора баланса встреченных по пути узлов и
их ротациями. Функции adjust_for_add(Tree_node *) нужно пройти вверх до
первого несбалансированного узла. В этой точке она производит
корректировку и далее только обновляет счетчики узлов, так как дальнейшего
реструктурирования не требуется. Однако функции adjust_for_remove(Tree__node *)
118
Глава 3
придется несладко. Она должна производить корректировку на всем пути
вплоть до самого корня. Ниже приводится краткая схема работы обеих
функций; детали вы найдете в комментариях к исходному коду.
Adjust_for_add(Tree_node *) состоит из двух частей. Первая часть
проходит вверх по дереву от вновь прикрепленного листа и делает довольно
очевидные корректировки счетчика и фактора баланса для встреченных по пути
узлов. Эту задачу выполняет код в начале функции и цикл while. Флаг
bal_was_changed служит для индикации того, изменился ли фактор баланса
родителя нового узла в результате вставки последнего. Эта информация
помогает определить, какие корректировки требуются для фактора баланса
прародителя (т. е. родителя родителя). По мере того, как мы двигаемся вверх,
bal_was_changed ссылается на все более высокие узлы.
Помимо этой работы, начальный цикл while все время проверяет, нужно ли
произвести ротацию. Булева переменная rotate_flag, начальное значение
которой false, помогает принять это решение. Как только мы обнаружим узел,
фактор баланса которого стал равен 2 или -2, мы знаем, что требуется
ротация, и устанавливаем флаг в true. Это будет также означать, что мы можем
далее не проверять факторы баланса после того, как выполним ротацию.
Поэтому мы прерываем цикл после установки этого флага.
Вторая часть adjust_for_add(Tree_node *) выясняет, какого рода ротацию
нужно произвести, выполняет ее и делает окончательные корректировки
факторов баланса. (Код этого раздела функции растянут и содержит повторения.
По существу он выделяет различные варианты конфигурации поддерева и
делает для каждого из них соответствующую коррекцию. Как только вы
поймете, что половина этих вариантов является зеркальным отражением другой
половины, проследить работу кода будет гораздо проще.) Последней задачей
будет обновление счетчиков узлов по пути от текущего узла к корню. Это нужно
сделать, хотя обновлять факторы баланса уже не требуется. Задача решается
вызовом adjust_nodecount_to_root(Tree_node *, int).
Adjust_for__remove(Tree_node *) отличается по структуре от adjust_for_add
(Tree_node *). Вместо того, чтобы прервать главный цикл в тот момент, когда
обнаружена необходимость ротации, функция остается в цикле. Однако
возможность прервать цикл все же остается. Если нам встретится узел, фактор
баланса которого не изменился, мы можем быт уверены, что и у всех его
предков баланс тоже остался тем же самым. Простое (но неочевидное) обоснование
этого условия проводится в комментариях к исходному коду.
Обратите внимание, что растянутый анализ различных случаев на предмет
того, какого рода ротацию требуется произвести, выделен в самостоятельную
функцию rotate_for_remove(Tree_node *, int). Это сделано потому, что такой
анализ проводится в двух различных местах функции adjust_for_reinove
(Tree_node *). rotate_for_remove(Tree_node *, int) возвращает также Tree_node *,
указывающий на узел, который вызывающая функция должна будет
использовать на следующей итерации. Это еще больше упрощает структуру ad-
just_for_remove(Tree_node *). Наконец, заметьте, что половина
анализируемых в rotate_for_remove(Tree_node *, int) вариантов является зеркальным
отражением другой половины.
Имейте в виду, что целью данной реализации не ставилось достижение
максимальной эффективности. Функции корректировки написаны так, чтобы достичь
больше ясности путем явного рассмотрения различных вариантов, возможных
при балансировке дерева. Их эффективность, безусловно, может быть улучшена.
ГЛАВА
h "V
h. **
i* * « *- - • ■ -. \ • , - ]
n „ > j-
" * -л v' ?- r -i" ?•
i 'i
Ал*
Al П
массивь
\
hashj.h
hashj.cpp
hashj.h
hash_2.cpp
Арт Фридман
1d.h
Id.cpp
2d.h
2d.cpp
■i.3*#^
Б* " ***
120
Глава 4
В этой главе рассматриваются два интересных предмета из области
программирования: смешанные таблицы (hash tables) и разреженные
массивы (sparse arrays). Их способность оптимизировать доступ к данным
некоторых видов делает их весьма полезными в целом ряде ситуаций. Хотя и те, и
другие концептуально просты, при их программировании встает ряд довольно
интригующих вопросов.
Введение в смешанные таблицы
и разреженные массивы
Термин поиск, грубо говоря, относится к любой методике, основным
принципом которой является отыскание нужной записи путем сравнения
известного ключевого значения с ключами каждого элемента структуры данных. Когда
найден совпадающий ключ, можно извлечь запись. Двоичные деревья поиска
из главы 3 позволяют эффективно проводить поиск на сортированных данных,
ограничивая необходимое число сравнений ключей.
В противоположность этому записи, хранящиеся в массиве (чей индекс
является аналогом ключа), могут быть отысканы очень быстро и без проведения
каких-либо сравнений. Если известен ключ (т. е. индекс массива), размер
записи и начальный адрес массива, мы вычисляем адрес нужной записи с
помощью одного умножения и одного сложения. Если массив дает нам такие
преимущества, почему бы нам вообще не забыть о поиске и не пользоваться
исключительно массивами? Простой (и несколько надуманный) пример даст
ответ на этот вопрос.
Предположим, мы хотим хранить записи с ключевыми значениями 3,
10 000 000 и 60 000 000. Хранение этих записей в массиве, индексированном
ключевыми значениями, потребовало бы массива из 60-ти миллионов элементов,
почти все из которых были бы пусты. Это явно неудачная мысль. Фактически
любая из методик поиска справилась бы с этой вымышленной ситуацией лучше.
Естественно, проблема становится интереснее, когда нам нужно хранить
значительное число записей. Допустим, вместо трех записей у нас есть
полмиллиона, а диапазон индексов остался в пределах от 1 до 60 000 000. Наш
гипотетический массив из 60-ти миллионов элементов все еще на 99% пуст, — но теперь
нам придется рассмотреть имеющиеся альтернативы более осторожно. Мы не
можем просто махнуть рукой на задачу поиска среди полумиллиона элементов.
Смешанные таблицы и разреженные массивы пытаются разрешить конфликт
путем компромисса. Обе эти методики сочетают манипуляцию индексами с
поиском. Смешанные таблицы, насколько возможно, акцентируют вычисление
индексов; разреженные массивы больше склоняются в сторону поиска.
Проектирование смешанной таблицы
Смешанные таблицы часто реализуют в виде массива фиксированного
размера, содержащего либо элементы, которые он будет хранить, либо указатели
на них. Вместо того, чтобы использовать в качестве индекса массива
непосредственно ключевое значение, мы вычисляем индекс с помощью функции
подстановки (hash function)^ которая принимает ключевое значение в качестве
Смешанные таблицы и разреженные массивы
121
параметра. Функция подстановки гарантирует, что возвращаемый ею индекс
будет находиться в пределах фиксированных границ массива. Она, кроме того,
пытается вычислять индексы таким образом, что они будут различными для
различных ключей.
В идеале функция подстановки никогда не должна возвращать один и тот
же индекс для двух различных ключевых значений. На практике этот идеал
достичь очень трудно (чаща всего невозможно). Когда функция возвращает
одинаковые значения индекса для нескольких ключей, говорят, что
происходит столкновение последних. Каждый из ключей хочет занять ту же самую
позицию в таблице, но место есть только для одного. Существуют две
критических характеристики, отличающих хорошие функции подстановки от плохих.
Одна из них — скорость; функции подстановки должны быть очен!* быстрыми.
Другая характеристика — равномерное распределение индексов по массиву,
что снижает вероятность столкновений.
Если подходить к делу реалистически, нужно предположить, что
столкновения будут случаться. То, как таблица будет справляться с проблемой
столкновений, является важным аспектом ее проектирования. Примеры
смешанных таблиц в этой главе контролируют столкновения путем сцепления. Это
значит, что каждый элемент, претендующий на данный индекс, будет
присоединяться к связанному списку элементов, имеющих тот же индекс. Эти
элементы не располагаются в каком-то определенном порядке; вместе с
элементом данных хранится также ассоциированный с ним ключ.
Отыскивая некоторый ключ, мы прежде всего применяем функцию
подстановки и вычисляем индекс массива, а затем производим просмотр
соответствующего связанного списка на предмет поиска совпадающего ключа.
Хранение вместе с данными ключа позволяет хранить повторяющиеся данные, но
сами ключи должны быть уникальными. Смешанные таблицы не понимают
таких отношений, как «меньше», и нет смысла говорить о «предшествующем
значении». Подобным же образом не смысла писать для смешанной таблицы
итератор. Рис. 4.1 показывает, как выглядит расположение смешанной
таблицы в памяти.
Рис. 4.1.
Расположение
смешанной таблицы
в памяти
a. Схема ячейки
b. Схема таблицы
Ь:
/
•-
».
►
►
11
33
49
0
а:
Date
Key
Next
51
•/
48
0
Проектирование обобщенной смешанной таблицы — хитрая задача. Чтобы
понять, почему, рассмотрите прямолинейный подход, который требует, чтобы
подставляемый тип ключа предусматривал функцию-элемент hash(),
возвращающую подходящее целое значение. Наиболее очевидным недостатком
такого подхода является то, что, поскольку нет встроенной операции hash(), «обоб-
122
Глава 4
щенная» смешанная таблица не смогла бы обрабатывать встроенные типы
вроде int или char. Кроме того, откуда эта hash() могла бы знать, каков
допустимый диапазон индексов, которые она может возвращать? Другими словами,
если объект в контейнере должен слишком много знать о реализации своего
контейнера, то контейнер не приспособлен к тому, чтобы быть обобщенным.
Мы не будем пытаться разрешить эти трудные вопросы проектирования, а
приведем некоторые примеры, которые, хотя и не являются по-настоящему
обобщенными, все же могут применяться достаточно широко. Каждая из двух
представленных смешанных таблиц основана на использовании
специфического типа ключа и предусматривает собственную функцию подстановки. Это
разумный подход, так как смешанные таблицы часто требуют специальной
настройки. По всей вероятности, вам придется подгонять таблицы, если вы
захотите приспособить их для своих собственных нужд.
Простая смешанная таблица с целыми ключами
Первый наш пример имеет ключи типа int. Вводя объект в таблицу, вы
должны явным образом ассоциировать его с уникальным целым ключом. На
первый взгляд, это неудачная мысль — возлагать на пользователя такую
задачу. Может быть, для обеспечения уникальности ключа ему придется
поддерживать еще ивторую таблицу? Не придется, потому что мы определили
функцию add(const T&, int), возвращающую false, если ключ повторяется, и true,
если все в порядке.
В некоторых ситуациях целые ключи оказываются очень простым и
естественным представлением. Отладчик, например, должен уметь находить
символическое имя по данному адресу или строку исходного кода по ее номеру. В
таких случаях разработчику не приходится делать что-то особенное, чтобы
генерировать ключи или обеспечить их уникальность.
Код
Код примера содержится в файлах hash_l.h и hashl.cpp. Вот hash_l.h:
///////////////////////////////////////////////////////
//
// Файл hash_l.h
// Шаблон прострой смешанной таблицы с целыми ключами. В
// конструкторе пользователь должен указать специальный объект
// типа Т, который будет служить в качестве индикатора
// "не найдено", возвращаемого find(int) в случае, когда
// она пытается отыскать объект, отсутствующий в таблице.
// Пользователь указывает также размер массива для реализации
// таблицы. Число элементов в таблице не имеет какого-либо
// предопределенного предела.
// Add(const T &, int) обнаруживает повторяющиеся ключи
// и отказывается включать их в таблицу. Повторение *данных*,
// однако, допускается.
//
// Т должен поддерживать копирование. Поддержка operator«{ )
Смешанные таблицы и разреженные массивы 123
t // требуется для печати.
к //
///////////////////////////////////////////////////////
Е- i#include <iostream>
['fusing namespace std;
template<class T>
class Hash_table {
private:
// Закрытая struct cell для внутреннего хранения
// данных и ключей.
struct cell {
const T& data;
int key;
cell * next;
cell (const T& cell__data, int cell_key,
cell * cell_next ■ 0)
: data(cell_data), key(cell_key), next(cell_next) { }
};
// Внутренний массив таблицы. Каждый элемент указывает
// на (возможно пустой) список ячеек.
cell ^table-
unsigned int table_size;
// T_notfound - специальное значение типа Т, передаваемое
// конструктору пользователем. Когда find(int) возвращает
// данное значение, это означает, что искомый ключ в
// таблице отсутствует.
Т T__notfound;
// Hash(int) вычисляет значение простейшей подстановки.
// Она может подойти, а может и не подойти для вашей задачи.
// Функции подстановки должны быть просты (и быстры),
// насколько возможно, отвечая, тем не менее, основному
// своему назначению.
unsigned int hash(int key) {
return unsigned(key % table_size);
}
' i
-i' i
1 j // Find_cell( ) возвращает указатель на ячейку, содержащую
■* *1 // key. Возвращает нуль, если ключ не найден.
и ч
"г Ч
I I
5 j
hi
cell * find_cell(int key) {
unsigned int slot = hash(key);
for (cell *cp = table[slot]; cp != 0; cp = cp->next) {
if (cp->key == key) return cp;
124
м >
- j return cp;
: i }
*. "\ II Присваивание и копирование не поддерживается.
Ъ\ Hash_table (const Hash_table &) ;
\ j Hash_table & operator=(const Hash_table &);
jpublic:
г i
■ ■ // Конструктор выделяет память под таблицу
t 1 //и инициализирует T_notfound.
п
Hash_table(unsigned int size, const T& notfound)
: table_size(size), T_notfound(notfound)
{
table = new cell*[table_size];
for (int i = 0; i < size; table[i++] = 0);
■; )
Ь 1
■I
V*
. J
// Деструктор удаляет все ячейки, затем удаляет
// саму таблицу.
~Hash_table( )
{
for (int i = 0; i < table_size; i++) {
cell *cp, *cp_next;
if ((cp = table[ij) = 0) continue;
else cp_next = cp->next;
// Table [i] is non-empty.
while (true) (
delete cp;
, if (cp_next == 0) break;
\f з cp = cp_next;
• jl cp_next = cp_next->next;
■ i }
И >
* !", delete [] table;
; : )
\ i
i i
|"J // ввести элемент в таблицу, если только его ключ не
J- i // обнаружен в таблице. Вызов find_cell(int)
// удобен и облегчает сопровождение, но дублирует
// вызов hash(int). Если добавление элементов должно
£;. " // быть высокоэффективным, поиск ключа должен быть
fc-J // закодирован здесь "вручную". Элементы вставляются
■#И // в начало списка, чтобы избежать прохода по нему.
Уя // Если почему-то важно, чтобы они присоединялись в конец,
1м // потребуется чуть более сложная схема поддержания
!£я // хвостового указателя, работающая достаточно быстро.
Смешанные таблицы и разреженные массивы 125
' ] // Возвращает true, если элемент успешно размещен,
'*1 // false в противном случае.
Н
I
м
t
II
bool add(const T& item, int key) {
if (find_cell (key) != 0) return falser-
unsigned int slot = hash(key);
if (table[slot] =» 0)
table[slot] - new cell(item, key);
else {
table[slot] = new cell(item, key, table[slot]);
}
return true;
// Удалить пункт таблицы с ключом key. Для удаления
// нужна переменная - указатель на предыдущую ячейку,
// так как список односвязный.
void remove(int key) (
unsigned int slot = hash(key);
cell * cp_prev = table[slot];
if (cp_prev = 0) return;
// Специальный случай для первого элемента,
if (cp_prev->key == key) {
table[slot] = cp_prev->next;
delete cp_prev;
return;
>
for (cell * cp = cp_prev->next;
cp '= 0;
cp_prev = cp_prev->next, cp = cp->next)
{
if (cp->key == key) {
cp_j?rev->next = cp->next;
delete cp;
return;
}
}
// Возвращает элемент, соответствующий key. Если
// этого элемента нет в таблице, возвратить специальное
// значение T_notfound, переданный пользователем.
const Tfi find(int key) {
cell * cp = f ind__cell (key) ;
return (cp ? cp->data : T_notfound);
>
Глава 4
I/ Функция print( ) для отладки. Пригодна только для
// небольших таблиц.
void print( ) {
cout « endl;
cout « "slot #" « endl « " " « endl;
for (unsigned int index = 0; index < table_size; ++index)
// Print a row of the table.
cout « index « ":
for (cell *cp - table[index]; cp != 0; cp = cp->next) {
cout « '[' « cp->data « ',' « cp->key « "] ";
}
cout « endl;
}
cout « endl;
};
А это hash_l.cpp:
#include "hash_l.h"
// Создать смешанную таблицу для строк с пятью
// позициями, используя пустую строку как специальное
// значение. Для ключей Hash_l.h использует целые.
Hash_table<char *> my_hash(5,"");
int main( )
I // Ввести несколько значений.
my_hash.add("abc", 99);
' my_hash.add("abc", 100)
I my_hash.add("def", 100)
1 my_hash.add("ghi", 101)
// Дублирование данных допустимо.
// Повторяющийся ключ игнорируется
х II Напечатать таблицу.
my__hash. print ( ) ;
i
i // Отыскать некоторые значения, идентифицированные ключами.
Л cout « "my_hash.find(99) : " « my_hash. find(99) « endl;
\ cout « "my_hash.find(100): " « my_hash.find(100) « endl;
4 cout « "my_hash.find(101): " « my_hash.find(101) « endl;
cout « "my_hash.find(66): " « my_hash.find(66) « endl;
1
* II Удалить идентифицированные ключами элементы.
my_hash.remove(99);
^ my_hash.remove(100);
my_hash.remove(101);
my_hash.remove(5); // Ключ игнорирован - нет в таблице.
// Ввести 25 идентичных значений, каждое со своим ключом.
for (int i - 0; i < 25; ++i) {
Смешанные таблицы и разреженные массивы ^_ 127
my__hash.add("xyz" , i) ;
}
// Снова напечатать таблицу.
my__hash. print ( ) ;
return 0;
Вывод hash_l.cpp имеет следующий вид:
slot #
[abc,100]
[ghi,101]
[abc,99]
my_hash.£ind(99): abc
my_hash.find(100): abc
my_hash.find<101): 9ni
my_hash.find(66) :
slot #
[xyz,20] [xyz,15] [xyz,10] [xyz,5] [xyz,0]
[xyz,21] [xyz,16] [xyzrll] [xyz,6] [xyz,l]
[xyz,22] [xyz,17] [xyz,12] [xyz,7] [xyz,2]
[xyz,23] [xyz,18] [xyz,13] [xyz,8] [xyz,3]
[xyz,24] [xyz,19] [xyz,14] [xyz,9] [xyz,4]
I npk
ПРИМЕЧАНИЯ
Шаблон template<class T> Hashtable принимает параметр Т, который
должен обеспечивать копирование. Если в целях настройки или отладки вы хотите
печатать небольшие таблицы, то тип Т должен также определять operator«().
Hash_table содержит вложенную закрытую структуру с именем cell,
которая является типом для сцепления данных друг с другом. Каждая cell-ячейка
содержит объект типа Т, ассоциированный с ним целый ключ и указатель на
следующую ячейку. Закрытый элемент данных table объявляется как cell**.
Это позволяет использовать его подобно массиву элементов cell*, и от нас не
требуется указывать его размер заранее. Каждый элемент массива является
заголовком связанного списка cell-ячеек. В результате Hash_table может
хранить произвольное число элементов, хотя размер массива и фиксирован. Tab-
le_size представляет этот размер. Пользователь задает его, передавая
конструктору Hash_table(int, const T&), который выделяет массив указателей на
cell и инициализирует его нулями. Закрытый элемент данных T_notfound —
это специальное значение типа Т, также специфицируемое пользователем в
конструкторе.
128
Глава 4
Find(irit) является открытой функцией-элементом, возвращающей объект
типа Т для данного ключа. Если ключ, переданный аргументом, в таблице
отсутствует, возвращается значение T_notfound. Функция вызывает закрытую
find_cell(int), которая и выполняет большую часть работы.
Add(const T&, int) проверяет, нет ли уже в таблице ключа,
специфицированного в вызове. Если такой ключ есть, функция просто возвращает false.
В противном случае она вызывает закрытую функцию-элемент hash(int) для
определения элемента массива table для данного ключа, инициализирует
новую ячейку, присоединяет ее к голове связанного списка и возвращает true.
( ЗАМЕЧАНИЕ ПРОГРАММИСТА
Для add(const T&, int) удобно определять, не является ли ключ
дубликатом, вызывая find_cell(int). Заметьте, однако, что при этом
лишний раз вызывается hash (int). Такие дополнительные расходы не
вызваны никакой необходимостью, и если эффективность add(const T&,
int) является критической, она должна производить проверку на
дублирование не вызывая findjcell(int).
Remove(int) находит ячейку, соответствующую аргументу-ключу, и
удаляет ее. Если такой ячейки нет, ничего не происходит. Заметьте, что remove(int)
должна вновь инициализировать элемент массива table нулем, если она
удаляет последнюю ячейку в связанном списке этого элемента. Это кодируется как
специальный случай. Деструктор ~Hash_table() удаляет все ячейки таблицы и
затем саму таблицу table. Print() позволяет распечатать таблицу в целях
отладки; она полезна только в случае небольших таблиц. Обратите внимание,
что копирование и присваивание не поддерживаются.
Перед тем как завершить наше исследование hashl.h, поближе
рассмотрим функцию hash (int). Она просто возвращает остаток от деления своего
аргумента на table_size. Это простейшая из возможных работоспособная
функция, и она удовлетворяет требованиям скорости. Но обеспечивает ли она
равномерность распределения подстановок по массиву? Как вы уже, вероятно,
догадались, это зависит от самих ключей. Если ключи распределены случайным
образом, эта функция не хуже любой другой. Но обстоятельства иногда
складываются не в пользу случайного их распределения.
Например, на многих системах адреса с большой вероятностью будут кратны
четырем. Если такие адреса используются в качестве ключей и table_size
кратен четырем, большинство элементов массива таблицы останутся пустыми —
несомненно, плохой результат. Если вам известно данное обстоятельство, вы
можете свести его на нет, заставив hash() сдвигать свой аргумент вправо на
2 бита перед вычислением остатка. Но могут существовать и другие факторы,
препятствующие случайному поведению ключей. Вообще говоря, выбор в
качестве tab_size простого числа помогает минимизировать подобные факторы.
Если бы в приведенном примере размер таблицы был равен 5, а не 4, никакой из
его элементов не был бы исключен из распределения. Может быть, стоит даже
модифицировать конструктор Hash_table(int, const T&) так, чтобы он
использовал для размера таблицы не свой аргумент, а наименьшее простое число,
большее или равное ему. Тестирование и уточнение подстановочной функции и
размера таблицы наверняка принесет выигрыш почти для любой таблицы.
Смешанные таблицы и разреженные массивы 129
Смешанная таблица с ключами типа char *
Второй наш пример использует ключи типа char*. Добавляя объект в
таблицу, вы должны явным образом ассоциировать с ним уникальный строковый
ключ. Для любого приложения, работающего с таблицей символов, это может
оказаться полезным. Вообще говоря, таблица символов предоставляет
приложению способ поиска информации о символе, например, адресе, по его
строковому представлению. Смешанная таблица с ключами char* — естественный
способ реализации такого механизма.
Другой особенностью данной смешанной таблицы является массив, размер
которого может увеличиваться по мере добавления значений. Когда средняя
длина связанных списков становится слишком большой, размер массива
удваивается. Здесь также выбор наименьшего простого числа, большего
удвоенного старого размера, может дать лучшую эффективность. Значение 5 для
максимальной средней длины списка было выбрано произвольно и также
может быть скорректировано для наилучшей производительности. А возможны
и совершенно иные эвристики.
Реализация смешанной таблицы предполагает, что строковые ключи
остаются стабильными, т. е. указатели будут ссылаться на одни и те же
неизменяемые строки на протяжении всего времени жизни таблицы. Если такое
предположение представляется сомнительным, таблицу нужно
модифицировать так, чтобы она делала собственную копию для каждой ключевой строки и
удаляла бы ее при удалении соответствующей ячейки или уничтожении
таблицы. Предложения, как это можно сделать, вы найдете в комментариях.
Код
Код для таблицы со строковыми ключами содержится в файлах hash_2.h и
hash_2.cpp. Вот hash_2.h:
// Файл hash_2.h
// Шаблон простой смешанной табшицы с ключами типа char *.
// В конструкторе пользователь должен указать специальный
// объект типа Т, который будет служить индикатором
* // "не найдено", возвращаемым функцией find(char *),
// когда она пытается отыскать элемент, которого нет в
// таблице. Пользователь указывает также размер массива,
// используемого для реализации таблицы. Число пунктов
// таблицы не имеет предопределенного предела.
I. , // Add (const T &, char *) обнаруживает повторяющиеся ключи
// и отказывается включать их в таблицу. Дублирование
// *данных* допускается.
//
// Т должен поддерживать копирование. Для печати необходима
// поддержка operator«( ).
i //
I 1"
•i///////////////////////////////////////////////////////
5 Зек. 1208
130 Глава 4
#include <string>
#include <iostream>
using namespace std;
f
к-J
t
I
fc
■Г
■f
template<class T>
class Hash_table {
private:
// Закрытая struct cell для внутреннего хранения данных
// и ключей.
struct cell {
const Tfi data;
char * key;
cell * next;
cell(const T& cell_data, char * cell_key,
cell * cell_next = 0)
: data(cell_data), key(celljkey), next(cell_next) { }
};
// Внутренний массив таблицы. Каждый элемент указывает
// на (возможно пустой) список ячеек.
cell *♦tables-
unsigned int table_size;
unsigned int n__entries;
// T_notfound - специальное значение типа Т, передаваемое
// конструктору пользователем. Когда find(char *)
// возвращает данное значение, это означает, что искомый
// ключ в таблице отсутствует.
Т T_notfound;
// Hash(char *) вычисляет простую функцию подстановки.
unsigned int hash(char* key) {
unsigned int return_value = 0;
for (char * cp = key; *cp != 0; ++cp) {
return_yalue += *cp;
}
return unsigned(return_value % table_size);
}
// Find_cell( ) возвращает указатель на ячейку, содержащую
// key. Возвращает нуль, если ключ не найден.
// Производит сравнение строк, а не указателей.
cell * find_cell(char * key) {
unsigned int slot = hash(key);
for (cell *cp = tablefslot]; cp != 0; cp = cp->next) {
if (strcmp(cp->key, key) = 0) return cp;
}
Смешанные таблицы и разреженные массивы
131
return cp;
}
// Присваивание и копирование не поддерживаются.
Hash_table( const Hash_table £ );
£ Ц Hash table £ operator^( const Hash table & );
P/ J ~ ~"
void destroy__table (cell ** dead_table,
unsigned int dead_table_size)
{
for (int i = 0; i < dead_table_size; i++) {
cell *cp, *cp_next;
if ( (cp = dead_table[i]) — 0) continue;
else cp_next = cp->next;
// Dead_table[i] is non-empty,
while (true) {
delete cp;
if (cp_next =3= 0) break;
cp - cp__next;
cp_next = cp_next->next;
}
}
delete [] dead_table;
}
void expand_table( )
{
// Сначала создать указатель на старую (существующую)
// таблицу.
cell ** old_table = table;
unsigned int old_table_size - table_size;
// Организовать новую таблицу.
table_size *= 2;
n_entries =0;
table = new cell*[table_size];
for (int i = 0; i < table_size; table[i++] = 0);
// Включить каждый из старых элементов в новую таблицу.
// Обход, аналогичный destroy_table( ).
for (int j = 0; j < old_table_size; j++) {
cell *cp, *cp_next;
if ( (cp = old__table[j]) == 0) continue;
else cp_next = cp->next;
// 01d_table[j] is non-empty,
while (true) {
132
Глава 4
Е?. j add(cp->data, ср->кеу);
к.1 if (cp_next == 0) break;
;-':! ср — cp_next;
•■ -3 cp_next — cp_next->next;
}
)
*i // Наконец, уничтожить старую таблицу. В целях
IH
1:1
j // увеличения эффективности эти удаления можно сделать
* // "вручную" в предыдущем цикле.
}
destroy_table(old_table, old_table_size);
Ь '(public:
И
. ■ .1 // Кг
* ■
г-
(-.
» Л
// Конструктор выделяет память под таблицу
// и инициализирует T_notfound.
Hash_table( unsigned int size, const T& notfound)
: table_size(size), T_notfound(notfound), n_entries(0)
{
table = new cell*[table_size];
for (int i ~ 0; i < size; table[i++] = 0);
}
*t't П Деструктор удаляет все ячейки, затем удаляет
V -Я // саму таблицу.
иг. 'Щ
-Hash_table( ) { destroy_table(table, table_size ); }
M
Щ
1,/\ If Ввести элемент в таблицу, если только его ключ не
■ • j // обнаружен в таблице. Вызов find_cell(int)
// удобен и облегчает сопровождение, но дублирует
// вызов hash(int). Если добавление элементов должно
£-ъд // быть высокоэффективным, поиск ключа должен быть
// закодирован здесь "вручную". Элементы вставляются
// в начало списка, чтобы избежать прохода по нему.
// Если почему-то важно, чтобы они присоединялись в конец,
// потребуется чуть более сложная схема поддержания
// хвостового указателя, работающая достаточно быстро.
// Возвращает true, если элемент успешно размещен,
// false в противном случае.
// Обратите внимание, что строковые ключи не копируются.
//Мы полагаем, что эти указатели на char будут
// продолжать ссылаться на те же самые строки на
// протяжении всего времени жизни смешанной таблицы.
\.у- bool add (const Tfi item, char * key) {
if ( find cell(key) '= 0 ) return false;
Смешанные таблицы и разреженные массивы
133
// Проверить, не превосходит ли средняя длина связанного
// списка пяти. Если превосходит, выделить новую таблицу
// с удвоенным числом позиций и уничтожить старую,
if ( (n_entries / table__size) >= 5 ) expand_table ( ) ;
// Новая ячейка инициализируется адресом ключевой строки.
// Если есть сомнения относительно того, что эти
// указатели будут стабильны (т.е. будут продолжать
// указывать на ту же строку), скачала должна быть
// сделана внутренняя копия. В этом случае деструктор
// ~cell( ) должен быть переписан, чтобы эти строки
// удалялись.
unsigned int slot = hash(key);
if (table[slot] « 0)
table[slot] = new cell(item, key);
else {
table[slot] = new cell(item, key, table[slot]);
}
++n_entries;
return true;
// Удалить пункт таблицы с ключом key. Для удаления
// нужна переменная - указатель на предыдущую ячейку,
// так как список односвязный.
void remove(char * key) {
unsigned int slot = hash(key);
cell * cp_prev = table[slot];
if ( cp_prev = 0 ) return;
// Special case for first item.
if (strcmp(cp_j>rev->key, key) == 0) {
table[slot] = cp_prev->next;
delete cp_prev;
—n_entries;
return;
}
for (cell * cp = cp_prev->next;
cp != 0;
cp_prev = cp_prev->next, cp = cp->next)
{
if (strcmp(cp->key, key) = 0) {
cp_prev->next = cp->next;
delete cp;
—n_entries;
return;
}
}
}
134
Глава 4
и
\
i. )
Г*
V
I*-1
1 *
// Возвращает элемент, соответствующий key. Если
// этого элемента нет в таблице, возвратить специальное
// значение T^notfound, переданное пользователем.
const T£ find(char * key) {
cell * cp = find_cell(key);
return (cp ? cp->data : T notfound);
// Функция print( ) для отладки. Пригодна только для
// небольших таблиц.
void print( ) {
» cout « endl;
cout « "slot #" « endl « " " « endl;
for (unsigned int index = 0; index < table_size; ++index) (
// Print a row of the table,
cout « index « ": ";
for ( cell *cp = table[index]; cp != 0; cp — cp->next ) {
cout « ■[' « cp->data « *,' « cp->key « "] ";
>
cout « endl;
Г i У
p. j cout « endl;
el:
А это файл hash_2.cpp:
|#include "hash_2.h"
// Создать смешанную таблицу для целых с одной позицией
// и ключами типа char *; специальное значение равно -1.
!// Число позиций увеличивается автоматически по мере
!// добавления элементов в таблицу.
Hash_table<int> my_hash(l,-l);
[int main( )
{
my_hash.add(l, "a")
my_hash.add(l, "b")
my_hash.add(l, "c")
my_nash.add(l, "d")
my_hash.add(l, "e")
my_hash. print ( ) ;
my_hash.add(l, "f") ;
my hash.print( ) ;
Смешанные таблицы и разреженные массивы
135
* \
I;
1
\
i
И
н
i
U.JI
my_hash.add(l, "g")
my_hash.add(l, "h")
my_hash.add(l, "i")
my_hash.add(l, "j")
my_hash.add(l, "k")
my_hash.print( ) ;
my_hash.
my__hash.
my_hash.
my__hash.
my_hash.
my__hash.
my_hash.
my_hash.
my hash.
my_hash.
my_hash,
my_hash.
my_hash.
my_hash.
my_hash.
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
add(l
my__hash. print (
return 0;
"1")
"m")
"n")
"o")
"p")
"q")
"r")
"s")
"t")
"u")
"v")
"w")
"x")
"У")
"z")
) ;
Программа hash_2.cpp создает смешанную таблицу, начальный размер
массива которой равен 1. Это, конечно, дикий выбор. Мы сделали так только
для того, чтобы продемонстрировать автоматический рост массива по мере
добавления элементов в таблицу. Вот вывод hash_2.cpp:
slot #
0:
[1,е] [l,d] [l,c] [1,Ь] [1,а]
slot #
0:
i-i
[l,f] [l,b] [l,d]
[1,а] [1,с] [1,е]
slot #
0
1
2
3
[l,d]
[1,«]
[Х,Ь]
Е1,к]
Clrh]
[1,а]
U,f]
ClrC]
[l,i]
[l,j]
d,g]
136
Глава 4
slot #
0
1
2
3
4
5
6
7
[1,х]
[i,y]
[l,z]
[1,с]
[l,d]
[l,u]
U,v]
[IrW]
l,h]
1Д]
l,j]
l,k]
1,1]
!,•]
l,f]
i,g]
[i,p]
[1»а]
[l,b]
[i,e]
[i,t]
[l,m]
[l,n]
[l,o]
[i,q]
[l,r]
I ПР1/
ПРИМЕЧАНИЯ
Между hash_2.h и hash_l.h имеется два существенных отличия.
Во-первых, была изменена функция подстановки, чтобы она соответствовала ключам
нового типа. Помимо очевидного изменения типа параметра с int на char*,
вычисления производятся над символами ключевой строки. Функция hash_2.h
рассматривает символы просто как целые и суммирует их значения. Затем она
делит результат на tablesize и возвращает остаток.
Повторим, что это отвечает нашим требованиям к простоте, но, возможно,
требует дальнейшей настройки. Например, строки "abc", "acb", "bac", "bca",
"cab" и "cba" подставляются в одну и ту же позицию, поскольку порядок не
влияет на сумму символов. Если у строк имеется склонность к некоторой
постоянной длине и к одним и тем же символам, пусть в различном порядке, то
выбор такой функции подстановки явно неудачен. Одним из способов
справиться с подобной проблемой может быть попеременное отрицание или смена
знака символов в строке перед их суммированием. Тем самым будет введена
хотя бы некоторая зависимость от их порядка.
Вторым важным отличием двух примеров является то, что в hash_2.h
размер массива автоматически увеличивается по мере ввода новых элементов в
таблицу. Хотя в том, как это сделано, нет ничего особенного, стоит отметить,
что это привело к небольшой реорганизации кода. В частности, мы ввели две
новых закрытых функции expancM.able() и destroy_table(cell**, unsigned
int). Функции add(const T&, char*) и ~Hash_table() были модифицированы и
теперь вызывают их. Новый закрытый элемент данных n_entries отслеживает
текущее число элементов (не позиций) в таблице. Это помогает определить,
когда следует расширять таблицу.
Expand_table() делает именно то, что предполагает ее имя: увеличивает
объем таблицы. После инициализации локальных переменных old_table и
old_table_size значениями для текущей (и устаревшей) таблицы функция
выделяет новую таблицу двойного размера. Затем она обходит старую таблицу и
вводит каждый из ее элементов в новую, вызывая add(const T&, char*).
Наконец, она вызывает destroy_table(cell**, unsigned int), передавая ей в качестве
аргументов old_table и old__table_size.
Смешанные таблицы и разреженные массивы 137
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Как отмечено в комментариях к исходному коду, было бы менее удобно,
но более эффективно удалять каждую из ячеек «на ходу», при
выполнении цикла обхода, который переносит существующие элементы в
новую таблицу. Это сделало бы ненужным вызов destroy_t able (cell**,
unsigned int), которая выполняет схожий избыточный обход. Код
этой функции заимствован из ~HashJtable() и оформлен в виде
отдельной функции, поскольку деструктор теперь не единственная
функция, которой нужно уничтожать таблицу.
Проектирование разреженного массива
Некоторые проблемы, возникающие, в частности, при обработке матриц,
коренятся в механизме индексирования массивов. Некоторые из этих
приложений работают с очень большими массивами, большинство элементов
которых равны 0 или какому-то другому значению по умолчанию. Для такого типа
приложений часто бывает выгодно устранить расходы на хранение, скажем,
тысяч нулей. Разреженные массивы являются объектами, которые ведут себя
как массивы, но не требуют места в памяти для хранения элементов,
содержащих значение по умолчанию.
Рис. 4.2 показывает распределение памяти для одномерного разреженного
массива целых. Каждая ячейка этой структуры имеет три поля: номер
столбца, сами данные и указатель, ссылающийся на следующий элемент справа.
Заголовочный элемент, крайний слева, имеет номер столбца -1 и не содержит
данных. Первый значимый элемент, с номером 0, содержит значение 4.
Второй элемент, элемент 1, не показан вообще. В этом случае считается, что
элемент 1 имеет значение по умолчанию. Третий элемент, элемент 2, имеет
значение 12.
Col
Data
Right
-1
/
0
4
2
12
/
Head element 0 element 2
Рис. 4.2. Одномерный разреженный массив
a. Схема ячейки
b. Схема массива
На рис. 4.3 показан двумерный массив целых. Это прямое обобщение
одномерного массива, но заметьте, что каждый элемент входит в два списка и
помимо номера столбца имеет еще и номер строки. Хотя двумерный вариант
более сложен, он может быть и более выгоден, поскольку сокращение
требований к памяти здесь обычно более значительно. В некотором смысле наиболее
важной частью разреженных массивов являются «дырки» — элементы, не
хранящиеся в памяти явно.
138
Глава 4
Рис. 4.3.
Двумерный
разреженный
массив с четырьмя
значимыми элементами
a. Схема ячейки
b. Схема массива
а:
Data
row
col
right
down
3
-1
•-
/
►
*
3
0
Разрабатывая структуру данных, называемую разреженным массивом,
естественно думать о ней в понятиях перегрузки operator[](); именно так на
самом деле и реализован одномерный массив. Как мы увидим, такой подход
имеет свои недостатки. Мы попробуем их обойти в нашем двумерном разреженном
массиве, который не использует operator[]().
Одномерный разреженный массив
Наш одномерный разреженный массив реализован в виде связанного
списка. У него нет фиксированного размера и он растет по мере добавления
элементов. Поскольку и для чтения, и для записи используется operator[](), ее
код должен правильно управлять доступом в обоих случаях, не зная, какой
именно имеет место в конкретный момент. Минимальным допустимым
индексом является 0; производится проверка, не допускающая применения
отрицательных индексов.
Код
Пример одномерного разреженного массива состоит из двух файлов, ld.h и
ld.cpp. Вот файл ld.h:
Смешанные таблицы и разреженные массивы
139
// Одномерный разреженный массив с переменной длиной.
//Мы рассматриваем его как "горизонтальный" массив.
// Тип Т должен поддерживать копирование. Если нужна
// печать, он должен также поддерживать operator«( ) .
#include <iostream>
using namespace std;
template <class T>
I 'class One_d_sa {
я
, private:
I
struct cell {
// Первый конструктор не определяет данные,
cell(int col_arg, cell * right_arg)
col(col_arg), right(right_arg) { }
// Второй конструктор специфицирует данные,
cell(int col_arg, T data_arg, cell * rightjarg)
col(col_arg) , data (data__arg) , right(right_arg) { }
int col;
cell * right;
T data;
void print( > {
cout « "[" « col « ", " « data « "] ";
}
};
T de£ault_value;
j cell head;
* Jpublic:
!*!
One_d_sa(const T S dv)
: default_value(dv), head(-l, 0) { }
i * // Так как мы не можем сказать, используется ли operator[](int)
Ь ' II для чтения или же модификации, один и тот же код должен
! // управлять доступом в обоих случаях. Это заставляет нас
. // вводить элемент по умолчанию в столбец к всякий раз, когда
[ // происходит доступ к к. Когда производится доступ к столбцу,
I// не имеющему элемента, мы создаем для него новую ячейку и
// инициализируем ее значением по умолчанию.
t J
Tfi operator[](int col) {
4 // Убедиться, что col неотрицателен.
l..j if (col < 0) throw "column out of range";
140
Глава 4
*_■
*-
■«
ч
■v?
// Поиск элемента col. Если не найден,
// возвратить значение по умолчанию.
cell * cp_prev = fihead;
cell * cp = head.right;
while (true) {
if ((cp ~ 0) || (cp->col > col)) {
// Добавить новую ячейку между cp_j?rev и ср со
// значением по умолчанию, и возвратить это значение
cp_prev->right = new cell(col, default_value, cp);
return cp_prev->right->data;
}
else if (cp->col == col) return cp->data;
cp_prev = cp;
cp =B cp->right;
}
void print( ) {
cell *cp - head.right;
.,. я cout « endl;
У fl while (cp != 0) {
t; '1 cp->print ( ) ;
cp = cp->right;
}
cout « endl;
Ы
Li J
Mil
Файл ld.cpp:
!// Горизонтальный массив переменной длины.
M#include "ld.h"
111 Создать одномерный разреженный массив целых
I/ со значением по умолчанию -1.
One_d_sa<int> my_ld(-l);
\r 1
'Л/1 Доступ к элементу 3 форсирует его создание,
V \lI со значением по умолчанию -1.
!'~.|int i = my_ld[3];
fjint main( )
iiS my_ld.print( );
Смешанные таблицы и разреженные массивы
141
[~~j my_ld[3J = 5;
I my_ld.print( );
f
i -
i
I-
1_J)
my_ld[5J = 7;
my_ld.print( );
try {
my ld[-l];
}
catch (const char * msg) {
cout « •msg « endl;
}
return 0;
Код ld.cpp выводит:
[3, -1]
[3, 5]
[3, 5] [5, 7]
column out of range
ПРИМЕЧАНИЯ
| UPP
Массив называется template<class T> One_d_sa. Закрытая вложенная
struct cell определяет ячейку для хранения элемента, явно записанного в
массив. Ее полями являются col, идентифицирующий номер столбца элемента;
right, указывающий на ячейку непосредственно справа; и data, в котором
хранятся действительные данные. Закрытыми элементами данных являются de-
fault_value и head. Default_value — это значение типа Т, переданное
пользователем конструктору One_d_sa(const T&). Любой элемент, не
представленный в массиве явно, считается имеющим это значение. Тип Т должен
поддерживать копирование. Если требуется печать, он должен также поддерживать
operator«(). Функция print() определена для удобства отладки.
Центром данной реализации является operator[](int), которая обеспечивает
доступ для чтения и записи в массив. Как уже отмечалось выше, главная
проблема ее реализации состоит в том, что функция не знает, для чего она
вызывается — чтения или записи. Чтобы понять, почему это важно, предположим,
что мы могли бы различать доступ для чтения и для записи. Доступу для
чтения нужно сделать совсем немногое. Если требуемая ячейка обнаружилась бы,
функция возвратила бы ссылку на ее поле данных. В противном случае она
просто возвратила бы значение по умолчанию.
А как обстоит дело с доступом для записи? Если бы функция нашла
требуемую ячейку, она вела бы себя точно так же, как при чтении, т. е. вернула
ссылку не ее поле данных. Но если нужная ячейка не найдена, функция должна
была бы ее создать; иначе просто некуда было бы что-то записывать. Мы
решаем эту проблему, предполагая, что каждый вызов запрашивает доступ для запи-
142
Глава 4
си. Как только запрашивается несуществующая ячейка, в нужном столбце
создается новая ячейка, которая инициализируется значением по умолчанию.
Двумерный разреженный массив
Хотя двумерный разреженный массив не является «концептуальным
прорывом» по сравнению с одномерным случаем, верно также, что реализация
operator[](int) обобщается на двумерный случай не особенно удачно. Одной из
причин является невозможность провести различение чтения и записи, что
является источником неэффективности. Всякий раз, когда читается
несуществующая ячейка, мы вынуждены создавать новую и инициализировать ее
значением по умолчанию. Если это случается достаточно часто, терпит крах вся
идея разреженного массива. Для двумерных массивов, вероятно, проблема
станет еще более серьезной.
Второе препятствие состоит в том, что operator[](int) может принимать
только один аргумент. Это заставило бы нас реализовать двумерный случай
как массив массивов. Такой подход на первый взгляд разумен, но мы
немедленно окажемся в тупике, как только попробуем определить значение по
умолчанию. Мы хотим, чтобы оно имело, как и в одномерном случае, тип Т, но
если взять в качестве модели нашу простую одномерную реализацию, массив
массивов должен иметь типом по умолчанию массив.
Вместо того, чтобы искать умное решение этих проблем, в двумерной
модели мы вовсе отказались от operator[](int) и заменили эту операцию двумя
функциями-элементами доступа, отдельно для чтения и для записи. Это
позволяет аккуратному пользователю избежать создания ненужных ячеек. Другие
пользователи, те, кого это мало волнует, могут применять исключительно
функцию доступа для записи.
Ради простоты наш двумерный массив имеет фиксированные размеры,
устанавливаемые пользователем. Мы создаем строку заголовков столбцов «над»
строкой,0 и столбец заголовков строк «слева* от столбца 0. Это расположение
было показано на рис. 4.2. Нетрудно былобы модифицировать такой подход,
чтобы разрешить изменение размеров. Другое возможное
усовершенствование — не создавать заголовка строки (столбца), пока в эту строку (столбец) не
будет действительно помещена ячейка.
Код
Пример состоит из двух файлов, 2d.h и 2d.cpp. Вот файл 2d.h;
// Двумерный массив с фиксированными размерами и проверкой
// диапазона. Тип Т должен поддерживать копирование. Если
// требуется печать, он должен также поддерживать
// operator«( ) .
#include <cassert>
#include <iostream>
using namespace std;
Смешанные таблицы и разреженные массивы
143
? «template <class T>
1 -class Two_d_sa {
. i
с .'private:
struct cell {
// Первый конструктор не специфицирует данные.
cell(int row_arg, int col_arg,
cell * right__arg, cell * down_arg)
row(row_arg), col(col_arg),
right(right_arg), down(down_arg) { )
.f ' // Второй конструктор определяет данные.
1 ^ cell(int row_arg, int col_arg, T data_arg,
['7 cell * right_arg, cell * down_arg)
row(row_arg), col(col_arg), data(data_arg)
:
] right(right arg), down(down arg) ( }
M .t
,- j int row;
) * int col;
<: ■! cell * right;
cell * down;
T data;
void print( ) (
cout « "[(•■ « row « "," « col « ")" « data « "]
}
);
Г"|
fi" int n_rows;
int n_cols ;
T default_value;
p"j cell head;
l"
¥
Ш
I
t
a
\
public:
// Конструктор организует заголовки для строк и столбцов.
Two_d_sa(int rows, int cols, const T & dv)
: n_rows(rows), n_cols(cols), default_value(dv),
head(-l, -1, 0, 0)
<
int i;
cell * cp;
// Подготовить заголовки столбцов.
for (i = 0, cp = &head; i < cols; ++i) {
cp->right = new cell(-l, i, 0, 0);
cp = cp->right;
}
I ^
i i
{ i // Подготовить заголовки строк.
i \ for (i = 0, cp - Shead; i < rows; ++i) {
cp->down = new cell(i, -1, 0, 0);
I
cp — cp->down;
)
}
// Мы определяем две различные функции доступа для чтения
// и для записи, так как хотим, чтобы их поведение
// отличалось.
// R__access(int, int) ищет ячейку, чьи поля строки
// и столбца соответствуют аргументам row и col. Если
// функция ее находит, она возвращает const-ссылку на поле
// data ячейки. В противном случае функция возвращает const-
// ссылку на значение по умолчанию, никакие модификации
// массива не производятся и не допускаются.
const T& r__access (int row, int col) const {
// Поиск элемента (row,col). Если не найден,
// возвратить значение по умолчанию.
// Сначала проверить допустимые границы диапазона.
if ((row < 0) || (row >= n_rows)) throw "row out of range
if ((col < 0) || (col >= n_cols)) throw "col out of range
// Найти нужную строку.
int i;
const cell * cp;
for (i = -1, cp = fihead; i < row; ++i)
cp = cp->down;
assert(cp->row « row);
// Теперь искать ячейку с нужным номером столбца,
cp = cp->right;
while (true) (
if ((cp =0) || (cp->col > col)) return default_value;
if (cp->col = col) return cp->data;
cp = cp->right;
>
)
// W_access(int, int) ищет ячейку, чьи поля row и col
// совпадают с аргументами row и col. Если ячейка
// найдена, функция возвращает ссылку (не-const) на ее
// поле data. В противном случае она создает новую ячейку,
// дает ей значение по умолчанию и возвращает ссылку
// (не-const) на ее поле данных.
Т& w_access(int row, int col) {
// Искать элемент (row,col). Если не найден, создать ее
//и присвоить значение по умолчанию.
// Сначала проверить допустимый диапазон.
Смешанные таблицы и разреженные массивы 145
w
I
ft \
№ <
ни
Г
I
■г
р
г
г
i
к
f
if ((row < 0) || (row >= n_rows)) throw "row out of range";
if ((col < 0) || (col >= n_cols)) throw "col out of range";
// Найти нужные заголовки строки и столбца.
int i ;
cell * row_p;
cell * col_jp;
for (i = -1, row_j> = Ahead; i < row; ++i)
row_p = row_p->down;
assert (row_jp->row == row);
for (i =s -1, col_p = Ahead; i < col; ++i)
col_p = col_p->right;
assert (col_jp->col == col) ;
// Теперь искать ячейку с нужным номером столбца.
// Указатель row_p_prev хранит предыдущую ячейку
j // на случай, если потребуется вставка.
г i
! ' cell * col_p_prev = col_p;
cell * row_jp_jprev = rowjp;
row_j> = row_j>->right;
col_jp = col__p->down;
while (true) {
if ((row_p ==0) || (row_p->col > col)) (
1 // В этой точке мы знаем достаточно, чтобы прикрепить
' 1 П новую ячейку к ее строке. Но нам еще необходим
\ \ II дополнительный поиск, прежде чем мы сможем связать
\\ II ячейку с ее столбцом.
while (true) {
if ((coljp ==0) || (col_p->row > row)) {
// Вставить новую ячейку между row_jp_prev и row_p,
// и между col_p_prev и col_p, со значением
//по умолчанию, и возвратить по ссылке это
// значение.
row_p_prev->right =
new cell(row, col, default_value, row_p, col_p);
col_p_prev->down = row_p_jprev->right;
return row_jp__prev->right->data;
}
assert (col_p->row != row) ;
col_p_jprev = col_p;
col_p = col_p->right;
}
}
else if (row_p->col == col) return row_p->data;
146
Глава 4
*""J row_p_prev = row_p;
, row_j> — row_j?->right;
:i
:A >
' . // Распечатать массив по строкам.
.. \ void print{ ) {
cell *row_p = head.down;
while (row_j> != 0) {
// Print the row.
cell * col_p = row_p->right;
e bool linefeed = (col_j? ? 1 : 0) ;
t
| while {col_p != 0) {
| i col_p->print( );
f col p = col p->right;
} ' if (linefeed) cout « endl;
row_j> — row_p->down;
\, )
* i
П >
cout « endl;
Файл 2d.cpp:
Г {jttinclude "2d.h"
i J
■j// Создать двумерный разреженный массив целых
,- 1// с тремя строками, пятью столбцами и значением
. \\f/ no умолчанию -1.
• "!Two_d_sa<int> my_2d(3, 5, -1) ;
V i
t int main( )
!(
1 my_2d. print ( ) ;
_i my_2d.w_access(2,3) = -7;
..- i cout « my_2d.r_access(2,3) « endl;;
f J my_2d.print ( ) ;
'* ! cout « my_2d.r_access(0,l) « endl;;
•y-\ my_2d.w_access (2,4) - -5;
► « i my_2d.print( );
';|
\ 1 my_2d.w_access(2,1) - -4;
•-i * my__2d.print ( ) ;
'* i
.j my 2d.w access(2,0) = -4;
Смешанные таблицы и разреженные массивы
147
my_2d.print( );
my_2d.w_access(0,1) » -8;
my_2d.w_access(1,3) = -9;
my_2d.print( );
int i, j;
for (i = 0; i < 3; ++i)
for (j = 0; j < 5; ++j)
my_j2d.w_access(i, j) = i+j ;
my_2d.print( );
try {
my_2d.r_access(3,5);
>
catch (const char * msg) {
cout « msg « endl;
}
return 0;
Программа 2d.cpp выводит:
-7
I(2,3)-7]
-1
[(2,3)-7] [(2,4)-5]
[(2,l)-4] t(2,3)-7] [(2,4)-53
E(2,0)-4] [(2,l)-4] [(2,3)-7] [(2,4)-5]
[(0,l)-8]
[(l,3)-9]
[(2,0)-4] [(2,l)-4] [(2,3)-7] E(2,4)-5]
[(0,0)03 [(0,1)13 [(0,2)23 [(0,3)3] [(0,4)43
[(1,0)1] [(1Д)2] [(1,2)3] [(1,3)4] [(1,4)5]
[(2,0)2] [(2,1)3] [(2,2)43 [(2,3)5] [(2,4)6]
row out of range
148
Глава 4
I ПРИМЕЧАНИЯ
Два изменения, которые следует отметить в 2d.h, касаются struct cell и
функций доступа. Так как каждая ячейка входит теперь в два списка и кроме
номера столбца имеет еще и номер строки, в struct cell появляются два
дополнительных поля. Row представляет номер строки, a down указывает на ячейку
непосредственно под данной. Конструктор Two_d_sa(int, int, const T&) теперь
принимает, кроме значения по умолчанию, два целых числа, задающих
фиксированные размеры массива.
Функциями доступа являются r_access(int, int) и w_access(int, int).
Каждая принимает в качестве аргументов номера строки и столбца. Функция
доступа для чтения r_access(int, int) возвращает const T&. После проверки
диапазона функция проходит по списку заголовков строк, пока не найдет тот, что
указан аргументом row. Затем она сканирует строку слева направо в поисках
ячейки, соответствующей аргументу col. Если функция встречает ячейку,
поле col которой больше указанного аргумента, или достигает конца списка,
она возвращает значение по умолчанию.
W_access(int, int) возвращает Т& и следует похожему алгоритму. Однако,
когда эта функция доступа обнаруживает, что искомой ячейки не существует,
она создает ячейку, инициализирует ее значением по умолчанию и вставляет
ее в нужное место массива. Поскольку это место определяется путем
сканирования соответствующей строки, функция знает достаточно, чтобы вставить
при необходимости новую ячейку в ее строку. Но функция должна еще
вставить эту ячейку и во второй связанный список — в ее столбец. Она делает это,
сканируя заголовки столбцов и, наконец, нужный столбец аналогично
сканированию строки. За сканирование столбца отвечает внутренний цикл while,
отсутствующий у функции доступа для чтения.
Некоторые пользователи, возможно, захотят минимизировать число ячеек
заголовка и придать массиву способность менять свои размеры. Эти изменения
можно осуществить, модифицировав Two_d_sa(int, int, const T&) и две
функции доступа. Модифицированный конструктор будет иметь только один
параметр (const T&) и, вместо того чтобы создавать все заголовки строк и столбцов,
создаст только главную ячейку заголовка в строке -1 и столбце -1.
Модифицированные, функции доступа будут сканировать ячейки заголовка не путем
подсчета, а искать строку или столбец с номером, большим или равным
требуемому. При необходимости будет создана новая ячейка заголовка. Такой подход
имитирует то, как мы сейчас сканируем строки и создаем ячейки данных по
мере надобности.
Функция-элемент print() теперь печатает в скобках номера строки и
столбца, за которыми следует значение поля данных. Печать предусмотрена только
в целях иллюстрации и отладки.
та
-1 lUOU О Л Htl>.l
* * ■ . 1 J(
— -
1 s а Й/
'£ '
mm.globalnew.cpp
mmfarray.h
mm_array.cpp
Арт
mmlist.h
mmjistcpp
май
■ i'>. 4
'm r. ^»
IT f, ' 1! _ ,«' ' , '
*>#
А* ^Щ
■^-"■gli 1$$Щ&*&*
150
Глава 5
Любой язык, поддерживающий динамическое распределение памяти,
должен так или иначе решать основной вопрос — на кого ложится
ответственность? Неудивительно, что существует широкое разнообразие точек зрения
по данному предмету.
В соответствии с одной из них, программисты никогда не должны
заботиться о выделении и освобождении памяти для своих приложений. Любая
связанная с этим деятельность должна происходить строго скрытно, поскольку она
редко имеет прямое отношение к приложению. Противоположная точка
зрения утверждает, что прямой программный контроль над распределением
памяти существен для эффективности выполнения. В приложениях реального
времени, где критично время отклика, любая «закулисная деятельность»
может оказаться неприемлемой.
Управление памятью в С и C++
Делая акцент на эффективности реального времени, С и C++ возлагают
ответственность за динамическое распределение памяти на программиста. В С
программисты чаще всего пользуются для выделения и освобождения памяти
библиотечными функциями. Соответствующими операциями C++ являются new
и delete.
Две эти операции обычно существуют в четырех разновидностях, оттенки
которых определяются тем, управляют ли они одиночными объектами или
массивами и выбрасывают ли исключение bad_alloc при исчерпании памяти.
Варианты функций, имеющих дело с массивами, обозначаются как new[] и
delete[]. Часто реализация new и delete опирается на функции malloc(size_t) и
free(void*), выполняющие действительную работу. Тем не менее, любая
память, выделенная new (или new[]) может быть освобождена только операцией
delete (соответственно delete[]).
Тот факт, что malloc(size_t) и free(void*) реализованы как библиотечные
функции, означает, что программист, работающий на С, может заменить эти
функции их доморощенными версиями. Следовательно, в дополнение к
программному контролю над распределением памяти программист может, на
самом деле, заново реализовать сам механизм распределения (аллокатор). В C++
программист может похожим образом контролировать механизм
распределения, перегружая глобальные операции new и delete. Имейте в виду, что в
большинстве случаев это будет неудачной мыслью и очень опасным
предприятием. Однако в некоторых, тщательно проанализированных, ситуациях оно
может принести пользу. Подробнее об этом говорится в следующем разделе.
В дополнение к контролю, предоставленному программисту над
глобальным механизмом распределения, в C++ возможна выборочная замена
операций new и delete. Перегружая эти операции для отдельного класса,
программист может написать специализированный аллокатор для объектов только
этого класса. Специализированный аллокатор, благодаря ограниченной
области действия, с меньшей вероятностью будет вести себя непредсказуемым
образом и, следовательно, будет менее опасен, чем доморощенный глобальный
аллокатор. Два из трех примеров программ этой главы демонстрируют приемы
написания специализированных аллокаторов.
Управление памятью
151
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Вряд ли стоит писать специальные аллокаторы для всех классов
подряд. Целью написания аллокатора является улучшение эффективности
времени выполнения и/или использования памяти. А для этого лучше
всего тщательно выбрать типы объектов, которые чаще всего будут
выделяться и освобождаться.
Перегрузка глобальной операции new
Как уже говорилось, перегрузка глобальных операций new и delete — или
замена функций ma!loc(size_t) и free(void*) — как правило, неудачное
решение. Написать «с нуля» распределение памяти общего назначения очень
непросто. Написанный аллокатор должен быть не только эффективен, он должен
быть чрезвычайно устойчив. Ошибки распределения памяти достаточно
трудно локализовать даже в том случае, когда сам аллокатор не имеет дефектов!
При таких строгих требованиях, когда же имеет смысл заменять глобальный
аллокатор?
Замена глобального аллокатора выглядит оправданной в двух ситуациях.
Иногда, хотя и не часто, появляется лучшие аллокаторы, возможно, на рынке
коммерческого программного обеспечения. Ни в коей мере не
«доморощенные», это, как правило, тщательно спроектированные, всесторонне
протестированные индустриальные продукты. И очень неплохо испробовать такой
аллокатор, вставив его в вашу программу.
С другой стороны, глобальная замена может потребоваться в случае, когда
в распределении памяти имеется некий дефект, который трудно
диагностировать. Как правило, ошибки связаны с неправильным использованием
аллокатора, а не с ним самим. Например, программа может применять операцию
delete к адресу, который уже был освобожден. Есть коммерческие аллокаторы,
которые оснащены инструментальными средствами, помогающими
обнаруживать программные ошибки во время выполнения. Как только ошибки найдены
и исправлены, можно вернуться к стандартному аллокатору.
Используя механизм перегрузки C++, программисты могут писать
собственные инструментальные версии new и delete. Иногда самый топорный
инструментальный аллокатор может оказаться очень полезен в диагностике
ошибок времени выполнения. Описанная здесь короткая программа
демонстрирует, как это может выглядеть.
Код
Код примера содержится в единственном файле mm_globalnew.cpp:
// Программа демонстрирует, как перегрузить глобальные операции
// new( ) и delete( ). Имейте в виду, обычно это *плохое*
//и очень опасное решение. Одной из ситуаций, когда это может
// быть оправдано - присоединение коммерческого диспетчера
// памяти, специально разработанного для отладки ошибок
// распределения памяти. Как альтернатива, возможна грубая
152
Глава 5
• j// отладка с простыми операторами печати.
#include <new>
#include <cstdlib>
#include <cstdio>
" fusing namespace std;
Avoid * operator new(size_t s) throw(bad_alloc)
4
л
W. 1
"*■ ;
li
I
\.
t
! 4
■■ 1
printf ("Global new: ");
// Попытка выделить память с помощью malloc.
void * allocated_memory = malloc(s);
// Определить текущий new_handler, если он есть.
// Второй вызов восстанавливает значение new_handler
// которое обнулил первый вызов.
new_handler nh = set_new_handler(0);
if (nh != 0) set_new_handler(nh);
i] while (allocated_memory == 0) {
i ] if (nh != 0) (*nh)( );
Г.
i
•■-.
else throw bad_alloc( );
allocated_memory = malloc(s);
}
// Код для грубой отладки распределения памяти.
printf("Allocated %d bytes of memory at, address 0x%x\n",
s, allocated_memory);
I \ return allocated_memory;
-i
i'1
.0
■ . i
?■ I jvoi
|void operator delete(void * vp) throw( )
f .1 printf("Global delete: ") ;
Г j if (vp !=0 ) free(vp);
j printf("Deleted memory at address 0x%x\n", vp);
l о
7 'int main( )
i
int * int_pointer = new int;
t. printf ("Sint_pointer = 0x%x\n", fiint_pointer) ;
1 delete int_pointer;
r ! return 0;
'■■1
Управление памятью 153
Вот примерный вывод этой программы:
Global new: Allocated 64 bytes of memory at address 0x400035d8
Global new: Allocated 64 bytes of memory at address 0x40003620
Global new: Allocated 64 bytes of memory at address 0x40003668
Global new: Allocated 64 bytes of memory at address 0x400036b0
Global new: Allocated 4 bytes of memory at address 0x400035a8
&int_pointer == 0x7b03a548
Global delete: deleted memory at address 0x400035a8
I npk
ПРИМЕЧАНИЯ
Программа mm_globalnew.cpp демонстрируют, как можно реолизовать
глобальные операции new и delete на основе функций malloc(size_t) и free(void*).
Мы реализуем операцию new, определяя функцию operator new(size_t). Наша
версия операции при исчерпании памяти выбрасывает исключение bad_alloc.
Никакого реального управления памятью, кроме вызова malloc(size_t) и
возврата полученного от него указателя, не производится. Целью функции-операции
является корректная обработка ошибок и обеспечение интерфейса C++.
После начального вызова malloc(sizet) мы подготавливаем цикл проверки
на ошибку, извлекая указатель на текущий new_handler. Указатель, который
мы сохраняем в локальной переменной nh, является адресом функции,
вызываемой в случае исчерпания динамической памяти. Функция new_handler
может быть задана пользователем и может быть нулевой. Вызов set_new_hand-
ler(new__handler) устанавливает аргумент в качестве процедуры new_handler
и возвращает адрес предыдущей процедуры. Цель вызова set_new_hand-
ler(new_handler) здесь — запись возвращаемого ею значения. Затем мы
восстанавливаем первоначальный new_handler, вызывая функцию второй раз.
Цикл проверки на ошибку опирается на возвращаемое malloc(sizc_t)
значение, чтобы решить, нужно ли вызывать ее снова. Если возвращаемое mal-
loc(size_t) значение — не нуль, мы просто возвращаем это значение. В
противном случае, если в наличии имеется new_handler, мы вызываем его. Идея
состоит в том, что, возможно, он как-то реорганизует память таким образом, что
в распоряжении malloc(size_t) окажется больше свободной памяти; поэтому
мы вызываем ее снова. Заметьте, что цикл весьма оптимистично расценивает
способность new_handler получить достаточное количество свободной памяти.
Если new_handler определен, он вызывается снова и снова, пока у mal-
loc(size_t) не окажется достаточно свободной памяти, чтобы возвратить
ненулевое значение. Если этого так и не происходит, цикл продолжается
бесконечно. Если new__handler не определен, мы выбрасываем исключение bad_alloc.
Тем самым функция, находящаяся выше на стеке вызовов", получает
возможность перехватить это исключение и либо как-то справиться с ситуацией, либо
завершить программу.
Функция operator delete(void*) значительно проще. Она просто передает
свой аргумент free(void*), если только он не нулевой. Никаких исключений не
выбрасывается.
Как вы можете видеть, наши операции new и delete генерируют вывод
некоторых сообщений. Понятно, что это не предназначено для конечного
программного продукта. Цель этого вывода здесь двойная. Во-первых, он позволя-
154
Глава 5
ет нам написать маленькую тестовую программу, подтверждающую, что эти
операции действительно вызываются во время выполнения. Во-вторых,
печатный вывод может быть использован как грубое инструментальное средство для
обнаружения дефектов распределения памяти. Например, прослеживая вывод
наших операций, легко определить распространенную ошибку двух удалений
адреса без нового выделения памяти между ними.
Вывод этого примера будет отличаться от системы к системе. Например,
показанный выше вывод некоторых может удивить, поскольку он состоит из
семи строк. Изучение текста программы вроде бы дает основание полагать, что
будет выведено три строки. Что здесь произошло? В C++ глобальные
конструкторы, которые могут быть ассоциированы с кодом пользователя или
системными библиотеками, вызываются до начала плавной программы. На
данной конкретной системе глобальные конструкторы четыре раза вызывали
operator new(size„t). Эти «неожиданные» вызовы еще раз иллюстрируют
трудность предвидения всех возможных обстоятельств использования глобальных
операций new и delete.
Простой ал локатор, основанный на массиве
Хотя механизмы распределения памяти общего назначения писать трудно,
специальный аллокатор может быть довольно очевидным. Он может также
быть очень быстрым. Язык C++ позволяет перегружать операции new и delete
для отдельного класса. Иногда это приводит к значительному улучшению
скорости выполнения и/или использованию памяти. Два оставшихся примера
главы демонстрируют распределение памяти для объектов фиксированного
размера. Такое ограничение ведет к колоссальному упрощению программной задачи.
Чтобы понять, почему это так, рассмотрите некоторые проблемы, которые
приходится решать аллокатору общего назначения. Начать с того, что блоки
свободной памяти могут иметь различный размер. Ясно, что аллокатор
должен найти блок по крайней мере того же размера, что и объект, запрошенный
пользователем. Одним из способов сделать это является последовательный
просмотр свободных блоков и остановка, как только будет найден достаточно
большой блок. Это называется стратегией «первого попадания». Другие
распространенные стратегии — «наихудшего попадания», которая может
способствовать уменьшению фрагментации, и «наилучшего попадания», которая
имеет тенденцию к увеличению фрагментации, но сокращению общего
размера памяти, растраченной впустую. Оценка выгод и цены, которую придется
платить в случае выбора этих и других стратегий — непростая задача. Другой
вопрос — слияние соседствующих свободных блоков. Например, два смежных
8-байтовых блока можно использовать для удовлетворения 16-байтового
запроса, если перед этим слить их в один блок.
Аллокатор для объектов фиксированного размера не сталкивается с
подобными проблемами. Каждый фиксированный блок эквивалентен любому
другому, так что нет смысла говорить, как в случае аллогатора общего назначения,
о стратегиях «наилучшего», «наихудшего» и «первого попадания». Не
придется беспокоиться и о слиянии смежных свободных блоков. Вообще важным
следствием ограничения на размер блока является то, что нам не нужно
искать блок, удовлетворяющий каким-либо условиям. Мы просто берем первый
попавшийся свободный блок.
. Управление памятью 156
Методика, разработанная в этом разделе, предполагает, что доступная
свободная память представлена массивом фиксированного размера с
фиксированным размером элементов. Эти размеры определяются во время компиляции.
Параллельный массив типа bool с именем is_reserved отслеживает
зарезервированные (т. е. занятые в данный момент) элементы. Когда все элементы
фиксированного массива распределены, у нас больше «нет свободной памяти*,
пока программа не освободит какие-то из них, вызвав delete. Такие
ограничения могут быть оправданы в приложениях встроенных процессоров, где
количество доступной памяти строго ограничено. Можно сэкономить еще больше
памяти, если заменить массив типа bool битовым вектором.
Выбранный нами подход зависит от правил выравнивания и может
потребовать модификации для других компиляторов или систем. Почему нас
вообще заботят эти проблемы? В конце концов, мы могли бы просто определить в
качестве нашей области свободной памяти массив объектов Special_class (это
имя, выбранное для класса, объекты которого мы будем размещать) и
предоставить, как обычно, отработку деталей компилятору. Но мы отказались от
этой мысли, поскольку C++ автоматически выполняет конструктор по
умолчанию для каждого объекта вновь созданного массива. Это как раз то, чего мы не
хотим делать, потому что память, которую мы отводим для нашей
специальной свободной области, должна быть «сырой». (Комментарии в исходном коде
детально обсуждают этот пункт.) Альтернативный подход описан в последнем
примере этой главы.
Другое ограничение касается классов, которые могут быть выведены из
Special_class. Если производный класс имеет дополнительные элементы
данных, наша специальная операция new неправильно вычислит его размер.
Другими словами, если любой производный от Special_class класс будет
распределяться динамически, размер его должен быть тем же.
Код
Файлы mm_array.h и mm_array.cpp содержат код нашего аллокатора,
основанного на массиве. Файл main_array.cpp содержит пример его
использования. Вот файл mm_array.h:
#include <new>
#include <cstdlib>
#include <cstdio>
using namespace std;
class Special_class {
int int_data; // действительные данные, хранящиеся в классе,
char ch data; // действительные данные, хранящиеся в классе.
public:
Special__class ( ) { /*Инициализация объекта*/ }
void * operator new(size_t s) throw(bad_alloc);
void operator delete(void * vp, size_t s);
}; __^^^_^^^^_
156
Глава 5
Файл mm_array.cpp:
!// Реализация специализированных операций new и delete
[// для класса Special_class. Ключевое слово "static" применяется,
]// чтобы ограничить область действия определения данным модулем
// компиляции.
|#include "mm_array.h"
I #include <cassert>
// Число элементов фиксированного размера в массиве,
static const int num_elts = 512;
[// Количество памяти, отводимое под элемент массива. Его
!// непросто определить корректно, не зная, как компилятор
[// выравнивает классы. Мы предполагаем, что базовый класс long
[// предъявляет самые строгие требования к вырарниванию, и
!// выделяем под один элемент пространство, кратное размеру long.
[// Обычно это означает, что в каждом элементе будет оставаться
}// небольшое неиспользуемое пространство. Это правило
[// не обязательно приложимо ко всем системам.
[static const int elt_size —
(sizeof(Special_class) % sizeof(long)) = 0
? sizeof(Special_class)
: (sizeof (Special__class) / sizeof(long)) * sizeof(long)
+ sizeof(long);
// Фиксированный массив фиксированных блоков памяти. Каждый
// правильно выровнен и предоставляет достаточно места для
// размещения объекта Special_class.
//
[// Этот тип реализован как union, чтобы обеспечить максимально
[// строгое выравнивание первого элемента и, таким образом,
// сделать возможным размещение в нем любого типа данных.
I// Ограничения выравнивания для базового типа "long" наиболее
// строги, и потому именно он был выбран для этой цели.
// Нельзя гарантировать, что это будет работать на всех системах,
//
!// Union анонимный, чтобы его скрыть и чтобы избежать
// "эагряэнения"имен.
[static union {
// Имитировать блок памяти, отводимой под гипотетическое
// определение Special_class array[num_elts];
// Предполагает, что тип long выравнивается наиболее строго
// и что sizeof(unsigned char) = 1.
long dummy;
unsigned char raw[num_elts * elt_size];
;} scjnemory ;
I// Исходно false.
static bool is reserved[num elts];
Управление памятью
157
// Выполнить простой линейный поиск на is_reserved, чтобы
// найти индекс первого свободного элемента памяти. Возвратить
// адрес этого элемента. Когда память исчерпана, выбрасывает
// исключение bad alloc. New handler не вызывается.
Jvoid * Special_class::operator new(size_t s) throw(bad alloc)
{
int new_elt = -1;
for (int elt = 0; elt < num_elts; ++elt) {
if (is_reserved[elt] == false) (
new_elt = elt;
break;
)
)
if (new__elt == -1) throw bad__alloc( );
printf
("Special_class::new allocated %d bytes at address Ox%x\n",
s, &(sc_memory,raw[new_elt * elt_size]) );
// Удостовериться, что элемент начинается на границе "long",
assert(
( (unsigned int)(S(sc_memory.raw[new_elt * elt_size]))
%
sizeof(long)
) == о
);
is_reserved[new_elt] = true;
return &{sc_memory.raw[new__elt * elt_size]);
[// Присвоить false соответствующему элементу is_reserved.
[void Special_class::operator delete(void * vp, size_t s)
(
// Вычислить индекс is_reserved.
int index =
{(unsigned char *)vp - &sc_memory.raw[0]) / elt_size;
is__reserved [index] = false;
printf
("Special_class::delete released %d bytes at address Ox%x\n",
s, vp);
Файл main_array.cpp:
I#include "mm_array.h"
|Special_class sc;
158
Глава 5
FF*
lu
int main( )
{
Special_class * scp_0 = new Special_class;
J^ ^ Special_class * scp__l = new Special_class;
Special_class * scp_2 = new Special_class;
.*"■
delete scp_l;
delete scp_0;
delete scp_2;
L..J return 0;
Ml
Код main_array.cpp выводит:
Special_class::new allocated 8 bytes at address 0х400014Ь8
Special_class::new allocated 8 bytes at address 0x400014c0
Special_class::new allocated 8 bytes at address 0x400014c8
Special_class::delete released 8 bytes at address 0x400014c0
Special_class::delete released 8 bytes at address 0x400014b8
Special_class::delete released 8 bytes at address 0x400014c8
ПРИМЕЧАНИЯ
| T\PV
Файл mm_array.h содержит определение класса Special_class. Объявления
operator new(size_t) и operator delete(void*, size_t) являются единственными
модификациями, необходимыми для определения класса. Файл mm_array.cpp
включает mm_array.h и содержит реализацию new и delete.
В коде приняты меры для того, чтобы сделать константы и переменные
скрытыми в пределах текущего модуля компиляции. Для этого использовано
анонимное объединение и ключевое слово static. Константа num_elts
представляет число блоков фиксированного размера, которые мы собираемся
распределять. Это число произвольно выбрано равным 512. Годится и любое
другое положительное значение. Константа elt^size — число байтов, которые мы
должны выделить под каждый объект. Заметьте, что мы, возможно,
переоцениваем это число, чтобы гарантировать размещение каждого элемента на
границе long. Анонимное объединение sc_memory содержит сырую память,
которую мы предоставляем операциям new и delete для распределения.
Массив блоков фиксированного размера называется sc_memory и
представляет собой анонимное объединение. Анонимным этот тип сделан, чтобы
«засекретить» его от других модулей трансляции.
Почему sc_memory — union, а не struct или class? Мы хотим, чтобы к
массиву raw применялось самое строгое выравнивание. На многих системах
наиболее строго выравнивается long. На некоторых системах может
потребоваться другой тип, чтобы достигнуть того же результата. Имя элемента long —
dummy — подчеркивает тот факт, что у него нет другого назначения, кроме
как обеспечить необходимое выравнивание.
Массив is_reserved содержит булевы элементы, исходно равные false.
Значение false у n-го элемента показывает, что n-ый элемент массива sc_memory
не занят; другими словами, этот элемент доступен для выделения. Значение
Управление памятью 159
true показывает, что и-ый элемент уже был выделен более ранним вызовом
Special_class::new(size_t) и в данный момент выделен быть не может.
Функция Special_class::operator new(size_t) действует прямолинейно.
Линейный поиск в is_reserved дает индекс, п, первого свободного элемента. Если
свободного элемента нет, выбрасывается исключение bad_alloc; в противном
случае она возвращает адрес n-го элемента sc_memory.
Функция S ре ел ale lass-operator delete(void*, size__t) еще проще. По
значению своего аргумента void* и известному начальному адресу массива sc_me-
mory функция вычисляет индекс, п, удаляемого элемента. Затем она
присваивает п-му элементу is_reserved значение false, что делает соответствующий
элемент sc_memory вновь доступным для выделения.
Операторы печати предусмотрены только для целей демонстрации и
отладки. В коде конечного продукта их нужно закомментировать. Заметьте, что обе
функции управления памятью игнорируют свой аргумент size__t, поскольку
размер распределяемых блоков памяти всегда один и тот же.
Файл mainarray.cpp показывает несколько простых примеров
распределения памяти. Заметьте, что порядок удаления переменных sc_0, sc_l и sc_2
несколько отличается от порядка их выделения.
Простой аллокатор, основанный на списке
Аллокатор, основанный на массиве, из предыдущего раздела имеет две
очень привлекательных особенности: это простота и скорость. Главный его
недостаток — отсутствие гибкости. Число блоков, доступных для
распределения, определяется во время компиляции. Хотя это может отвечать
ограничениям реального мира во встроенных системах, большинству других
приложений необходима более гибкая политика.
Аллокатор, представленный в этом разделе, достигает нужной гибкости,
наращивая свою динамическую память во время выполнения с помощью
глобальной операции ::new. Спроектированная таким образом динамическая память
выглядит уже не как массив, а как связанный список. Каждая ячейка списка
является структурой, состоящей из Specialclass и указателя на следующую
ячейку. Так как такая схема предоставляет все вычисления и выравнивания
компилятору, конструктор по умолчанию для Special_class должен быть
определен как нулевой, поскольку мы не хотим, чтобы при каждом выделении
новой ячейки для динамической памяти вызывался настоящий конструктор.
Сейчас внимательный читатель может оказаться в недоумении: а чем же
такая методика лучше использования стандартной операции ::new? В конце
концов, что можно выиграть, построив связанный список из ячеек памяти, если
для получения этих ячеек мы в первую очередь должны вызвать ::new?
Коротким ответом будет: «очень многое!*
Начать с того, что как только ячейка получена от ::new и присоединена к
списку динамической памяти, наш специализированный аллокатор может
использовать ее снова и снова в быстрых циклах выделения/освобождения. В
добавок нет никакой причины, чтобы ::new выделяла эти ячейки обязательно по
одной. Всякий раз, когда у Special_class::operator new(size_t) кончаются
свободные ячейки динамической памяти, она может запросить у ::new сразу
большое количество новых ячеек. Получив такой массив, мы делаем из него свя-
160
Глава 5
занный список, в котором каждая ячейка указывает на следующую по
порядку в массиве.
Какие недостатки имеет такой, ориентированный на список, подход?
Главный из них тот, что если ::new добавляет массив ячеек к списку динамической
памяти, эти ячейки уже никогда нельзя будет вернуть в глобальную
динамическую память операцией ::delete. Когда ::new выделяет свежий массив ячеек,
они пусты и образуют непрерывный блок. Если бы мы вызывали для их
возвращения в глобальную память -delete, последняя должна была бы вернуть
их все сразу. Проблема в том, что между вызовами ::new и "delete могло быть
любое число вызовов Specialclass:: operator new(size_t) и Special_cIass::ope-
rator delete(void*, size_t). Обычно это приводит к тому, что какие-то ячейки
массива будут свободны, а какие-то заняты. До вызова "delete они все должны
быть освобождены, но нет никакого способа это гарантировать. В результате с
точки зрения ::new число ячеек, выделенных под объекты Special_class,
непрерывно растет по ходу выполнения программы, пока не достигнет «отметки
высокой воды» (или чуть большего числа, если учесть незанятые ячейки
последнего выделенного массива). Но во многих ситуациях такая цена вполне
приемлема.
Второй недостаток данного подхода передается по наследству от аллокато-
ра, основанного на массиве. Классы, производные от Specialclass, должны
иметь тот же размер, что и он сам. Это ограничение можно обойти, сделав два
изменения.
Первое — определение виртуального деструктора для Specialclass.
Совершенно нормально, если деструктор ничего не делает, раз он определен. По
причинам, которые мы здесь не будем объяснять, определение виртуального
деструктора гарантирует, что при любых обстоятельствах аргументы size_t,
передаваемые операциям new и delete класса Special_class, будут иметь
корректные значения. До сих пор это не имело значения, поскольку мы
совершенно игнорировали эти аргументы.
Второе изменение состоит в том, что мы должны организовать не один, а
столько связанных списков, сколько имеется различных размеров
производных классов. Один из способов сделать это — определить avail как массив
указателей, а не одиночный указатель. Можно определить параллельный массив,
в котором будет храниться размер ячеек памяти связанного списка,
соответствующего каждому индексу.
Код
Код аллокатора содержится в файлах mm_list.h и mm_list.cpp. Файл
main_list.cpp демонстрирует пример его использования. Вот mm_list.h:
#inelude <new>
#include <cstdlib>
#include <cstdio>
using namespace std;
class Special_class {
private:
Управление памятью
161
EH
4 -
t
If'
int data; // данные класса.
public:
// Конструктор по умолчанию не должен ничего делать
Special_class( ) { }
Special__class (int i) : data(i) { }
void * operator new(size__t s) throw(bad_alloc);
void operator delete(void * vp, size_t s);
>;
Файл mmlist.cpp:
// Реализация специализированных операций new и delete
// для класса Special_class. Ключевое слово "static" применяется,
// чтобы ограничить область действия определения данным модулем
// компиляции.
j#include "mm_list.h"
\ \1/ Следующая структура применяется для построения связанных
|// списков из блоков памяти Special_class.
f" struct Special_class_mem_cell (
\ Special_class memory_chunk;
t j Special__class_mem_cell * next;
L );
■ \U Число элементов фиксированного размера в массиве.
Г чstatic const int num_elts = 512;
■ l
'J// Указатель на связанный список доступных блоков памяти.
* "j static Special_class_mem_cell * avail = 0;
н
*■ jvoid * Special_class::operator new(size_t s) throw(bad_alloc)
t | // Выделить новый массив ячеек типа Special__class_mem_cell
[ * //с помощью ::newr если требуется.
f <
^ if (avail == 0) {
I * // ::new выбрасывает bad_alloc, если память исчерпана.
avail = : :new Special_class_mem_cell [num_elts] ;
t I
for(int i = 0; i < num_elts-l; ++i) {
avail[i].next = £(avail[i+1]);
}
avail[num_elts-l].next = 0;
}
// Отделить первую свободную Special_class_mem_cell
■I // и возвратить ее адрес.
162
Глава 5
ш
v
7 ■'
F
Special_class__mem_cell * ret_cell = avail;
j avail = avail->next;
b\
i
)
// Обнуление этого указателя не является необходимым.
// Может быть полезно для различения выделенных и свободных
// ячеек, если возникают ошибки распределения памяти.
ret_cell->next = 0;
printf
("Special_class::new allocated %d bytes at address 0x%x\n",
s, £(ret_cell->memory_chunk));
return &(ret_cell->memory_chunk);
void Special_class::operator delete(void * vp, size_t s)
{
it // Присоединить Special__class_mem_cell в начало списка avail.
If ((Special_class_mem_cell *)vp)->next = avail;
V . avail = ((Special_class_mem_cell *)vp);
*-4 • .
! printf
1 j ("Special_class::delete released %d bytes at address 0x%x\n",
* J
i.i s' *p>;
Файл main_list.cpp:
" ;#include "mm__list.h'
Special__class sc;
I.
r
int main( )
{
Special__class * scp_0 = new Special_class;
Special_class * scp_l = new Special__class;
Special_class * SCP_2 = new Special__class ;
j <i delete scp 1;
Г.
L\
delete scp_0;
delete scp_2;
return 0;
}
Вывод программы main.Jist.cpp выглядит примерно так:
Special_class::new allocated 4 bytes at address 0x40003768
Special_class::new allocated 4 bytes at address 0x40003770
Special_class::new allocated 4 bytes at address 0x40003778
Special_class::delete released 4 bytes at address 0x40003770
Special_class::delete released 4 bytes at address 0x40003768
Special_class::delete released 4 bytes at address 0x40003778
Управление памятью 163
| ПРИМЕЧАНИЯ
Файл mm_list.h очень похож на своего близнеца, основанного на массиве.
Единственным существенным отличием является определение (обязательное)
конструктора по умолчанию SpeciaI_class::Special_class(), который ничего не
делает. Второй конструктор Special_cIass::Special_class(int) выполняет
инициализацию данных.
Файл mm_list.cpp включает заголовок mm_list.h и реализует Special_jclass::
operator new(size_t s) и Special_class::operator delete(void * vp, size_t s).
Определение struct Special_class_mem_cell имеет областью действия текущий
файл, поэтому оно видно обеим функциям Special_class:roperator new(size_t) и
Special_class::operator delete(void * vp, size_t s). Мы не можем гарантировать,
что это имя типа останется невидимым другим модулям компиляции, но
маловероятно, что какой-то другой исходный файл будет включать mm_list.cpp. К
сожалению, ничто не мешает другому исходному файлу определить свой класс с
тем же именем.
Num_elts задает число ячеек в одном массиве. Во время работы программы
может быть выделено 0 или некоторое ненулевое число массивов. Переменная
avail с областью действия в файле определяется как Special_class_mem—cell*
и указывает на связанный список этих ячеек. Начальное значение этого
указателя нулевое.
Первым делом Special_class::operator new(size_t s) проверяет указатель
avail. Если он нулевой, в данный момент нет свободных ячеек. Нужно
выделить дополнительную память, прежде чем можно будет удовлетворить запрос.
В этом случае у ::new запрашивается массив из 512-ти ячеек Special_class__
mem_cell. Заметьте, что ::new может выбросить исключение bad_alloc, если ее
свободная память исчерпана. Указатель next каждой ячейки устанавливается
на адрес следующей по порядку ячейки в массиве. Массив становится
похожим на связанный список. Теперь, когда мы знаем, что в списке есть
свободные ячейки, мы просто отделяем первую в списке и возвращаем адрес ее поля
memory_chunk.
Как и раньше, функция Special_class::operator delete(void * vp, size_t s)
является простейшей из двух функций. Она просто полагает, что переданный
ей в первом аргументе адрес относится к Special_class_mem_cell, и
прикрепляет эту ячейку к началу списка, на которое указывает avail.
Как обычно, операторы печати предназначены только для демонстрации и
отладки.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Действительный размер массивов Special_cla88_mem_ceU[num_elt8]
может существенно влиять на эффективность выполнения. Например,
если массив оказывается на один байт больше системного размера
страницы, эффективность может пострадать из-за биения страниц. Обычно
стоит затратить некоторые усилия на настройку значения num_elts
для конкретных характеристик вашей системы и приложения.
6*
#d ЬУ*
<s,
лО>
у
J*
Jt>
t
*
Г5*-Т^
1
ii*
■rr
,/■3
,.^
filterxpp
tabspace.cpp
walkdirs (Visual C++)
doshex.cpp
Winhex (Visual C++)
. M
t-Zbt
и
* b
A* '*!*k
166
Глава 6
Одной из важнейших задач для любого программиста является
манипуляция файлами и каталогами, с помощью которых компьютер
структурирует и управляет хранящейся на дисках информацией. C/C++ предоставляет
богатый инструментарий для работы с файлами. Более того, операционная
система компьютера предусматривает свои собственные функции —
доступные через интерфейс прикладных программ (API), — которые вы можете
вызывать для управления файлами и каталогами.
В этой главе мы сконцентрируемся в основном на манипуляциях с
различными классами потоков, которые поддерживает C++. Весьма незначительная
доля кода, который мы будем рассматривать, будет машинно- или
системно-зависимой; вместо этого мы будем пользоваться классами C++ высокого
уровня. Наиболее существенным исключением в этом смысле будет программа
walkdirs.cpp, представленная в середине главы, которая использует сервисные
средства Windows для рекурсивного доступа и отображения информации о
всех файлах дерева каталогов.
Поиск и замена в текстовом файле
Первая программа этой главы — filter.cpp — является утилитой командной
строки для поиска и замены, которая позволяет отыскать в текстовом файле
все вхождения определенного слова и заменить их другим словом.
Код
Вот код программы filter.cpp, простой утилиты фильтрации:
i I,
i _.
#include <cstdlit»
#include <iostream>
#include <fstream>
using namespace std;
*
i
int main(int argc, char *argv[])
<
char buffer[1];
int i;
long currentpos;
bool match = false;
if (argc != 5) {
cout « "Usage: filter [search string] [replace string] " «
"[infile] [outfile]" « endl;
return 1;
< }
string searchstring = argv[l];
string replacestring = argv[2];
string infilename = argv[3];
string outfilename = argv[4];
Работа с файлами и каталогами 167
ifstream input(infilename.c_str(), ios::in);
if (input.fail()) {
cout « "Unable to open input file!" « endl;
return 1;
}
ofstream output(outfilename.c_str(), ios:rout I ios::trunc);
if (output.fail()) {
cout « "Unable to open output file!" « endl;
return 1;
}
while (!input.eof()) {
currentpos = input.tellg();
input.read(buffer, sizeof(buffer));
if (buffer[0] — searchstring[0]) {
match = true;
i - 1;
while (i < searchstring.length() &£ match) {
input.read(buffer, sizeof(buffer));
if (buffer[0] != searchstring[i]> (
input.seekg(currentpos);
input■read(buffer, sizeof(buffer));
match = false;
}
else
i++;
}
}
if (match) {
output.write(replacestring.c_str(), replacestring.length());
match = false;
}
else
output.write(buffer, sizeof(buffer));
}
input.close ();
output.close ();
ifstream display(outfilename.c_str(), ios::in);
i
(i
n
ui)
while (!display.eof()) (
display.read(buffer, sizeof(buffer));
if (display.good())
cout « buffer[0];
}
^ \ display.close ();
return 0;
168
Глава 6
| ПРИМЕЧАНИЯ
Большую часть того, что происходит в filter.cpp, понять относительно
нетрудно. Эта программа полезна в силу своей эффективности при обработке
входных и выходных файлов. Заголовки в верхней части программы указаны
именно те, что и следовало ожидать: это классы iostream и fstream, поскольку
мы имеем дело как с выводом на экран, так и с текстовыми файлами.
#include <cstdlib>
#include <iostream>
#include <fstream>
using namespace std;
Вся обработка, выполняемая программой, происходит в функции main(),
хотя можно было бы легко выделить код обработки и поместить его в
отдельную функцию. Заметьте» что в этом приложении main() принимает аргументы
командной строки. Она также использует: маленький, в один символ» буфер
для чтения файла; переменную счетчика (i); переменную, соответствующую
текущему положению указателя файла (currentpos) и булеву переменную для
индикации того, совпадает ли прочитанное из файла слово с текстом,
указанным пользователем в качестве искомого. Вот фрагмент main():
int main(int argc, char *argv[])
<
char buffer[l];
int i;
long currentpos;
bool match — false;
Первый условный оператор программы проверяет, правильное ли число
параметров ввел пользователь. Если нет, функция завершает программу, выводя
для пользователя информацию о параметрах:
if (argc != 5) {
cout « "Usage: filter [search string] [replace string] " «
"[infile] [outfile]" « endl;
return 1;
}
Затем программа объявляет четыре строки» в которых будет храниться
информация параметров, переданных пользователем. Каждая из этих переменных
относится к типу string, определяемому библиотекой стандартных шаблонов:
string searchstring = argv[l];
string replacestring = argv[2];
string infilename = argv[3];
string outfilename = argv[4];
После этого программа, используя указанное имя входного файла,
открывает входной файловый поток типа if stream. Хотя это и не обязательно,
второй параметр конструктора — ios::in — явно указывает компилятору, что
нужно конструировать объект как входной поток. Это значение присваивается
параметру по умолчанию; однако его можно комбинировать с другими
значениями, чтобы получить дополнительный контроль над входным потоком.
Оператор if просто удостоверяет, что программа успешно открыла файл, проверяя
Работа с файлами и каталогами
169
возвращаемое значение функции-элемента fail() и убеждаясь, что получен
корректный поток. Если это не так, программа оповещает пользователя и
завершается:
ifstream input(infilename.c_str(), ios::in);
if (output.failO) {
cout « "Unable to open output file!" « endl;
return 1;
)
Похожим образом открывается выходной поток output, который
соединяется с выходным файлом, хотя параметры конструктора здесь несколько
отличаются. По умолчанию параметр режима открытия равен ios::out. Однако,
специфицируя в дополнение к нему константу ios::tгипс, код программы
автоматически усекает (очищает) файл, если он уже существует и что-то содержит. Как и
прежде, программа проверяет, удалось ли успешно конструировать объект:
ofstream output(outfilename.c_str(), ios::out | ios::trunc);
if (output.failO) {
cout « "Unable to open output file!" « endl;
return 1;
}
После успешного открытия обоих потоков программа входит в цикл while,
который повторяется до тех пор, пока функция входного потока eof() не
покажет, что программа достигла конца файла. Именно в этом цикле программа
просматривает исходный файл и копирует его в выходной файл. Первый
оператор цикла while устанавливает переменную currentpos равной текущей
позиции указателя файла, что необходимо, поскольку мы будем «забегать
вперед» при чтении файла, когда попытаемся сопоставить его содержимое
искомому тексту. Вот начало цикла:
while (!input.eof ()) {
currentpos = input.tellg();
После получения указателя файла программа вызывает метод read() для
чтения одного символа из потока. Этот метод принимает в качестве первого
параметра массив char, а в качестве второго — значение, соответствующее
размеру массива. Затем программа сравнивает прочитанный из файла символ с
первым символом искомой строки. Если они совпадают, исполняется блок if
условного оператора (мы сейчас обсудим его):
input.read{buffer, sizeof(buffer));
if (buffer[0] == searchstring£0]) {
Итак, программа исполняет блок if только в том случае, если первый
символ строки поиска совпадает с символом, прочитанным из файла. Затем,
внутри этого блока программа должна прочитать все те символы, что будут
совпадать с символами искомой строки, выходя из блока и возобновляя
копирование одиночных символов, если какой-то из прочитанных символов не
совпадает со строкой. Мы используем переменную-счетчик (i) и цикл while, который
следит за длиной прочитанной строки и значением булевой переменной match,
прерывая свое выполнение, когда либо i сравняется с длиной строки поиска,
либо match станет равной false:
170
Глава 6
match = true;
i = 1;
while (i < searchstring.length() && match) {
Внутри этого цикла обработка проходит так, как и следовало ожидать.
Программа читает из файла по одному символу и затем проверяет, совпадает ли он
с соответствующим символом строки поиска. Если символы совпадают, код
увеличивает значение счетчика i. Если нет, приходится проявить некоторую
ловкость рук, чтобы вернуть файл к состоянию, предшествующему этим
сопоставлениям.
Возврат к предшествующему состоянию имеет решающее значение,
поскольку вы должны гарантировать, что возобновляете чтение файла с точки, в
которой началось сопоставление. В противном случае не только не будет
правильно работать механизм поиска; из конечного файла могут выпадать буквы,
слова и даже целые фразы. Восстановление состояния выполняет код внутри
блока if следующего условного оператора:
input.read(buffer, sizeof(buffer));
if (buffer[0] ! = searchstring[i]) {
Вспомните, что ранее мы вызывали функцию tellg(), чтобы в начале
итерации цикла получить текущий указатель файла. В данный момент указатель
файла продвинулся вперед на некоторое (неизвестное) расстояние от точки, в
которой он должен был бы находиться. Наиболее эффективным средством
восстановление правильной позиции является вызов функции seekg() с
переменной currentpos в качестве параметра. Затем программа читает из файла
одиночный символ, возвращая поток к состоянию, в котором он находился
непосредственно перед началом цикла сопоставления. Наконец, переменная match
устанавливается равной false, вследствие чего программа немедленно выходит
из цикла:
input.seekg(currentpos);
input.read(buffer, sizeof(buffer));
match = false;
}
else
i++;
)
}
После выхода из цикла while необходимо выяснить, прервался ли цикл
благодаря удачному или же неудачному сопоставлению. Для этого программа
проверяет состояние переменной match и реагирует соответственно. Если
match равна true, программа записывает в выходной файл всю строку замены
и устанавливает match равной false:
if (match) {
output.write(replacestring.c_str(), replacestring.length()) ;
match = false;
)
Если же соответствие не установлено, программа просто записывает
одиночный символ из массива buffer. Важно отметить, что программа не
восстанавливает указатель файла в случае, когда сопоставление успешно. Внешний
цикл возобновляет чтение с первого символа после найденной (во входном
Работа с файлами и каталогами 171
файле) строки поиска и, естественно, продолжает запись после текста замены
в выходном файле:
else
output.write(buffer, sizeof(buffer));
}
После того, как внешний цикл — проходящий по всему входному файлу —
закончит свою работу, код программы очищает переменные input и output,
закрывая потоки, которые с ними соединены. Затем, в целях ясности,
программа открывает новый объект ifstream, используемый ею для вывода на экран
содержимого полученного файла:
input.close();
output.close();
ifstream display(outfilename.c_str(), ios::in);
(В конечном продукте здесь должна была бы выполняться проверка
успешного открытия.) После этого программа просто проходит, символ за символом,
по выходному файлу и посылает прочитанные символы в поток cout (если при
чтении не произошло ошибки):
while (!display.eof()) {
display.read(buffer, sizeof(buffer));
if (display.good())
cout « buffer[0];
}
По окончании цикла вывода программа закрывает поток и успешно
завершается:
display.close();
return 0;
}
Если вы запустите эту программу с каким-нибудь текстовым файлом,
указав в командной строке замену, которую нужно произвести, на экран будет
выведено примерно следующее:
C:\>filter this TEST infile.txt outfile.txt
Four score and seven years ago/ our fathers brought forth upon TEST
continent, a new nation, conceived in liberty, and dedicated to the
proposition, that all men are created equal.
Now we are engaged in a great civil war, testing whether that
nation, or any nation so conceived and so dedicated, can long
endure. We are met on a great battlefield of that war. We have come
to dedicate a portion of that field, as a final resting place for
those who here gave their lives that that nation might live. It is
altogether fitting and proper that we should do TEST.
But, in a larger sense, we cannot dedicate — we cannot consecrate
-- we cannot hallow — TEST ground. The brave men, living and dead,
who struggled here, have consecrated it, far above our poor power to
add or detract. The world will little note, nor long remember what
we say here, but it can never forget what they did here. It is for
us the living, rather, to be dedicated here to the unfinished work
172
Глава 6
which they who fought here have thus so nobly advanced. It is rather
for us to be here dedicated to the great task remaining before us —
that from these honored dead we take increased devotion to that
cause for which they gave the last full measure of devotion -- that
we here highly resolve that these dead shall not have died in vain
— that TEST nation, under God, shall have a new birth of freedom —
and that government of the people, by the people, for the people,
shall not perish from the earth.
Единственным недостатком этой программы является то, что она не
обрабатывает корректно управляющие символы, — например, табуляции. Одним из
самых полезных применений программы-фильтра является обработка файлов
исходного кода, которые писались в различных редакторах. Одни редакторы
используют символы табуляции, другие — пробелы. Ниже мы исследуем
программу tabspace.cpp, логика которой напоминает логику фильтра,
позволяющего заменять в текстовом документе табуляции пробелами и наоборот.
Сначала посмотрим код, а затем разберем отличия его от предыдущей программы.
Код
Вот код программы tabspace.cpp.
» J#i
i
I-
i
k
J
i-
p
L.
#include <cstdlib>
#include <iostream>
#include <fstream>
using namespace std;
int main{int argc, char *argv[])
{
char buffer[1];
int i;
long currentpos;
bool match = false;
if (argc != 5) {
cout « "Usage: tabspace [infile] [outfile] [-s|-t] " «
"[numspaces]" « endl;
cout « "Use -s to replace spaces with tabs, " «
"and -t to replace tabs with spaces." « endl;
return 1;
}
string infilename = argv[l];
string outfilename = argv[2];
string switchtype = argv[3];
string searchstring, replacestring;
int numspaces = atoi(argv[4]);
if ((switchtype[1] •= 's') && (switchtype[1] != 't')){
cout « "Switch must be either -s or -t!" « endl;
return 1;
)
Работа с файлами и каталогами
173
ifstream input(infilename.c_str(), ios::in);
if (input.fail()) {
cout « "Unable to open input file!" « endl;
return 1;
}
ofstream output(outfilename.c_str(), ios::out | ios::trunc);
if (output.fail()) {
cout « "Unable to open output file!" « endl;
input.close();
return 1;
}
if (switchtype[l] — 's'H
for (i - 0; i < numspaces; i++)
searchstring += " ";
replacestring = "\t";
}
else {
searchstring = "\t";
for (i = 0; i < numspaces; i++)
replacestring += " ";
}
while (!input.eof()} {
currentpos = input.tellg();
input.read(buffer, sizeof(buffer));
if (buffer[0] = searchstring[0]) {
match = true;
i = 1;
while (i < searchstring.length() ££ match) {
input.read(buffer, sizeof(buffer));
if (buffer[0] != searchstring[i]) {
input.seekg(currentpos);
input.read(buffer, sizeof(buffer));
match = false;
}
else
i++;
}
}
if (match) {
output.write(replacestring.c_str(), replacestring.length());
match = false;
}
else
output.write(buffer, sizeof(buffer));
}
input.close();
output.close();
"" *Л if stream display (outfilename .c_str () , ios: : in);
while ('display.eof()) {
display.read(buffer, sizeof(buffer));
174
Глава 6
if (display.good())
cout « buffer[0];
}
display.close();
return 0;
)
ПРИМЕЧАНИЯ
Как и в filter.cpp, большая часть из того, что происходит в программе
tabspace.cpp, должна быть достаточно очевидна. К достоинствам программы
нужно отнести эффективность управления входными и выходными файлами,
а также простоту спецификации параметров преобразования табуляций в
пробелы и наоборот. Как и в программе фильтра, включаемые заголовки
поддерживают классы iostream и fstream, так как мы работаем как с файлами, так и
выводом на экран.
#include <cstdlib>
#include <iostream>
#include <fstream>
using namespace std;
Вся обработка, производимая tabspace.cpp, происходит в функции main(),
хотя можно было бы легко выделить код обработки и поместить его в
отдельную функцию. Заметьте, что в этом приложении main() принимает аргументы
командной строки. Она также использует: маленький, в один символ, буфер
для чтения файла; переменную счетчика (i); переменную, соответствующую
текущему положению указателя файла (currentpos) и булеву переменную для
индикации того, совпадает ли прочитанное из файла слово с текстом,
указанным пользователем в качестве искомого. Вот объявления в raain():
int main(int argc, char *argv[])
{
char buffer[1];
int i;
long currentpos;
bool match = false;
Первый условный оператор программы проверяет, правильное ли число
параметров ввел пользователь. Если нет, функция завершает программу, выводя
для пользователя информацию о параметрах. Эти параметры идут несколько в
иной последовательности, чем в первой программе, и имеют другой смысл.
Параметры infile и outfile те же самые, но третий параметр является ключом.
А четвертый параметр задает число пробелов, которые или будут заменяться
табуляцией, или вставляться на место табуляции.
if (argc != 5) {
cout « "Usage: tabspace [infile] [outfile] [-s|-t] " «
"[numspaces]" « endl;
cout « "Use -s to replace spaces with tabs, " «
"and -t to replace tabs with spaces." « endl;
return 1;
)
Работа с файлами и каталогами
175
Затем программа объявляет пять переменных для хранения информации,
переданной пользователем в параметрах. Каждая из этих переменных
относится к типу string, объявленному в библиотеке стандартных шаблонов.
Программа объявляет также целую переменную numspaces, преобразуя
строковый параметр командной строки в целое значение с помощью функции atoi():
string infilename = argv[l];
string outfilename = argv[2];
string switchtype = argv[3];
string searchstring, replacestring;
int numspaces = atoi(argv[4]);
После размещения параметров в переменных, которые удобно
обрабатывать, программа проверяет значение строки switch type, убеждаясь, что
пользователь ввел допустимый ключ. Если нет, программа выдает предупреждение
и немедленно завершает работу. Вот этот код:
if ((switchtype[l] != 's') && (switchtypefl] != 't')){
cout « "Switch must be either -s or -t!" « endl;
return 1;
}
Затем программа, используя переданное ей имя файла, открывает входной
поток типа if stream. Оператор if проверяет, что файл открыт успешно,
вызывая функцию-элемент fail(). Если произошла ошибка, программа оповещает
пользователя и завершается:
ifstream input(infilename.c_str(), ios::in);
if (input.fail()) {
cout « "Unable to open input file!" « endl;
return 1;
}
Похожая процедура выполняется для переменной output, соединяя ее с
выходным файловым потоком, хотя параметры конструктора здесь немного
другие. Снова программа проверяет, что объект потока был конструирован
успешно. Если нет, программа перед завершением очищает переменную input, что
показано ниже:
ofstream output(outfilename.c_str(), ios::out | ios::trunc);
if (output.fail()) (
cout « "Unable to open output file!" « endl;
input.close();
return 1;
)
После успешного открытия обоих потоков программа должна определить,
что она будет искать и на что заменять. Если указан входной ключ -s,
программа будет искать пробелы и заменять их на табуляции. Число пробелов,
которые нужно найти, соответствует четвертому параметру командной строки
(который мы присвоили numspaces), и мы используем цикл, чтобы поместить эти
пробелы в переменную searchstring. Затем мы присваиваем переменной
replacestring esc-код С "\t". Эти переменные будут использоваться
программой во время обработки файла:
if (switchtype[l] — 's')(
for (i = 0; i < numspaces; i++)
176
Глава 6
searchstring += " ";
replacestring = "\t";
}
С другой стороны, если пользователь выбирает ключ -t, производятся
противоположные присваивания, — пробелы помещаются в replacestring, а
табуляция в строку поиска, как показано:
else {
searchstring = "\t";
for (i — 0; i < numspaces; i++)
replacestring += " ";
}
После такой подготовки программа входит в цикл while, который
продолжается до тех пор, пока функция-элемент входного потока eof() не покажет,
что достигнут конец файла. Внутри этого цикла программа производит
действительный поиск в исходном файле и копирует его содержимое в выходной
файл. Первый оператор в цикле устанавливает переменную currentpos равной
текущей позиции указателя файла, что необходимо, поскольку мы будем
«забегать вперед» при чтении файла, когда попытаемся сопоставить его
содержимое искомому тексту. Вот начало цикла:
while (!input.eof()) {
currentpos = input.tellg();
После получения указателя файла программа вызывает метод read() для
чтения одного символа из потока. Этот метод принимает в качестве первого
параметра массив char, а в качестве второго — значение, соответствующее
размеру массива. Затем программа сравнивает прочитанный из файла символ с
первым символом искомой строки. Если они совпадают, исполняется блок if
условного оператора (мы сейчас обсудим его):
input.read(buffer, sizeof(buffer));
if (buffer[0] == searchstring[0]) {
Итак, программа исполняет блок if только в том случае, если первый
символ строки поиска совпадает с символом, прочитанным из файла. Затем,
внутри этого блока программа должна прочитать все те символы, что будут
совпадать с символами искомой строки, выходя из блока и возобновляя
копирование одиночных символов, если какой-то из прочитанных символов не
совпадает со строкой. Мы используем переменную-счетчик (i) и цикл while, который
следит за длиной прочитанной строки и значением булевой переменной match,
прерывая свое выполнение, когда либо i сравняется с длиной строки поиска,
либо match станет равной false:
match = true;
i = 1;
while (i < searchstring.length() fifi match) {
Внутри этого цикла обработка проходит так, как и следовало ожидать.
Программа читает из файла по одному символу и затем проверяет, совпадает ли он
с соответствующим символом строки поиска. Если символы совпадают, код
увеличивает значение счетчика i. Если нет, приходится проявить некоторую
ловкость рук, чтобы вернуть файл к состоянию, предшествующему этим
сопоставлениям.
Работа с файлами и каталогами 177
Возврат к предшествующему состоянию имеет решающее значение,
поскольку вы должны гарантировать, что возобновляете чтение файла с точки, в
которой началось сопоставление. В противном случае не только не будет
правильно работать механизм поиска; из конечного файла могут выпадать буквы,
слова и даже целые фразы. Восстановление состояния выполняет код внутри
блока if следующего условного оператора:
input.read(buffer, sizeof(buffer));
if (buffer[0] != searchstring[i]) {
Вспомните, что ранее мы вызывали функцию tellg(), чтобы в начале
итерации цикла получить текущий указатель файла. В данный момент указатель
файла продвинулся вперед на некоторое (неизвестное) расстояние от точки, в
которой он должен был бы находиться. Наиболее эффективным средством
восстановление правильной позиции является вызов функции seekgO с
переменной currentpos в качестве параметра. Затем программа читает из файла
одиночный символ, возвращая поток к состоянию, в котором он находился
непосредственно перед началом цикла сопоставления. Наконец, переменная match
устанавливается равной false, вследствие чего программа немедленно выходит
из цикла:
input.seekg(currentpos);
input.read(buffer, sizeof(buffer));
match ~ false;
}
else
i++;
}
}
После выхода из цикла while необходимо выяснить, прервался ли цикл
благодаря удачному или же неудачному сопоставлению. Для этого программа
проверяет состояние переменной match и реагирует соответственно. Если
match равна true, программа записывает в выходной файл всю строку замены
и устанавливает match равной false:
if (match) {
output.write(replacestring.c_str(), replacestring.length());
match - false;
>
Если же соответствие не установлено, программа просто записывает
одиночный символ из массива buffer. Важно отметить, что программа не
восстанавливает указатель файла в случае, когда сопоставление успешно. Внешний
цикл возобновляет чтение с первого символа после найденной (во входном
файле) строки поиска и, естественно, продолжает запись после текста замены
в выходном файле:
else
output.write(buffer, sizeof(buffer));
}
После того, как внешний цикл — проходящий по всему входному файлу —
закончит свою работу, код программы очищает переменные input и output,
закрывая потоки, которые с ними соединены. Затем, в целях ясности, програм-
178
Глава 6
ма открывает новый объект if stream, используемый ею для вывода на экран
содержимого полученного файла:
input.close();
output.close{);
ifstream display(outfilename.c_str(), ios::in);
(Как и в filter.cpp, здесь должна была бы выполняться проверка успешного
открытия.) После этого программа просто проходит, символ за символом, по
выходному файлу и посылает прочитанные символы в поток cout (если при
чтении не произошло ошибки):
while ('display.eof()) {
display.read(buffer, sizeof(buffer));
if (display.good())
cout « buffer[0]; <
}
По окончании цикла вывода программа закрывает поток и успешно
завершается:
display, close () ;
return 0;
}
Вы можете проверить работу этой программы, если запустите ее, например,
с файлом исходного сода tabspace.cpp. Однако затем вам придется загрузить
полученный файл в текстовый процессор (такой, как Word), или какую-то
другую программу, которая показывает индикатор табуляции. Это позволит
вам отличить первоначальные пробелы от табуляций, которыми вы их
заменили. Затем вы можете выполнить обратное преобразование и посмотреть
содержимое вновь созданного файла.
Работа с файловой системой
В сегодняшних сложных приложениях часто оказывается недостаточно
работать с одними файлами — нужно иметь возможность работать с файловой
системой, поддерживающей эти файлы. Хотя C++ предусматривает ряд
некоторые структуры высокого уровня, помогающие работать с файловыми
системами, большинство из них относится к категории интерфейсов,
экспонируемых (т. е. определенных в качестве доступных) API операционной системы,
используемой программистом. Среда разработки Windows дает тому
прекрасный пример: Win32 API предоставляет более 200 функций для управления и
взаимодействия с информацией, хранящейся на диске Windows.
Очевидно, эти функции слишком многочисленны, чтобы можно было их
здесь описать, — вам лучше обратиться к справочному руководству по Win32
API. Вместо этого мы сосредоточим внимание на одном приложении, которое,
возможно, окажется для вас полезным для перемещений по операционной
системе. Это приложение, WalkDirs, рекурсивно обходит все файлы в дереве
каталогов. »
Работа с файлами и каталогами
179
Код
Ядро проекта WalkDirs заключено в файле walkdirs.cpp.
■ | —-— - - ■ ..,» - ,..„,-„. _„„.
ttinclude "CmnHdr.H"
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
#include <cstdlib>
#include <cstdio>
#include <string>
#include "Resource.H"
, using namespace std;
'static BOOL IsChildDir (WIN32_FIND_DATA *lpFindData)
{
return(((lpFindData->dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY)
!= 0) &&
(lstrcmp(lpFindData->cFileName, _TEXT(".")) !=0) &&
(lstrcmp(IpFindData->cFileName, _TEXT("..")) ! = 0)) ;
}
t static BOOL FindNextChildDir (HANDLE hFindFile,
WIN32_FIND_DATA *lpFindData)
1
{
BOOL fFound = FALSE;
do
{
fFound = FindNextFile(hFindFile, IpFindData);
} while (fFound && !IsChildDir(IpFindData));
return(fFound);
■
1 ^static HANDLE FindFirstChildDir (LPTSTR szPath,
, i. WIN32_FIND_DATA *lpFindData)
BOOL fFound;
HANDLE hFindFile = FindFirstFile(szPath, IpFindData);
if (hFindFile != INVALID_HANDLE_VALUE)
{
fFound = IsChildDir(IpFindData);
if (!fFound)
| fFound = FindNextChildDir(hFindFile, IpFindData);
if (JfFound)
<
FindClose(hFindFile);
hFindFile = INVALID HANDLE_VALUE;
}
180
Глава 6
>
return (hFindFile);
>
// Данные, используемые WalkDirRecurse
typedef struct
{
HWND hwndTreeLB; // Дескриптор выходного окна списка
int nDepth; // Глубина вложенности
BOOL fRecurse; // TRUE, чтобы вывести подкаталоги
TCHAR szBuf[1000]; // Выходной буфер форматирования
int nlndent; // Счетчик символов отступа
BOOL fOk,- // Управляющий флаг цикла
BOOL flsDir; // Управляющий флаг цикла
WIN32_FIND_DATA FindData; // Информация о файле
} WALKDIRDATA, *LPWALKDIRDATA;
// Пройти по структуре каталогов и заполнить ListBox именами файлов.
// Если pDW->fRecurse установлен, распечатать все дочерние каталоги,
// рекурсивно вызывая WalkDirRecurse.
static void WalkDirRecurse (LPWALKDIRDATA pDW)
{
HANDLE hFind;
[ * pDW->nDepth++;
i ' pDW->nIndent = 3 * pDW->nDepth;
* ] _stprintf(pDW->szBuf, _TEXT("%*s"), pDW->nIndent, _TEXT(""));
I
i GetCurrentDirectory(chDIMOF(pDW->szBuf) - pDW->n!ndent,
i SpDW->szBuf[pDW->nIndent]);
[ ListBox_AddString(pDW->hvmdTreeLB, pDW->szBuf);
! H hFind = FindFirstFile(_TEXT("*.*"), &pDW->FindData);
" 1 pDW->fOk = (hFind != INVALID_HANDLE_VALUE) ;
; t while (pDW->fOk)
\ \ {
\ pDW->f!sDir = pDW->FindData.dwFileAttributes &
tЦ FILE_ATTRIBUTE_DIRECTORY;
? * if (1pDW->fIsDir ||
I (!pDW->fRecurse &£ IsChildDir(SpDW->FindData)))
{
i i _stprintf(pDW->szBuf,
j pDW->fIsDir ? _TEXT("%*s[%s]") : _TEXT("%*s%s"),
pDW->nIndent, _TEXT(""), pDW->FindData.cFileName>;
[ ListBox_AddString(pDW->hwndTreeLB, pDW->szBuf);
pDW->fOk = FindNextFile(hFind, &pDW->FindData);
}
if (hFind •- INVALID_HANDLE_VALUE)
FindClose(hFind);
if (pDW->fRecurse)
(
: 3
Работа с файлами и каталогами
181
Ь
i:
i
// Найти первый дочерний каталог
hFind = FindFirstChildDir(_TEXT("*.*"), &pDW->FindData);
pDW->fOk = (hFind != INVALID_HANDLE_VALUE);
while (pDW->fOk)
i
II Переключиться в дочерний каталог
if (SetCurrentDirectory(pDW->FindData.cFileName))
!. <
II Выполнить рекурсивный обход дочернего каталога.
// Помните, что некоторые элементы pDW будут
// переписаны этим вызовом.
WalkDirRecurse(pDW);
// Переключиться обратно в родительский каталог.
SetCurrentDirectory(_ТЕХТ(".."));
}
pDW->fOk = FindNextChildDir(hFind, &pDW->FindData);
}
if (hFind != INVALID_HANDLE_VALUE)
FindClose(hFind);
}
pDW->nDepth— ;
// Пройти по структуре каталогов и заполнить ListBox именами файлов.
// Эта функция организует вызов WalkDirRecurse, которая выполняет
// Всю действительную работу.
i
с
void WalkDir (HWKD hwndTreeLB, LPCTSTR pszRootPath, BOOL fRecurse)
<
static char szCurrDir[_MAX_DIR];
< WALKDIRDATA DW; // Создать экземпляр
* ^WALKDIRDATA
r J
■
// Очистить ListBox
ListBox_ResetContent(hwndTreeLB);
// Сохранить текущий каталог, чтобы восстановить его
//в дальнейшем.
GetCurrentDirectory(chDIMOF(szCurrDir), (LPTSTR) szCurrDir);
// Установить текущий каталог на точку, откуда нужно
// начать обход.
SetCurrentDirectory(pszRootPath);
// nDepth используется для управления отступами. При значении -1
// первый уровень отображается по левому краю.
DW.nDepth ■ -1;
DW.hwndTreeLB = hwndTreeLB;
DW.fRecurse = fRecurse;
// Вызвать рекурсивную функцию для обхода подкаталогов.
WalkDirRecurse(&DW);
// Вернуть текущий каталог к состоянию перед вызовом функции.
182
Глава 6
п.
SetCurrentDirectory((LPTSTR) szCurrDir);
\
4-
I 1
* --
L *
r.
ti
4
7'
BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam)
{
RECT re;
WalkDir(GetDlgItem(hwnd, IDC_TREE), _TEXT("\\"), TRUE);
GetClientRect(hwnd, &rc);
SetWindowPos(GetDlgItem(hwnd, IDC_TREE), NULL, 0, 0, re.right,
re.bottom, SWP_NOZORDER);
return(TRUE);
}
void Dlg_OnSize (HWND hwnd, UINT state, int ex, int cy)
{
SetWindowPos(GetDlgItem(hwnd, IDC_TREE), NULL, 0, 0, ex, cy,
SWP_NOZORDER);
)
void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT
CodeNotify)
{
switch (id)
{
case IDCANCEL:
EndDialog(hwnd, id);
break;
case IDOK:
// Вызвать рекурсивную процедуру для обхода дерева.
WalkDir(GetDlgItem(hwnd, IDC_TREE), _TEXT("\\"), TRUE);
break;
}
>
BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM IParam)
{
switch (uMsg)
<
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_SIZE, Dlg_OnSize);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
}
return(FALSE);
}
int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev,
LPSTR pszCmdLine, int nCmdShow)
{
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_WALKDIR), NULL, Dlg_Proc) ;
return(0);
}
Работа с файлами и каталогами 183
( ПРИМЕЧАНИЯ
Файл walkdirs.cpp вместе с другими файлами проекта WalkDirs
определяют рекурсивную программу, проходящую по всему жесткому диску (или
некоторому дереву диска, в зависимости от того, откуда вы начнете). Все
включаемые программой заголовки имеют довольно очевидный смысл.
#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
#include <cstdlib>
#include <cstdio>
#include <string>
#include "Resource.H"
using namespace std;
Первой функцией, определяемой в файле, является IsChildDir(), которая
принимает структуру типа WIN32_FIND_DATA и возвращает булево значение,
показывающее, является ли текущий пункт дочерним каталогом. О структуре
WIN32_FIND_DATA всегда важно помнить, работая с операционной
системой Windows, и чуть позже мы детально опишем ее.
static BOOL IsChildDir (WIN32_FIND_DATA *lpFindData)
<
return(((lpFindData->dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY) != 0) fi£
(lstrcmp(lpFindData->cFileName, JTEXT(n.")) !-0) &&
(lstrcmp(lpFindData->cFileName, _TEXT("..")) != 0));
}
Как вы можете видеть, оператор return проверяет элемент dwFileAttribu-
tes, определяя, является ли текущий пункт каталогом. Также проверяется,
что это не каталоги "." или "..", как они определяются операционной
системой. Если выполнены все эти условия, IsChildDir() возвращает true, в
противном случае — false.
Структура WIN32_FINDJDATA описывает файл, найденный функциями
API Win32 FindFirstFileO, FindFirstFileEx() и FindNextFile(). Структура
содержит 10 элементов, как показывает ее объявление:
typedef struct _WIN32_FIND_DATAA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReservedO;
DWORD dwReservedl;
CHAR cFileNarae[ MAX_PATH ];
CHAR cAlternateFileName[ 14 ];
} WIN32_FIND_DATAA;
Таблица 6.1 описывает детали структуры WIN32_FIND_DATA.
184
Глава 6
Таблица 6.1. Элементы данных WIN32_FIND_DATA
Элемент
dwFileAttributes
ftCreationTime
ftLastAccessTime
ftLastWriteTime
nFileSizeHigh
nRleSizeLow
dwRosorvedO,
dwReservedl
cFileName
cAltern ate File Name
Описание
Специфицирует атрибуты файлового объекта. Атрибуты файла
описывают, какой разрешен доступ к нему, файл это или каталог
и т. д. Этот элемент может содержать одно или несколько
значений, описанных в таблице 6.2.
Содержит структуру FILETIME (64-битное значение,
представляющее число 100-наносекундных интервалов,
прошедших с 1-го января 1601г.), специфицирующую время
первоначального создания файла операционной системой.
Структура имеет 2 элемента DWORD, которые функции Find
устанавливают в 0, если файловая система, содержащая файл,
не поддерживает этот элемент.
Содержит структуру FILETIME, специфицирующую время
последнего обращения пользователя к файлу. Как и в случае
ftCreationTime, оба ее элемента содержат 0, если файловая
система не предусматривает этой информации.
Содержит структуру FILETIME, специфицирующую время, когда
пользователем или процессом производилась последняя запись в
файл. Как и в случае ftCreationTime и ftLastAccessTime, оба ее
элемента содержат 0, если файловая система не
предусматривает этой информации.
Размер файла сохраняется операционной системой в двух
значениях DWORD, которые комбинируются в соответствии с
формулой
(nFileSizeHigh * MAXDWORD) + nFileSizeLow,
которая дает полный размер файла. (MAXDWORD обычно
равно 232-1.) Данный элемент специфицирует старшую часть
размера файла; он равен 0, если только размер файла не больше
MAXDWORD.
Специфицирует младшую часть размера файла. Такая структура
из.двух DWORD позволяет файлу иметь размер порядка терабайт
(согласитесь, такие попадаются нечасто).
Зарезервировано корпорацией Microsoft для будущего
использования.
Ограниченная нулем строка, содержащая длинное имя файла.
Его длина всегда меньше МАХ_РАТН, системной константы,
равной обычно 255 или 260.
Ограниченная нулем строка, содержащая альтернативное имя
файла. Это имя представлено в классическом формате 8.3
(filename. ext). Используйте этот элемент, если вы не уверены,
поддерживает ли каждая функция или система, обращающиеся к
файлу, длинные имена.
Как указано в таблице 6.1, элемент dwFileAttributes может содержать одно
или комбинацию из нескольких значений констант, характеризующих
свойства файла. Эти константы перечислены и описаны в таблице 6.2.
Работа с файлами и каталогами
185
Таблица 6.2. Значения констант для dwFileAttributes
Константа
FILE_ATTRIBUTE_ARCHIVE
FILE_ATTR1BUTE_C0MPRESSED
FILE_ATTRIBUTEJ)IRECTORY
FILE_ATTRIBUTE_HIDDEN
FILE_ATTRIBUTE_NORMAL
F!LE_ATTRIBUTE_OFFLINE
FILE_ATTRIBUTE_READONLY
FILE_ATTRIBUTE_SYSTEM
Описание
Приложение используют данное значение, чтобы
пометить файл для резервного копирования или
удаления. Всякий раз, когда файл изменяется,
Windows устанавливает архивный бит. Когда
программа резервного копирования архивирует
файл, она сбрасывает данный бит.
Для файла атрибут означает, что все данные в нем
сжаты, —т. е. это файл ZIP или он имеет подобный
сжатый формат. Для каталога атрибут означает, что
для новых файлов и подкаталогов сжатие
выполняется по умолчанию (т. е. операционная
система или какое-то инструментальное средство
сжимает новые файлы и подкаталоги во время
создания).
Файловый объект соответствует каталогу или
папке, —т. е. это логический объект операционной
системы, не содержащий собственных данных, а
служащий инструментом организации других
объектов.
Файл является скрытым, т. е. он не включается в
обычный листинг каталога, этот атрибут обычно
используется системными, вспомогательными или
другими файлами, которые разработчик приложения
хочет защитить от случайного обнаружения.
Файл обычный, допускающий чтение/запись, не
скрытый и не системный. Короче, это означает, что
все другие атрибуты файла не установлены и, таким
образом, данное значение действительно только в
случае, если используется оно одно.
Данные файла не являются непосредственно
доступными. Показывает, что данные были
физически перемещены для внешнего хранения.
Этот атрибут используется крайне редко, только в
некоторых сетевых средах.
Файл допускает только чтение. Приложения могут
его читать, но не могут модифицировать или
удалить. Приложение, которому требуется это
делать, должно либо обнулить сначала данный бит,
либо сохранить файл под другим именем.
Файл является частью операционной системы
(например, динамической библиотекой) либо
используется исключительно операционной
системой (например, ядро операционной системы).
Вообще модификация системных файлов — очень
нездоровое занятие, потому что в результате
Windows может начать работать неправильно или
даже перестанет загружаться.
186
Глава 6
Константа
FILE_ATTR1BUTE_TEMP0RARY
Описание 1
Файл используется для временного хранения. Этот
атрибут присваивается файлу, если он создается 1
функцией tmpfile() или любой из функций Windows I
для временных файлов, такой, как tmpnam{) и 1
_tempnam(). Временнные файлы, созданные 1
приложением, не являются «стабильными» — они I
должны быть стерты приложением при завершении. |
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Если файл имеет длинное имя, полный его вариант будет находиться в
поле cFileName. API в этом случае возвратит классический
(формат 8.3), усеченный его вариант в поле cAlternateFileName. Если
операционная система не поддерживает длинные имена файлов либо в
данном случае файл это имя не использует, элемент cAlternateFileName
будет пустым, a cFileName будет содержать имя в формате 8.3.
Чтобы получить версию имени формата 8.3, вы можете также вызвать
функцию API GetShortPathName(), которая возвращает 12-символь-
ную форматированную строку.
Итак, мы разобрали функцию IsChildDir(), которая выясняет,
соответствует ли конкретный объект типа WIN32_FIND_DATA дочернему каталогу. Эта
функция вызывается из FindNextChildDir(), функции, отыскивающей в
каталоге дерева следующий дочерний каталог. FindNextChiIdDir() принимает
дескриптор файла и объект WIN32_FIND_DATA.
static BOOL FindNextChildDir (HANDLE hFindFile,
WIN32 FIND_DATA *lpFindData)
<
BOOL fFound m FALSE;
FindNextChildDir() объявляет булеву переменную, используемую при
поиске следующего дочернего каталога. В функции имеется цикл do,
повторяющийся до тех пор, пока следующий файл в дереве не окажется дочерним
каталогом. При выполнении поиска код функции вызывает FindNextFiIe(),
которая обсуждается ниже. Цикл также заканчивается, если поиск не
обнаруживает подходящего файла; в этом случае вызывающей функции возвращается
false. Но если дочерний каталог найден, возвращается true:
do {
fFound = FindNextFile(hFindFile, lpFindData);
} while (fFound &£ 'IsChildDir(lpFindData));
return(fFound);
}
Функция FindNextFile() продолжает поиск файлов после того, как он был
инициирован вызовом функции FindFirstFiIe() или FindFirstFileEx().
Первым параметром является дескриптор, возвращенный инициирующим
вызовом; второй параметр — объект WIN32_FIND_DATA.
Работа с файлами и каталогами 187
Функцией, вызывающей FindNextChildDir(), является FindFirstChildDir(),
которая делает именно то, на что указывает ее имя. Она.принимает строковое
значение, соответствующее маршруту, с которого следует начать поиск, и
объект WIN32_FIND_DATA. Вот начало этой функции:
static HANDLE FindFirstChildDir (LPTSTR szPath,
WIN32_FIND_DATA *lpFindData)
{
BOOL fFound;
HANDLE hFindFile = FindFirstFile(szPath, lpFindData);
Вызов FindFirstFile() присваивает значение дескриптору hFindFile, который
программа потом использует для всех других поисков. Это значение будет либо
корректным дескриптором, либо INVALID_HANDLE_VALUE, показывая, что
не было найдено никакого действительного файла. В последнем случае функция
немедленно возвратит управление. Если же файл найден, программный код
рассматривает его на предмет того, является ли он дочерним каталогом.
if (hFindFile != INVALID_HANDLE_VALUE){
fFound - IsChildDir(lpFindData);
Если найденный файл — дочерний каталог, функция также завершается.
В противном случае она пытается обратиться к другим пунктам каталога, где
производится поиск, вызывая обсуждавшуюся ранее функцию FindNextChild-
Dir<):
if ('fFound)
fFound - FindNextChildDir(hFindFile, lpFindData);
Если вызов FindNextChildDir() неудачен, код закрывает дескриптор поиска
и завершает работу функции, возвращая INVALID_HANDLE_VALUE:
if (!fFound) {
FindClose(hFindFile);
hFindFile - INVALID_HANDLE_VALUE ,-
}
>
return (hFindFile);
}
Следующий раздел исходного кода определяет структуру, используемую
рекурсивными функциями поиска. Ее можно было бы определить и в начале
этого файла (или в заголовке); однако в целях ясности главы мы передвинули
ее описание значительно ниже, чтобы вы могли рассмотреть некоторые
функции низкого уровня, прежде чем углубиться в эту структуру.
typedef struct
{
HHND hwndTreeLB; // Дескриптор выходного окна списка
int nDepth; // Глубина вложенности
BOOL fRecurse; // TRUE, чтобы вывести подкаталоги
TCHAR szBuf[1000]; // Выходной буфер форматирования
int nlndent; // Счетчик символов отступа
BOOL fOk; // Управляющий флаг цикла
BOOL flsDir; // Управляющий флаг цикла
WIN32_FIND_DATA FindData; // Информация о файле
) WALKDIRDATA, *LPWALKDIRDATA;
188
Глава 6
Структура содержит поля, используемые рекурсивными функциями для
хранения информации о текущем состоянии процесса обработки. Таблица 6.3
описывает элементы WALKDIRDATA.
Таблица 6.3. Элементы данных WALKDIRDATA
Элемент
hwndTreeLB
n Depth
fRecurse
szBuf[1000]
nlndent
fOk
flsDir
FindData
Описание
Дескриптор выходного окна списка, в котором программа отображает
информацию о файлах.
Показывает глубину вложения текущего файла, т. е. насколько ниже
корня дерева он расположен.
Этот элемент устанавливается равным true, чтобы распечатать подкаталоги.
Выходной буфер форматирования для строки, описывающей файл.
Счетчик символов отступа — служит для наглядного отображения дерева.
Управляющий флаг цикла.
Управляющий флаг цикла.
Объект WIN32_FIND_DATA, содержащий информацию о файле. |
Следующие две функции являются центральными для программы.
Функция WalkDir() инициирует обход, в то время как WalkDirRecurse()
производит рекурсивную обработку, связанную с движением вниз по дереву.
WaIkDirRecurse() проходит по структуре каталогов и заполняет окно List-
Box именами файлов. Если установлен pDW->fRecurse, функция создаст
листинг и всех дочерних каталогов, рекурсивно вызывая саму себя. Функция
принимает указатель на структуру WALKDIRDATA. Она также определяет
HANDLE для хранения дескриптора искомого файла.
static void WalkDirRecurse (LPWALKDIRDATA pDW)
{
HANDLE hFind;
Всякий раз, когда функция вызывается, она увеличивает счетчик nDepth
(поскольку программа достигла нового уровня дерева), а также увеличивает
значение nlndent, определяя новую величину отступа в листинге. Затем она
форматирует строку szBuf соответствующим числом пробелов.
pDW->nDepth++;
pDW->nIndent ~ 3 * pDW->nDepth;
_stprintf(pDW->szBuf, _ТЕХТ(и%*з"), pDW->nIndent, _TEXT(""));
Затем программный код вызывает функцию API GetCurrentDirectory(),
которая возвращает имя текущего каталога и помещает его в поле szBuf,
начиная с позиции nlndent символьного массива. Наконец, функция добавляет
содержимое szBuf в окно списка, на которое ссылается дескриптор hwndTreeLB:
GetCurrentDirectory(chDIMOF(pDW->szBuf) - pDW->nIndent,
&pDW->szBuf[pDW->nIndent]);
ListBox_AddString(pDW->hwndTreeLB, pDW->szBuf);
После всей этой предварительной обработки программа вызывает функцию
'FirstFileQ, передавая ей строку универсального сопоставления в качестве
Работа с файлами и каталогами
189
имени искомого файла. Вы можете, конечно, модифицировать программу,
чтобы она вызывала эти функции с именем файла, указанным пользователем.
Но в целях демонстрации мы просто возвращаем все имеющиеся имена. Find-
FirstFile() устанавливает элемент fOk равным true или false в зависимости от
того, вернула ли функция действительный дескриптор файла или константу
INVALID_HANDLE_VALUE. Если fOk равно true, программа входит в цикл
while и выполняет его, пока fOk остается true.
hFind = FindFirstFile(_TEXT("*.*"), &pDW->FindData);
pDW->fOJc = (hFind != INVALID__HANDLE__VALUE) ;
while (pDW->fOk) {
Первый оператор, исполняемый внутри цикла, по элементу
dwFileAttributes определяет, является ли текущий файл каталогом, присваивая результат
полю flsDir. Затем выполняется проверка flsDir вместе с fRecurse и
значением, возвращаемым IsChildDir(), чтобы решить, нужно ли выводить файл в
окно списка. Если файл — не каталог или рекурсия запрещена, программный
код выводит файл в окно списка.
pDW->f!sDir = pDW->FindData.dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY ;
if (!pDW->fIsDir || (!pDW->fRecurse SS
IsChildDir(SpDW->FindData)))
{
_stprintf(pDW->szBuf,
pDW->fIsDir ? __TEXT("%*s[%s]") : JTEXTC^sSfes") ,
pDW->nIndent, _TEXT(M"), pDW->FindData.cFileName);
ListBox_AddString(pDW->hwndTreeLB, pDW->szBuf);
}
После оператора if программный код вызывает FindNextFile() для поиска
следующего файла:
pDW->fOk = FindNextFile(hFind, £pDW->FindData);
}
Когда управление покинет цикл, программа закрывает дескриптор, если он
действителен. Затем она проверяет состояние флага рекурсии. Если он
установлен, вызывается функция FindFirstChildDir() для отыскания дочерних
каталогов, которые нужно обойти:
if (hFind != INVALID_HANDLE_VALUE)
FindClose(hFind);
if (pDW->fRecurse) {
hFind = FindFirstChildDir(_TEXT("*.*"), &pDW->FindData);
Далее программа снова присваивает полю fOk значение, соответствующее
успеху или неудаче поиска. Если поиск успешен, программа входит в
следующий цикл while:
pDW->fOJc = (hFind != INVALID_HANDLE_VALUE) ;
while (pDW->fOk) {
Однако обработка внутри этого цикла немного отличается от того, что вы
видели раньше, — делается попытка установить текущий каталог именем
дочернего каталога. В случае успеха выполняется рекурсивный вызов функции.
Окончание рекурсии, естественно, попадает в тот же блок if. При этом, как и
190
Глава 6
следовало ожидать» текущий каталог поднимается на один уровень вверх по
дереву:
if (SetCurrentDirectory(pDW->FindData.cFileName)) {
WalkDirRecurse(pDW);
SetCurrentDirectory (JFEXTC.."));
)
Затем (после выполнения всех рекурсивных вызовов и подъема в исходную
точку дерева) программа ищет следующий дочерний каталог. Если он найден,
цикл повторяется:
pDW->fOk - FindNextChildDir(hFind, fipDW->FindData);
>
В конце функции мы снова проверяем, содержится ли в hFind
действительный дескриптор файла» и если да, вызываем FindClose(). Заметьте, что если
вызвать FindClose() с недействительным дескриптором, программа аварийно
завершится. (Для этой проверки вы могли бы также организовать блок try...catch.)
if (hFind •= INVALID_HANDLE_VALUE)
FindClose(hFind);
)
Наконец, перед возвратом из функции — это происходит только когда
программа обошла все файлы в данной ветви — программа уменьшает поле nDepth,
так как мы возвращаемся на один уровень вверх по дереву:
pDW->nDepth--;
)
Функция WalkDir() является инициатором для WalkDirRecurse() — она
устанавливает исходное состояние, устанавливает начальный каталог и
производит начальный вызов WalkDirRecurse(). В WalkDir() управление и вернется
после того, как рекурсивная функция обойдет все возможные маршруты.
WalkDir() принимает дескриптор окна списка, строку, соответствующую
корневому маршруту, и булево значение, определяющее, будет ли
выполняться рекурсия при поиске. Функция объявляет статический массив размера
_MAX_DIR, который определяется в заголовке windows.h равным 260-ти
символам. Наконец, WalkDir() объявляет экземпляр WALKDIRDATA,
используемый оставшейся частью программы:
void WalkDir (HWND hwndTreeLB, LPCTSTR pszRootPath, BOOL fRecurse)
<
static char szCurrDir[_MAX_DIR];
WALKDIRDATA DW;
После этого функция WaIkDir() выполняет подготовительную работу. Она
очищает окно списка и получает от операционной системы текущий каталог.
Каталог сохраняется в szCurrDir для восстановления при выходе из функции.
Затем для текущего каталога устанавливается значение pszRootPath:
ListBox_ResetContent(hwndTreeLB);
GetCurrentDirectory(chDIMOF(szCurrDir), (LFTSTR) szCurrDir);
SetCurrentDirectory(pszRootPath);
Затем функция инициализирует некоторые элементы объекта DW. Полю
nDepth присваивается -1 (так что на первой итерации WalkDirRecurseQ оно бу-
Работа с файлами и каталогами
191
дет равно 0). В hwndTreeLB устанавливается дескриптор окна списка. Наконец,
код устанавливает fRecurse значением, полученным в качестве параметра:
DW.nDepth = -1;
DW,hwndTreeLB = hwndTreeLB;
DW.fRecurse — fRecurse;
После установки начальных значений вызывается функция WalkDirRecur-
se(), которой передается объект 0W. Далее вся обработка проходит в этой
рекурсивной функции. Когда управление вернется в WaIkDir(), ее код снова
вызовет SetCurrentDirectoryQ, чтобы вернуть операционную систему к
состоянию, в котором она находилась перед тем, как программа начала поиск:
WalkDirRecurse (&DW) ;
SetCurrentDirectory((LPTSTR) szCurrDir);
}
Функция OnlnitDialogO вызывается оболочкой, когда программа
отображает панель диалога, определенную в файле ресурсов. Основной задачей
функции является вызов WalkDir() с передачей дескриптора окна списка,
размещенного в этой панели.
BOOL DlgjOnlnitDialog (HWND hwnd, HWND hwndFocus, LPARAM 1Param)
{
RECT re;
WalkDir(GetDlgItem{hwnd, IDC_TREE), _TEXT("\\"), TRUE);
GetClientRectfhwnd, fire);
SetWindowPos(GetDlgltern(hwnd, IDCJTREE), NULL, 0, 0,
re.right, re.bottom, SWP_NOZORDER);
return(TRUE);
}
Событие OnSize() вызывается всякий раз, когда пользователь изменяет
размер диалоговой панели. Задачей OnSize() является перерисовка окна списка с
его содержимым в случае, если оно сместилось или было скрыто:
void DlgjOnSize (HWND hwnd, UINT state, int ex, int cy)
{
SetWindowPos(GetDlgItem(hwnd, IDC_TREE), NULL, 0, 0,
ex, cy, SWP_NOZORDER);
}
Функция OnCommand() обрабатывает сообщения, посланные диалоговой
панели. В данном приложении нас интересуют только два из них — то, что
закрывает диалоговую панель (ID_CANCEL) и то, что перезапускает программу
поиска (Ш_ОК). Оба сообщения обрабатываются в одном операторе switch:
void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT
CodeNotify)
<
switch (id) {
case IDCANCEL:
EndDialog(hwnd, id);
break;
При получении ID_CANCEL программный код закрывает диалог вызовом
EndDialogO, в результате чего закрывается и программа.
192
Глава 6
Если получено ГО_ОК, программа вызывает функцию WalkDir(),
передавая ей необходимую информацию:
case IDOK:
WalkDir(GetDlgIt«n(hwnd, IDC_TREE) , _ТЕХТ("\\"), TRUE);
break;
)
)
Возвратно-вызываемая функция Dlg_Proc() принимает сообщения
операционной системы. Для их обработки она использует макросы, определенные в
заголовке CmnHdr.h:
BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM 1Param)
{
switch (uMsg) {
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WMJ3IZE, Dlg_pnSize);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg__OnCommand);
}
return(FALSE);
}
Наконец, ниже показана функция WinMain(), которая не делает ничего
кроме создания панели диалога и выхода из программы с кодом успешного
завершения, когда диалог закрывается:
int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstPrev,
LPSTR pszCmdLine, int nCmdShow)
{
DialogBox(hinstExe, MAKEINTRESOURCE(IDD_WALKDIR), NULL, DlgJProc) ;
return(0);
}
Программа WalkDirs является полезным примером манипулирования
операционной системой. Вряд ли подобное понадобится вам в ваших собственных
программах, однако этот пример показывает, как получить доступ к системе,
читая и передавая информацию о файлах и каталогах.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Может случиться, что программа WalkDirs не будет правильно
работать в Windows NT, поскольку она не учитывает модель защиты этой
системы. В таком случае вам придется внести в программу небольшие
изменения.
Последняя программа, которую мы разберем в этой главе, описана в
следующем разделе и имеется на прилагаемом диске в двух модификациях — с
выводом на консоль и с выводом в окно Windows. Однако логика этих
программ идентична, так что мы изучим только вариант, ориентированный на
консоль.
Работа с файлами и каталогами _____„ 193
Просмотр содержимого файла
Одной из самых полезных разновидностей утилит для многих
программистов и опытных пользователей являются программы, позволяющие
просматривать (и часто — редактировать) содержимое файла в шестнадцатеричном
представлении. Написание шестнадцатеричного редактора выходит за рамки
^ задач этой книги, но разбор программы, читающей файлы и отображающей их
содержимое, будет и полезным, и в то же время простым. Такими
программами являются doshex.cpp и winhex; мы рассмотрим код программы doshex.cpp,
поскольку она работает непосредственно с потоками.
Код
Ниже приводится код программы doshex.cpp.
#include <iostream>
#include <fstream>
#include <iomanip>
#define numcols 18
using namespace std;
int main(int argc, char *argv[])
|{
! int counter =0, j=0, i*0;
j char text[numcols];
t i
; if (argc != 2) {
I i cout « "Usage for DOSHEX: doshex <filename>" « endl;
return 1;
■ 1
\ ifstream incoming(argv[l], ios::in | ios::binary);
if ((incoming) {
I " cout « "Cannot open file for display!" « endl;
i return 1;
>
' I
*
с.
!
X
cout.setf(ios::uppercase);
while (!incoming.eof()) {
for (i = 0; (i < numcols && 'incoming.eof()); i++)
incoming.get(text[i]);
S I if (i < numcols)
\
x-
h
for (j - 0; j < i; j++)
[ ■ cout « setw(3) « hex « (int) text[j];
j H for (; j < numcols; j++)
Li COUt « "
7 Зах.1208
194
Глава 6
Н-.
cout « "\t";
for (j - 0; j < i; j++)
if ((isprint(text[j])))
:^fl cout « text[j] ;
I "j else
ft ' cout « ".";
Л ■"!} cout « endl;
\
Щ
•j
I ■ j
£'■■
if (++counter == 24) {
counter =0;
cout « "Press Enter/Return to Continue...
cin.get();
cout « endl;
)
}
incoming.close()>
return 0;
( X\P\
ПРИМЕЧАНИЯ
Программа doshex.cpp использует прием, который мы уже применяли —
чтение символов из файла по одному, — но для прочитанных символов она
генерирует вывод на экран, соответствующий как шестнадцатеричному, так и
текстовому их представлению. Так как мы работаем с потоками, необходимо
включить заголовки iostream и fstream. Потребуется также заголовок ioma-
nip, поскольку мы будем вызывать некоторые манипуляторы потоков.
Программа определяет константу numcols, которая задает число отображаемых на
экране колонок. Мы определили это число как константу, чтобы вы могли
легко модифицировать метод отображения информации в зависимости от
разрешения экрана консоли.
#include <iostream>
#include <fstream>
#include <iomanip>
#define numcols 18
using namespace std;
Вся обработка данных происходит в функции main(). Функция принимает
параметры командной строки, что необходимо, так как нам нужно сообщить
программе, что она будет обрабатывать. Внутри функции мы объявляем
несколько переменных-счетчиков и массив char, в котором будет формироваться
текстовый вывод для каждой строки экрана. Вот объявления в main():
int main(int argc, char *argv[])
i
int counter =0, j = 0, i = 0;
char text[numcols];
Затем мы убеждаемся, что пользователь при запуске ввел имя файла, и
ничего больше. Если пользователь запустил программу с неправильной
командной строкой, она выводит сообщение о правильном формате команды:
Работа с файлами и каталогами
195
if (argc != 2) {
cout « "Usage for DdSHEX: doshex <filename>" « endl;
return 1;
}
При правильном запуске программы она инициализирует переменную
incoming типа if stream. Заметьте, что мы специфицируем входной файл как
двоичный, — важный момент, поскольку нам нужно извлекать из файла
действительные, а не интерпретированные значения. Если программа не может
открыть файл, она также выводит соответствующее сообщение и завершается.
ifstream incoming(argv[l], ios::in | ios::binary);
if (!incoming) {
cout « "Cannot open file for display!" « endl;
return 1;
)
После этого мы входим в цикл чтения, в котором программа проходит по
всему содержимому файла, читая его символы блоками. Внутри цикла while
имеется вложенный цикл for, который читает numcols символов, если только
ему не встретится маркер eof — в этом случае управление немедленно
покидает цикл. Во внутреннем цикле программа вызывает функцию gct(), читая
значения из файла по одному.
while (!incoming.eof()) {
for (i = 0; (i < numcols && {incoming.eof()); i++)
incoming.get(text[i]);
Выйдя из цикла for, программа выясняет, по какой причине завершился
цикл — в результате заполнения буфера или из-за встреченного конца файла.
Если достигнут конец файла, программа уменьшает счетчик на единицу
(чтобы избежать попытки отображения маркера конца файла).
if (i < numcols)
з.--;
В следующем вложенном цикле программа выводит шести ад цатеричные
значения для только что прочитанных байтов. Манипулятор setw() задает
ширину поля для вывода числа (в данном случае три позиции). Hex указывает,
что вывод должен производиться в шестнадцатеричной форме, а не в
десятичной или восьмеричной. Наконец, текущий символ приводится к типу int и
передается в поток:
for (j - 0; j < i; j++)
cout « setw(3) « hex « (int) text[j];
После выхода из этого цикла программа выполняет при необходимости
второй цикл, помещающий в поток заполнители-пробелы, чтобы текстовые
данные в правой части экрана всегда отображались правильно, в том числе для
последней строки файла:
for (; j < numcols; j++)
cout « "
После этого программы посылает в поток символ табуляции и входит в
третий цикл. Он распечатывает текстовые значения элементов массива, так что
пользователь может видеть рядом сразу и шестнадцатеричное, и текстовое
представление данных:
196
Глава 6
cout « "\t";
for (j = 0; j < i; j++)
В цикле функция isprint() проверяет, является ли значение текущего
элемента отображаемым символом — если нет, программа печатает вместо него
точку (.):
if ((isprint(text[j]))>
cout « text[j];
else
cout « ".";
После вывода текстовых значений программа посылает в поток пару CR/LF;
она также проверяет значение переменной counter, соответствующее числу
выведенных строк. Если переменная равна 24, выводится подсказка,
приостанавливающая вывод, пока пользователь не нажмет клавишу; это
предотвращает уход информации за верхний край экрана:
cout « endl;
if (++counter = 24) (
cout « "Press Enter/Return to Continue...";
cin.get();
counter = 0;
После того, как пользователь нажмет клавишу, счетчик обнуляется, что
позволяет программе вывести следующие 24 строки. После вывода всего
содержимого файла программа закрывает его и возвращает управление системе с
кодом успешного завершения:
cout « endl;
}
}
incoming.close() ;
return 0;
}
Если вы запустите программу doshex для ее собственного исходного файла
doshex.cpp, на экран будет выведено примерно следующее:
23
ЗЕ
61
74
6F
65
65
69
D
72
20
72
30
6D
72
6F
44
69
D
69
D
6D
79
6D
20
20
6Е
А
67
D
20
зв
63
67
75
4F
бс
А
6Б
А
ЗЕ
70
61
ЗС
6Е
67
D
63
А
3D
D
6F
63
74
53
65
20
63
23
D
65
6Е
63
75
20
А
2С
7B
20
А
6С
20
20
48
6Е
20
6С
69
А
ЗЕ
69
73
60
6Е
69
20
D
30
20
73
21
ЗС
45
61
20
75
6Е
2F
D
70
74
63
61
6Е
63
А
2С
20
5D
3D
ЗС
58
6D
20
64
63
2F
А
ЗЕ
64
6F
6D
74
68
20
20
63
ЗВ
20
20
ЗА
65
72
65
6С
23
23
D
69
6С
65
20
61
20
6А
68
D
32
22
20
ЗЕ
65
20
75
69
69
А
6F
73
73
6D
72
69
20
61
А
29
55
20
22
74
ЗС
64
6Е
6Е
2F
ЗЕ
20
70
61
20
6Е
3D
72
D
20
73
64
20
75
69
65
63
63
2F
D
31
61
69
2А
74
20
20
А
7B
61
6F
ЗС
72
6F
20
6С
6С
23
А
38
63
6Е
61
20
30
74
20
D
67
73
ЗС
6Е
73
ЗС
75
75
69
23
D
65
28
72
63
2С
65
20
А
65
68
20
20
74
66
64
64
6Е
64
А
20
69
67
6F
20
78
69
20
20
65
65
31
72
73
65
65
63
65
D
73
6Е
76
75
69
74
66
20
66
78
6Е
зв
65
74
20
20
6С
66
А
74
74
5В
6Е
20
5В
20
20
6F
20
64
D
61
72
ЗС
ЗС
75
69
75
64
20
5D
74
3D
6Е
28
20
72
ЗС
6С
А
6D
65
63
69
64
6Е
73
ЗВ
61
29
65
20
75
61
63
20
66
ЗВ
20
#include <iostream
>..#lnclude <fstre
am>..//#include <c
type>..#include <i
oraanip>..//#includ
e <cstdio>..#defin
e muncols 18....us
ing namespace std;
....int main(int a
rgc, char *argv[])
..{.. int counte
r = 0, j = 0, i =
0;.. char text[nu
mcols];.... if (a
rgc != 2) {.. с
out « "Usage for
DOSHEX: doshex <f
ilename>" « endl;
return 1;..
Работа с файлами и каталогами
197
20 7D
6Е 63
6F 73
72 79
69 6Е
D
6F
ЗА
29
67
А
6D
ЗА
ЗВ
29
D
69
69
D
20
А
6Е
6Е
А
7В
Press Enter/Return
ЗС 20
65 20
20 65
20 31
2Е 73
73 65
63 6F
20 20
ЗС 20
6D 69
А 20
74 28
20 69
А 20
20 66
ЗВ 20
20 ЗС
20 ЗС
D А
75 6D
20 20
D А
ЗВ D
20 6А
22
66
6Е
ЗВ
65
29
6D
20
6Е
6Е
20
74
66
20
6F
6А
ЗС
ЗС
20
63
63
20
А
20
43
6F
64
D
74
ЗВ
69
66
75
67
20
65
20
20
72
2В
20
20
20
6F
6F
20
20
ЗС
61
72
6С
А
66
D
6Е
6F
6D
2Е
20
78
28
20
20
2В
73
28
20
6С
75
20
20
20
6Е
20
ЗВ
20
28
А
67
72
63
65
20
74
69
20
28
29
65
69
20
73
74
20
20
69
Press Enter/Return
20 20
74 5В
6Г 75
20 20
20 63
20 63
20 20
3D 3D
75 6Е
63 6F
65 72
6Е 75
6Е 2Е
75 74
D А
63 6С
20 30
69
6А
74
20
6F
6F
20
20
74
75
2F
65
67
20
20
6F
ЗВ
66
5D
20
20
75
75
20
32
65
74
52
2Е
65
ЗС
20
73
D
20
29
ЗС
20
74
74
69
34
72
20
65
2Е
74
ЗС
7D
65
А
28
29
ЗС
65
20
20
66
29
20
ЗС
74
2Е
28
20
D
28
7D
20
67
20
20
D
to
6Е
64
D
20
69
20
2Е
20
6F
6F
20
5В
20
20
6А
D
74
6Е
66
ЗВ
20
63
20
ЗВ
to
28
29
20
6С
ЗС
ЗС
20
20
3D
ЗС
75
22
29
65
А
29
20
28
7С
20
А
69
61
20
69
20
66
72
69
66
20
Continue.
6F
69
А
7D
6F
20
65
28
бс
66
69
69
ЗС
69
20
А
77
74
6F
20
ЗС
6F
66
20
74
73
20
D
73
77
6F
69
73
28
6Е
5D
20
2D
3D
20
28
29
72
6А
ЗС
75
6F
6А
20
70
20
А
ЗА
68
66
20
20
29
63
29
6Е
2D
20
20
33
20
20
2В
20
74
72
2В
Continue.
69
D
74
73
ЗС
ЗС
28
7В
20
20
72
ЗВ
ЗВ
6Е
20
ЗВ
73
А
65
65
20
20
2В
D
30
22
6Е
D
D
64
20
D
70
20
78
D
22
65
2В
А
ЗВ
50
20
А
А
6С
69
А
73
67
6F
20
20
, . .
6F
бс
20
D
ЗА
69
28
3D
26
29
6F
ЗВ
75
ЗВ
30
20
29
74
28
2В
22
20
20
2В
■ . .
72
20
74
А
2Е
6Е
63
20
D
72
74
20
20
ЗВ
6Е
20
74
76
73
28
20
70
61
20
А
75
6С
29
20
26
ЗВ
6D
D
6D
D
ЗВ
20
20
65
ЗВ
29
20
ЗС
28
29
69
20
5В
20
22
64
6F
20
А
65
6F
20
20
D
63
20
72
5В
ЗА
21
63
65
79
72
20
70
65
29
30
20
20
69
А
63
А
20
20
ЗС
78
20
D
20
ЗС
6А
D
6Е
20
6А
20
ЗВ
6С
75
20
20
73
20
20
20
А
6F
72
65
31
ЗА
69
6F
6Е
21
65
20
70
20
20
ЗВ
21
69
6Е
D
6F
D
6А
20
ЗС
74
6А
А
20
20
20
А
74
20
5D
20
D
ЗВ
6Е
20
20
73
43
20
20
20
6D
65
61
5D
62
6Е
75
20
22
74
63
65
28
7В
20
69
2В
67
А
6С
А
20
63
20
5В
20
20
22
22
3D
20
28
20
ЗВ
20
А
D
74
20
20
20
6F
20
20
20
69
74
6D
2С
69
63
74
66
20
75
6F
72
21
D
28
6Е
2В
2Е
20
73
20
ЗС
6F
68
6А
ЗС
20
ЗВ
5С
20
20
74
20
D
20
20
А
65
20
20
45
6Е
20
20
20
6Е
75
20
20
6Е
6F
20
69
ЗС
72
75
63
69
А
69
63
29
67
20
29
20
20
75
65
5D
20
20
D
74
30
20
65
20
А
20
20
D
72
63
20
6Е
74
63
63
20
67
72
69
69
61
6D
ЗС
6С
ЗС
6Е
74
61
6Е
20
20
6F
D
65
20
D
20
69
74
78
ЗВ
6Е
20
А
22
ЗВ
20
78
63
20
20
20
А
20
6F
20
74
69
69
6F
7D
2Е
6Е
}. . .. ifstream i
ncoming(argv[l], i
os::in | ios::bina
ry);.. if (!incom
ing) {., cout <
< "Cannot open fil
e for display!" «
endl;.. return
1;.. }. . . . cout
.setf(ios::upperca
se);.. while (!in
coming.eof ()) { ..
for (i * 0; (i
< numcols && !inco
ming.eof()); i++).
incoming.ge
t(text[i]);
if (i < numcols).
i~;
for (j = 0; j < i
; j++)- - cout
« setw(3) « hex
« (int) text[j];
for (; j < n
umcols; j++)..
cout « "
cout « "\t"
for (j = 0;
j < i; j++)..
if ((isprint(tex
t[j]))).. с
out « text[j];..
else..
cout « ".";..
cout « endl;....
if (++counter
-= 24) {.. со
unter = 0;,.
cout « "Press Ent
er/Return to Conti
nue...";.. ci
n.get () ;. . со
ut « endl;.. )
}.. incoming.
closed ; ■ ■ return
0;..)
Вариант для Windows генерирует сходный вывод, помещая его в окно
вместо того, чтобы посылать на консоль. Поэтому вы можете двигаться вниз и
вверх по файлу. Если вы запустите Windows-вариант программы для файла с
исходным кодом, то увидите нечто вроде показанного на рис. 6.1.
198
Глава 6
Экран
Windows-программы , _
просмотра файлов
ГЛАВА
тш
-^
Фи v
1*
tiaMmmmimmtlm
el
■ 'JA Л
«ч^
simplel.cpp
SimpXOR.cpp
ij?f4.
Ларе Кландер
#i
CryptoNotes
CCryptoDoc
i & ф. Ж В,
1„1*1
J4^->*-
200
Глава 7
Сегодня компании хранят на компьютерах все больше и больше
информации. С каждым днем как компании, так и отдельные лица передают все
больше цифровых данных через Internet и по другим информационным
каналам, таким, как локальные сети корпораций и радиочастотные системы связи.
Поскольку эта тенденция сохраняется, все более важной становится защита
передаваемой информации. Чтобы защитить ее от просто посторонних и от
злонамеренных «хакеров», большинство компаний и пользователей
компьютеров прибегают к шифровке.
Если обратиться к истории, то мы увидим, что системы шифровки
использовали два ключа, один на передающем конце линии связи и другой — на
принимающем. В этих шифрах ключи содержались в кодовых книгах. Обе
стороны имели совершенно одинаковые экземпляры такой книги. Другими
словами, в определенный момент одна сторона должна была передать кодовую
книгу (содержащую ключи) другой стороне.
Мощность современных компьютеров изменила подход к шифровке.
Введение шифросистем с общими ключами позволило пользователям компьютеров
шифровать документ и другие данные для передачи другим пользователям, не
заботясь о том, чтобы заранее иметь нужные ключи, или посылать кодовую
книгу, и вообще без каких-либо ограничений, свойственных старомодным
системам шифровки. Вместо этого, используя систему общих ключей,
пользователи могут публиковать свои общие ключи где угодно, а другие могут их
использовать для шифровки документов. Пользователь, обладающий личным
ключом, соответствующим общему, будет единственным лицом, которое
сможет прочитать зашифрованный общим ключом документ.
В этой главе вы познакомитесь с основными принципами шифровки. С
точки зрения программиста существует два типа шифров — поточные и блочные
системы шифровки. Вы рассмотрите три программы. Первая реализует очень
простую модель шифровки; ее можно «взломать» с помощью бумаги и
карандаша. Вторая программа демонстрирует несколько более надежную модель
шифровки, полезную в ситуациях, когда вы не хотите, чтобы кто угодно мог
запросто заглянуть в данные, — модель, которая, тем не менее, быстро может
взломана с помощью компьютера. Наконец, вы увидите более
распространенную реализацию техники шифровки: применение коммерческой ключевой
структуры, которая производит действительную обработку и предусматривает
удобные средства включения в ваши программы надежной системы
шифровки. Хотя третий пример ориентирован специально на платформу Win32, для
систем Macintosh и UNIX существуют коммерческие библиотеки,
предоставляющие программисту похожие инструменты шифровки.
Эта глава не обсуждает математическую специфику моделей шифровки и
даже специфику реализации моделей, защищенных авторским правом, вроде
DSS (Стандарта цифровой подписи). Вы можете найти массу других книг с
изложением соответствующей математики. Здесь мы сосредоточим внимание на
том, как можно включить шифровку в свою собственную программу.
Что такое шифровка
Предположим, вы хотите послать своему кузену конфиденциальное
сообщение через Internet. Другими словами, вы не хотите, чтобы кто-то перехва-
Основы шифровки 201
тивший сообщение смог его прочитать. Чтобы защитить посылаемое
сообщение, вы зашифруете или закодируете его. Вы делаете это с помощью сложной
системы изменения букв сообщения, с целью сделать его не доступным ни для
кого, кроме вашего кузена. Хотя теоретически эта цель недостижима —
любой, кто располагает знаниями, временем и вычислительными мощностями,
может в принципе раскрыть код, — существует некоторый уровень
сложности, но достижении которого можно считать, что система достаточно надежна.
Вы дадите кузену ключ к шифру, позволяющий расшифровать сообщение и
сделать его читаемым. В системе шифра с одним ключом вы с кузеном должны
иметь один и тот же ключ до того, как станете шифровать сообщения (вы
можете послать ключ в другом электронном сообщении, отправить по почте и т. д.).
Например, простая система с одним ключом может сдвигать каждую букву
сообщения вперед на три позиции по алфавиту, а пробел предполагается
расположенным сразу за буквой Z. Слово DOG, к примеру, превратится в GRJ. Рис. 7.1
показывает строку документа, закодированную одноключевым шифром.
THIS IS A SIMPLE CRYPTOSYSTEM
WKLVCLVCDCVLPSOHCFUASWRVWAWHP
Рис. 7.1. Строка текста, закодированная простым шифром
Ваш кузен получит закодированное сообщение и сдвинет его буквы обратно
на три позиции по алфавиту, в результате чего GRJ снова превратится в DOG.
Рис. 7.2 показывает ключ для простой системы, который вы используете для
отправки сообщения кузену.
А
D
В
Е
С
F
D
G
Е
Н
F
I
G
J
Н
К
I
L
J
М
К
N
L
О
М
Р
N
Q
О
R
Р
S
Q
Т
R
и
S
V
т
W
и
X
V
Y
W
Z
X
-
Y
А
Z
В
.
с
Рис. 7.2. Ключ шифровки/дешифровки для простого одноключевого шифра
Код
Давайте рассмотрим простую программу simplel.cpp, которая применяет
только что показанный метод к строке, введенной пользователем.
#include <string>
#include <cstdio>
#include <iostream>
#include <fstream>
using namespace std;
char *strencrypt(char *source)
{
char ^original = source;
while (*source) {
*source = *source + 3;
| source++;
I }
202
Глава 7
[ return(original);
}
char *strdecrypt(char *source)
{
char *original = source; '
while (*source) {
♦source = *source - 3;
source++;
)
return(original);
}
int main()
<
char linein[81], converted[81];
int encryptdecrypt = 1;
int i;
ifstream in, inclear;
cout « "Enter 1 to encrypt, 2 to decrypt: ";
cin » encryptdecrypt;
if (encryptdecrypt — 1)
in.open("test-enc.txt") ;
else
in.open("test-dec.txt");
if(!in) {
cout « "Cannot open file." « endl;
return (1) ;
}
while (!in.eof()> {
for (i=0; i < 80 66 !in.eof(); i++)
. 4 in.get(linein[i]);
» -j linein[i] = NULL;
! i if (encryptdecrypt == 1)
1 l strcpy(converted, strencrypt(linein));
'1' ] else
, | strcpy(converted, strdecrypt(linein));
cout « converted « endl;
t I
•1
■ 4
in.close();
cout « endl « endl;
if (encryptdecrypt == 1)
, j inclear.open("test-enc.txt");
" j else
I'.^j inclear.open("test-dec. txt") ;
У •• if (Unclear) {
I j cout « "Cannot open file." « endl;
■ J return (1);
xJ )
Основы шифровки 203
while (!inclear.eof(>) {
for (i=0; i < 80 && !inclear.eof(); i++)
inclear.get<linein[i]);
linein[i] = NULL;
t cout « linein « endl;
}
inclear.close();
return 0;
}
| ПР»
ПРИМЕЧАНИЯ
Первая функция в приведенном коде — strencrypt(), которая возвращает
указатель на начало массива char. Вот код этой функции:
char *strencrypt(char *source)
<
char ^original — source;
while (*source) {
*source = *source + 3;
source++;
}
return(original);
}
Обработка, выполняемая функцией, достаточно примитивна. Сначала
объявляется локальная переменная original, которая указывает на начало
массива char, переданного вызывающей функцией. Затем функция проходит по
массиву в цикле while, обращаясь к последовательным символам через
указатель source и прибавляя 3 к их ASCII-значениям. Когда указатель доходит до
нуль-символа, цикл заканчивается и функция возвращает указатель на
начало закодированного теперь массива.
Функция strdecrypt() делает практически то же самое, как можно видеть
из ее кода:
char *strdecrypt{char *source)
{
char *original = source;
while (*source) {
*source = *source - 3;
source++;
>
return(original) ;
}
Функция тоже объявляет локальный указатель на входной массив и затем
проходит по массиву, отнимая 3 от ASCII-значений символов (и, таким
образом, возвращая их к исходному состоянию до кодировки). Когда функция
завершается, она возвращает указатель на начало массива.
204
Глава 7
Функция main() читает с диска текстовый файл, шифрует его и отображает
зашифрованный текст на экране. Функция обращается к библиотекам
ввода/вывода C++ iostream и fstream:
int main()
{
char linein[81], converted[81];
int encryptdecrypt = 1;
int i;
ifstream in, inclear;
cout « "Enter 1 to encrypt, 2 to decrypt: ";
cin » encryptdecrypt;
if (encryptdecrypt = 1)
in.openCtest-enc.txt") ;
else
in.open("test-dec.txt");
Функция main() объявляет переменные, которые в дальнейшем использует
для чтения и вывода символьных массивов, переменные потоков для
управления файлами и некоторые вспомогательные переменные. Затем она просит
пользователя ввести число, задающее шифровку либо дешифровку. Если
пользователь выбирает шифровку, программа зашифрует файл и выведет
шифрованный текст на экран; противном случае файл будет дешифрован и
программа опять-таки выведет полученный текст.
if(!in) {
cout « "Cannot open file." « endl;
return (1);
>
Этот оператор if —- дань «правилам хорошего тона» в программировании —
проверяет^ что программа смогла успешно открыть файл, до какой-либо его
обработки. Если программе не удалось открыть файл, функция main() возвращает
управление системе, не проводя никакой дальнейшей обработки. Если же файл
успешно открыт, программа входит в цикл while и обрабатывает файл:
while (!in.eof()) {
for (i=0; i < 80 £& !in.eof(); i++)
in.get(linein[i]);
linein[i] = NULL;
if (encryptdecrypt = 1)
strcpy(converted, strencrypt(linein));
else
strcpy(converted, strdecrypt(linein));
cout « converted « endl;
)
Программа просто читает строку файла (80 символов) во вложенном цикле
for. После этого она шифрует или дешифрует файл (в зависимости от
произведенного ранее пользователем выбора), копирует преобразованный текст в
массив converted и выводит его на выходное устройство по умолчанию.
Программа повторяет цикл while, пока не будет достигнут конец файла. Тогда она
закрывает файл и открывает его снова, чтобы вывести исходный текст:
Основы шифровки
205
in.close{);
cout « endl « endl;
if (encryptdecrypt == 1)
inclear.open("test-enc.txt");
else
inclear.open("test-dec.txt");
if(!inclear) {
cout « "Cannot open file." « endl;
return (1);
}
while (!inclear.eof{)) {
for (i=0; i < 80 Sfi 'inclear.eof(); i++)
inclear.get(linein[i]);
linein[i] = NULL;
cout « linein « endl;
}
Этот фрагмент программы производит те же действия, что и предыдущий
цикл, за исключением того, что он просто выводит прочитанные данные
файла, не подвергая их никакой обработке. Это делается, чтобы вы могли
нагляднее представить себе взаимоотношение между исходным и
модифицированным текстом.
Наконец, программа снова закрывает файл и завершается:
inclear.close();
return 0;
Если вы запустите программу simplel.cpp, выводимая ею информация
окажется именно тем, что и следовало ожидать — последовательностью букв и
знаков, не имеющих на первый взгляд ничего общего с исходным текстом,
наподобие следующего:
Enter 1 to encrypt, 2 to decrypt: 1
Wklv#lv#d#whvw#ri#wkh#hqfu j swlrq#v [ vwhpl#Wklv#whvw#xvhv#d#vlpsoh#urwdw
lrq#ydoxh#
wr#hqfu!sw#wkh#wh{wl#\rx#zloo#vhh#pruh#dgydqfhg#hqfuj swlrq#odwhu00wkh#
pdmru#lwx
h#zlwk#d#urwdwlrq#olnh#wklv#lv#lw#ohqgv#lwvhoi#wr#fudfnlqjl
This is a test of the encryption system. This test uses a simple
rotation value
to encrypt the text. You will see more advanced encryption later—the
major iss
ue with a rotation like this is it lends itself to cracking.
Как видите, шифровка текста из файла приводит к желаемому результату:
измененной форме входного массива. Однако ясно, что эту программу трудно
назвать настоящей программой шифровки, — такой шифр попадает скорее в
раздел головоломок.
Если даже не говорить о недостатке, связанном с необходимостью заранее
знать значение ключа, применяемая в simplel.cpp модель шифровки не
является надежной, так как ее чрезвычайно легко раскрыть. На самом деле боль-
206
Глава 7
шинство людей разгадают этот шифр, просто внимательно посмотрев на него.
Некоторые из моделей шифровки, которые вы увидите далее в этой главе,
затрачивают значительные усилия на то, чтобы сделать шифр надежным, т. е.
таким, который очень трудно раскрыть.
Усиление защиты шифра
Самые криптографически надежные модели шифров основаны на блочном
кодировании и выполняют очень сложные математические преобразования
данных. Блочный алгоритм шифровки работает с минимальными
«блоками» — размером обычно от 128 до 512 байт. Если нужно зашифровать данные
размером менее полного блока, оставшаяся часть блока заполняется
«мусором» (который алгоритм дешифровки должен затем удалить). Вообще говоря,
блочная шифровка должна применяться к наборам данных размером не менее
нескольких блоков. Например, если ваш алгоритм шифровки кодирует блоки
размером 512 байт, он не должен применяться к данным, размер которых
менее 2К.
Однако часто вашим программам необходимо зашифровать информацию
лишь с относительной надежностью. Может быть, в расчет должна в первую
очередь приниматься скорость — надежная блочная шифровка требует
значительных затрат времени. Приемлемым решением в этом случае является по-
точное кодирование, которое шифрует байты по мере их поступления в
процессор (или перед отправкой их через устройство коммуникации). Поточный
алгоритм может обрабатывать всего один байт за раз. Поэтому поточные
методики шифровки будут хорошо работать на небольших объемах данных, они
вполне соответствуют требованиям большинства коммерческих приложений и
предъявляют значительно более скромные требования к памяти, чем похожие
блочные модели.
В недавнем выпуске «C/C++ User's Journal» Уильям Уорд представил
превосходную программу на C++, которая реализует весьма защищенный процесс
поточной шифровки. Хотя мы не будем здесь стремиться к подобному уровню
шифровки, мы применим ту же методику (использующую исключающее
ИЛИ), что и Уорд в своей программе. Разница заключается в способе
генерирования значения XOR. После того, как вы рассмотрите следующую программу,
SimpXOR.cpp, и ее методику сокрытия данных, мы вкратце расскажем, что
необходимо для того, чтобы сделать ее более надежным инструментом
кодирования, — т. е. более близкой к реализации Уорда. Когда вы проработаете
следующий раздел, мы исследуем стандартную реализацию блочного
кодирования в коде ваших программ, когда вы применяете кем-то другим написанные
алгоритмы и для шифровки своей информации просто вызываете
реализующие их функции.
Более сложный алгоритм шифровки
Программа SimpXOR.cpp применяет одну из самых фундаментальных
методик шифровки — на самом деле она лежит в основе всей компьютерной
криптографии. Различие между нашей методикой и настоящей, надежной блочной
шифровкой заключено в способе создания ключей. В программе SimpXOR.cpp
Основы шифровки 207
ключ представляет собой простую последовательность, которая повторяется
через каждые 255 символов. Вообще хороший ключ шифра должен иметь
период повторения не меньше чем 263-1, и даже такой шифр можно раскрыть.
Вот почему самые современные процессы кодирования применяют для
шифровки блоков большие (порядка 4096 бит) ключи, а не отдельные символы или
последовательности символов.
SimpXOR.cpp формирует повторяющуюся последовательность символов,
просто перебирая по порядку набор символов ASCII; каждый шифруемый
символ комбинируется по XOR со следующим символом ключевой строки. Если
общее число символов, которые нужно зашифровать, превышает длину
ключевой строки, последняя повторяется с начала каждый раз, когда процедура
кодирования достигает ее конца. При дешифровке файла снова применяется та
же самая операция XOR с ключевой строкой.
1 ЗАК
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Имейте в виду, что такой метод применения XOR, который отпугнет
разве что случайных людей, не должен применяться, когда вопрос о
защите данных ставится серьезно. В таких случаях для кодирования нужно
использовать либо генератор псевдослучайных чисел в комбинации с
регистром сдвига, либо какую-то другую апробированную методику. Есть
способы, посредством которых любой человек, не пожалевший потратить на
это некоторого времени и усилий, быстро раскроет шифр, подобный
представленному в этой программе. Тем не менее стоит исследовать эту
методику, чтобы лучше понимать другие ее приложения.
Код
Вот код программы SimpXOR.cpp.
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void XORjChar(unsigned char& Target, unsigned charfi CryptVal)
{
. * Target « Target A CryptVal;
f return;
• }
Г-
D
void SampleFile(const strings SourceName, const strings TargetName)
(
char InChar = 'A';
int CryptChar =1;
ifstream Source(SourceName.c_str(), ios::in { ios::binary);
ofstream Target(TargetName.c_str(), ios::out [ ios::binary);
while (!Source.read(filnChar, sizeof(InChar)).eof()) {
208
Глава 7
XOR_Char((unsigned char&)InChar, (unsigned chars)CryptChar);
Target.write(SInChar, sizeof(InChar));
if (++CryptChar ■= 256)
CryptChar ■ 1;
h- >
Source.close();
Target.close();
return;
r
i.
I I.
i void SampleString(strings Target)
r .
f1"
Г
{
int Position - 0;
int Length =0;
int CryptChar ~ 255;
Length = Target.length();
for (Position = 0; Position < Length; Position++) {
XOR__Char ((unsigned charS) Target [Position] ,
(unsigned charS)CryptChar);
if (--CryptChar — 0)
CryptChar - 255;
)
* i
^
j
f
' t
int main()
string Target_String("C/C++ Annotated Archives");
r:
p
r •
cout « "Original String: " « endl;
cout « Target_String « endl;
SampleString(Target_String);
cout « "Encrypted String: " « endl;
cout « Target_String « endl;
SampleString (Target_J5tring);
cout « "Decrypted String: " « endl;
cout « Target_String « endl;
cout « endl;
string Source_Name("gbaddr.txt");
string Target_Name("engbaddr.txt");
char Character;
cout « "Before encryption (gbaddr.txt):" « endl;
ifstream Source_File(Source_Name.c_str(), ios::in ] ios::binary);
if (Source_File.fail()) {
cout « "Unable to open source file!" « endl;
return 1;
}
while ('Source_File.read(SCharacter, sizeof(Character)).eof()) {
cout « Character;
>
],<ц i Source File, close () ;
Основы шифровки 209
И
I
I.
** ■
А.
**л
J
cout « endl;
// Зашифровать содержимое исходного файла
//и сохранить его в целевой файле.
SampleFile(Source_Name, Target_Name);
// Вывести содержимое зашифрованного файла.
cout « "After encryption (engbaddr.txt):" « endl;
ifstream EncFile(Target_Name.c_str(), ios::in | ios::binary);
if (EncFile.fail()) {
cout « "Unable to open encrypted file!" « endl;
return 1;
)
while (!EncFile.read(&Character, sizeof(Character)).eof()) {
cout « Character;
}
EncFile.close();
cout « endl « endl;
// Дешифровать целевой файл в третий файл,
string Dec_Target_Name("decbaddr.txt") ;
SampleFile(Target_Name, Dec_Target_Name);
// Вывести дешифрованное содержимое.
cout « "After decryption (decbaddr.txt):" « endl;
ifstream DecFile(Dec_Target_Name.c_str(), ios::in j ios::binary);
if (DecFile.fail()) {
cout « "Unable to open encrypted file!" « endl;
return 1;
)
while (!DecFile.read(&Character, sizeof(Character)).eof()) {
cout « Character;
}
DecFile.close();
f cout « endl;
return 0;
| ПРк
ПРИМЕЧАНИЯ
Действительную шифровку данных выполняет процедура XOR_Char(),
Поскольку это первая функция программы, мы начнем с нее:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void XOR_Char(unsigned charfi Target, unsigned char& CryptVal)
{
210
Глава 7
Target = Target A CryptVal;
return;
}
Функция принимает два беззнаковых символьных значения:
преобразуемый символ и символ, производящий преобразование. Затем XOR_Char()
выполняет преобразование и возвращает полученное значение в своем первом
параметре Target. Этот шаг — объединение по XOR криптографического
значения со значением, которое нужно преобразовать, — является корнем всей
компьютерной криптографии. С теоретической точки зрения он является и
математической основой всей криптографии вообще.
Заметьте, кстати, что мы говорим о преобразовании, а не шифровке
символа. Это важное различие. Применение XOR к символу шифрует его, но
повторное применение той же операции его дешифрует — при условии, что значение,
с которым комбинируется символ, одинаково в обоих случаях. Важно также
отметить, что в любой разработанной вами программе шифровки этот шаг
обязательно будет где-то присутствовать. Главное отличие между представленной
здесь программой и настоящей программой надежной шифровки лежит в
способе генерации этого ключевого значения.
Вот что это значит. В данном приложении ключевые значения образуют
простую циклическую последовательность: 1, 2, 3, 4, ...255, 1, 2 и т. д. Для
любого специалиста по шифрам раскрыть такой ключ ничего не стоит —
повторения в коде будут быстро обнаружены, поскольку ключевое значение
повторяется через каждые 255 символов. Имея документ размером в 10000 или
около того, хороший криптограф раскроет этот ключ за считанные минуты.
Но это не изъян метода; это просто изъян его реализации. Если изменить
способ генерирования криптографического ключа — т. е. с известной
цикличностью, но такой, которую нельзя с легкостью предсказать, не зная устройства
ключа, — вы получите очень защищенную программу. Ограничения
коренятся не в методе XOR, а в том факте, что ключ очень быстро и очень часто
повторяется. Но создайте XOR-байт с помощью метода, который не повторяет
значения сколько-нибудь регулярно, и вы легко сделаете программу гораздо более
защищенной.
Чтобы наглядно представить себе вышесказанное, рассмотрите опять
последовательность значений в криптографическом ключе:
1, 2, 3, 4, 5, 6, 7, ...
Другими словами, если байт строки кодируется при помощи значения 3, то
предыдущий обязательно кодируется значением 2, а последующий
значением 4 и т. д.
Рассмотрите теперь такую последовательность:
2, 3, 7, 22, 128, 3, 1, 255, ...
Здесь нет легко предсказуемой зависимости между значением 3 и
соседними значениями. В оптимальном случае хороший алгоритм кодирования
никогда не повторяется при шифровке любого отдельного файла. Это мы и имеем
в виду, когда говорим, что не метод, а его реализация может иметь изъяны.
Оставшаяся часть программы достаточно очевидна. Каждая из двух других
функций обрабатывает'последовательность символов, передавая их по одному
в XOR_Char(). Следующий код реализует функцию SampleFile():
Основы шифровки 211
void SampleFile(const strings SourceName, const strings TargetName)
{
char InChar - 'A';
int CryptChar = 1;
ifstream Source(SourceName.c_str(), ios::in ] ios:rbinary);
ofstream Target(TargetName.c_str(), ios:rout | ios::binary);
Функция принимает в качестве параметров только имена исходного файла
и файла, в котором должны сохраняться преобразованные значения. Затем
она объявляет пару локальных переменных — одну для хранения
прочитанных символов, а другую для циклически меняющегося криптографического
ключа. После этого функция открывает два заданных параметрами файла,
один с доступом для чтения и другой — для записи.
while {!Source.read(SInChar, sizeof(InChar)).eof()) {
XOR_Char((unsigned chars)InChar, (unsigned chars)CryptChar);
Target.write(SInChar, sizeof(InChar));
if (++CryptChar == 256)
CryptChar = 1;
}
Затем программа просто проходит в цикле по исходному файлу, проверяя,
не приводит ли чтение следующего символа к состоянию «конец файла». Если
нет, программа передает прочитанный символ функции XOR^CharO и
записывает преобразованный символ в целевой файл. На каждом проходе цикла
увеличивается значение CryptChar увеличивается на 1, пока не достигнет
значения 255. После 255 снова следует 1 — это, как говорилось выше, слабое место
данного процесса шифровки.
Source.close();
Target.close();
return;
}
После того, как SampleFile() обработает весь файл, она производит
необходимую «уборку» и завершается. В данной реализации не возвращается никакого
значения. В реальном приложении, однако, следовало бы проверить, что
программа успешно открыла исходный и целевой файлы, и возвратить код
ошибки, если это не так, или код успешного завершения, если все прошло как надо.
Функция SampleString() выполняет практически идентичные действия.
Однако вместо чтения с проверкой конца строки она сначала вызывает метод
length() стандартной библиотеки строк C++, определяя заранее число
содержащихся в строке символов. Затем функция записывает преобразованные
значения обратно в исходную строку — такое решение не всегда желательно и вы,
вероятно, в своих программах постараетесь этого избежать. Вот код функции
SampIeS tring():
void Samplestring(strings Target)
{
int Position = 0;
int Length = 0;
int CryptChar = 255;
Length = Target.length();
212
Глава 7
for (Position = 0; Position < Length; Position++) {
XOR_Char((unsigned charfi)Target[Position],
(unsigned char&)CryptChar);
if (—CryptChar — 0)
CryptChar = 255;
}
}
Опять-таки обратите внимание на то, что функция просто последовательно
перебирает значения криптографического символа (хотя она, в отличие от
предыдущей, считает в обратном порядке). Конечно, в реальном приложении
функция могла бы возвращать код успешного завершения и, конечно, должна
была генерировать значение CryptChar каким-то другим способом.
Код функции main() не делает ничего особенного помимо того, что
вызывает эти функции преобразования и выводит текущие или выходные значения.
Сначала обрабатывается простая строка:
int main()
{
string Target_String("C/C++ Annotated Archives");
cout « "Original String: " « endl;
cout « Target_String « endl;
SampleString(Target_String);
cout « "Encrypted String: " « endl;
cout « Target_String « endl;
SampleString(Target_String);
cout « "Decrypted String: " « endl;
cout « Target_String « endl;
cout « endl;
Заметьте, что этот код вызывает SampleString() дважды — один раз для
шифровки текста строки после чего строка выводится на экран; второй раз для
его дешифровки, после чего строка выводится вновь. Как вы увидите из
программного вывода, исходное ее содержимое и содержимое после дешифровки
идентичны.
Следующий блок кода определяет пару строк с именами дисковых
файлов — одно для файла с обычным текстом и другое для конечного
шифрованного файла. Затем программа открывает входной файл и выводит его символы
на экран по одному.
string Source_Name("gbaddr.txt");
string Target_Name("engbaddr.txt") ;
char Character;
cout « "Before encryption (gbaddr.txt):" « endl;
ifstream Source_File(Source_Name.c_str(), ios::in | ios::binary);
if (Source_File.fail()) {
cout « "Unable to open source file!" « endl;
return 1;
}
while (!Source_File.read(&Character, sizeof(Character)).eof()) {
cout « Character;
}
Основы шифровки
213
Source_File.close{);
cout « endl;
Заметьте, что программа читает файл как двоичный. В данном случае это
не является необходимым, поскольку известно, что данный файл —
текстовый, но это будет необходимо при обработке зашифрованного файла.
ВНИМАНИЕ!
| BHI/
Применяя любой алгоритм шифровки к файлам, всегда рассматривай-
те их как двоичные, даже если известно, что это текстовые файлы.
Процесс преобразования может генерировать, и часто генерирует,
символы '\0' е выходном файле. Функции, ожидающие в качестве
параметров ограниченные нулем строки или явные символы конца строки и
конца файла, не будут работать правильно, если вы открываете
обрабатываемые файлы как текстовые.
На самом деле, если вы измените код программы так, что он будет просто
сравнивать прочитанный символ со значением EOF, чтение входного файла
может завершиться преждевременно.
Следующее чтение файла в нижеприведенном фрагменте, где на экран
выводится содержимое шифрованного файла, также выполняется как двоичное;
символы обрабатываются по одному:
SampleFile(Source_Name, Target_Name);
cout « "After encryption (engbaddr.txt):" « endl;
ifstream EncFile(Target_Name.с str(), ios::in j ios::binary);
if (EncFile.fail()) {
cout « "Unable to open encrypted file!" « endl;
return 1;
}
while (!EncFile.read(&Character, sizeof(Character)).eof()) {
cout « Character;
)
EncFile.close();
cout « endl « endl;
Этот код совершенно идентичен предыдущему, только вместо исходного
здесь обрабатывается зашифрованный файл.
Наконец, программа выполняет обратное преобразование шифрованного
файла и выводит результат дешифровки:
string Dec_Target_Name("decbaddr.txt");
SampleFile(Target_Name, Dec_Target_Name);
cout « "After decryption (decbaddr.txt):" « endl;
ifstream DecFile(Dec_Target_Name.c_str(), ios::in | ios::binary);
if (DecFile.fail()) {
cout « "Unable to open decrypted file!" « endl;
return 1;
)
while (!DecFile.read(&Character, sizeof(Character)).eof()) {
214
Глава 7
cout « Character;
}
DecFile.close();
cout « endl;
return 0;
)
Когда вы запустите компилированную программу SimpXOR.cpp, она
генерирует следующий вывод. (Распечатка исходного файла ради экономии места
здесь не воспроизводится.)
Original String:
C/C++ Annotated Archives
Encrypted String:
+-++-++ЦВДБХЗЧХ-оЬ0ДВЬМЫ
Decrypted String:
C/C++ Annotated Archives
Before encryption (gbaddr.txt):
Four score and seven years ago...
After encryption (engbaddr.txt):
Gmw%udg{o+mcj/ctdvz5oryki; }zq3 NWQCGS@LXXO\@EVZGiSYELQ+0d&) )<
$."9bolq<6#u86,055p}=0 GJnAS§W-B-++3K-+ - ++0 +Jb++ _Jr+ b-_
Яб эублбжпмоыепооЁ а 6У _п +№к+н -ЦЖЧСПИП+ЭГЙЩЖКВ-
3KbDCB+nWHy*T-_Ps"bj J 6ii}cdb- > ч Or} }wpDa} } : zry>10FF@LEF\LNNOA] ] 1 S§SY\LHAi
Z' 0&d(#3h&$k-m)=50&s64"#4«291:DnBG+++4_++bI+
+- ее ллхов мъмн+ляд + ж! +о+й_и +л+ХКВР+ИЖЬАЕЕ АЗИШЕ-ЯЭГУ++_0_ХО_Юппг1
с'гот{*теу1 6gjvj~n=jwAUTAUOG\FOIAnDY[@F
[ОРхбсо»ьыщеисычртп++_ч»»х+_ыхюе_+Зс+ Г + —Й +-БО++ +Х++ Щ +ТЯ+
йну+ пожои-
эжкв _е+ а_° п++нБХЗЗ-МТ-+ШЩ *MA^HaB3+34u?I_MTHbs''wk%gcl)ey,ik{bpqg
:SBG)
After decryption (decbaddr.txt):
Four score and seven years ago, our fathers brought forth upon this
continent, a new nation, conceived in liberty, and dedicated to the
proposition, that all men are created equal.
Now we are engaged in a great civil war, testing whether that nation,
or any nation so conceived and so dedicated, can long endure. We are
met on a great battlefield of that war. We have come to dedicate a
portion of that field, as a final resting place for those who here
gave their lives that that nation might live. It is altogether
fitting and proper that we should do this.
But, in a larger sense, we cannot dedicate — we cannot consecrate --
we cannot hallow -- this ground. The brave men, living and dead, who
struggled here, have consecrated it, far above our poor power to add
or detract. The world will little note, nor long remember what we say
here, but it can never forget what they did here. It is for us the
living, rather, to be dedicated here to the unfinished work which
they who fought here have thus so nobly advanced. It is rather
for us to be here dedicated to the great task remaining before us —
Основы шифровки
215
that from these honored dead we take increased devotion to that cause
for which they gave the last full measure of devotion — that we here
highly resolve that these dead shall not have died in vain — that
this nation, under God, shall have a new birth of freedom -- and that
government of the people, by the people, for the people, shall not
perish from the earth.
Хотя зашифрованные строка и файл уже не поддаются расшифровке, так
сказать, невооруженным глазом, их вполне можно расшифровать с помощью
программы, похожей на ту, что их генерировала. Повторим еще раз, что
применение более сложного метода порождения значений криптографического
ключа поможет сделать эту методику менее уязвимой.
В следующем разделе мы рассмотрим некоторые ограничения, присущие
шифрам с одним ключом, а также альтернативные системы шифровки,
позволяющие эти ограничения обойти.
Ограниченность традиционных систем
шифровки с одним ключом
В традиционных криптосистемах с одним ключом передатчик и приемник
используют один ключ (т. е. тот же самый ключ) для шифровки и
дешифровки. Таким образом, передатчик и приемник должны предварительно передать
ключ по надежным каналам, чтобы обе стороны имели в своем распоряжении
ключ до того, как они начнут посылать или принимать зашифрованные
сообщения по незащищенным каналам связи. Рис. 7.3 показывает типичный
обмен информацией при использовании системы с одним ключом вроде тех, что
вы уже видели в этой главе.
Рис. 7.3.
Система шифра с одним ключом
требует, чтобы обе стороны имели
копию ключа
текст
ключ
зашифрованный
текст
■ ч передача
Ч" ' ►
зашифрованный
текст
ключ
текст
216
Глава 7
Какие ограничения присущи такой системе?
♦ Главным изъяном криптосистемы с одним ключом при обмене через
Internet является то, что обе стороны должны заранее знать ключ перед
каждой передачей.
♦ Кроме того, поскольку вы хотите, чтобы ваши передачи мог
дешифровать только конкретный получатель, вам придется создавать различные
ключи для каждого индивида, группы или компании, с которыми вы
сноситесь. Очевидно, вам придется иметь дело со слишком большим
числом одиночных ключей.
♦ Если вы пользуетесь защищенным каналом для передачи собственно
ключа, то с тем же успехом можете воспользоваться этим надежным
каналом и для передачи данных (что обессмысливает саму цель
криптографии). Большинство людей, однако, не имеют доступа к защищенным
каналам связи и поэтому должны обмениваться данными по
незащищенным каналам.
Современная связь с применением шифров пользуется совсем другим видом
криптографии, чтобы избежать проблем, возникающих на пути использования
криптосистем с единственным ключом. Это криптосистемы с общим ключом.
Криптосистемы с общим ключом
Для криптосистемы с общим ключом требуются два взаимосвязанных,
комплементарных (дополняющих друг друга) ключа. Вы можете свободно
распространять один из них, общий ключ, среди своих друзей, деловых
партнеров и даже конкурентов. У вас же в надежном месте (на вашем компьютере)
будет храниться второй, личный ключ, который вы никогда никому давать не
будете. Личный ключ «отпирает» шифр, закрытый общим ключом, что
иллюстрируется рис. 7.4.
Рис. 7.4.
Базовая модель криптосистемы
с общим ключом
текст
Общий ключ
зашифрованный
текст
передача
зашифрованный
текст
Личный ключ
текст
Основы шифровки 217
При использовании криптосистемы с общим ключом ваш кузен может
опубликовать свой общий ключ где угодно в Internet или послать его вам в
незашифрованном виде по электронной почте. Вы сможете зашифровать им свое
сообщение. Расшифровать его сможет только ваш кузен, — даже вы,
отправитель (и шифровальщик) сообщения, не сможете этого сделать. Когда ваш
кузен захочет ответить на ваше послание, он зашифрует свое сообщение вашим
общим ключом. Это сообщение сможете расшифровать только вы.
Протокол с общим ключом совершенно устраняет необходимость связи по
защищенному каналу, что требуют традиционные криптосистемы с
единственным ключом. Фундаментальные алгоритмы, лежащие в основании
криптографии с общим ключом, разработаны двумя группами людей. Одна из них — это
Райвест, Шамир и Эйдлман (Rivest—Shamir—Adleman); вторая — Диффи и
Хеллман (Diffie—Hellman). В следующих разделах мы будем ориентироваться
в основном на реализацию Райвеста—Шамира—Эйлдмана, поскольку это
один из протоколов, положенных в основу криптографического API системы
Windows.
Цифровые подписи
Программы шифровки с общими ключами широко используют
идентификацию сообщений, метод, с помощью которого получатели сообщения могут
удостоверить личность его отправителя и его подлинность. Процесс
идентификации довольно прямолинеен. Отправитель должен воспользоваться своим
секретным ключом, чтобы зашифровать уникальное значение,
ассоциированное с содержанием сообщения и, таким образом, подписать его. Секретный
ключ генерирует цифровую подпись на сообщении, которую получатель (да и
любое другое лицо) может проверить, дешифровав ее с помощью общего ключа
отправителя.
Благодаря цифровой подписи получатель сообщения может быть уверен,
что сообщение действительно исходит от того, кто его, по-видимому, послал.
Так как только обладатель личного ключа может создать цифровую подпись,
доступную для расшифровки общим ключом, эта подпись удостоверяет
личность отправителя сообщения. Кроме того, цифровая подпись обрабатывает
сообщение и генерирует уникальное числовое значение, соответствующее
содержанию, дате и времени создания и т. п. документа. Таким образом, если
цифровая подпись проверена, можно гарантировать, что никто не изменил файл в
процессе или после его передачи.
Построение цифровой подписи
Программы, подписывающие документы, генерируют цифровую подпись в
два этапа. Программа-получатель проверяет подпись посредством
аналогичного процесса. Во-первых, подписывающая программа передает текст или код,
требующий подписи, математической функции, называемой функцией
смешивания (hash function). Последняя создает из составляющих файл байтов
уникальное значение. Рис. 7.5 показывает, как функция-смеситель получает
входные значения и выдает уникальный результат.
218
Глава 7
Входные
xFF
х88
х10
хОО
х11
значения
х40
xEF
х01
xFA
xFF
Однонаправленный
Уникальное
значение
Рис. 7.5. Функция смешивания выводит свой уникальный результат из ряда других значений
После создания смешанного значения подписывающая программа шифрует
его, используя личный ключ шифрующего (пользователя). Наконец,
программа сохраняет подписанный вариант файла, который обычно содержит
информацию о подписавшей его программе и индикаторы того, где начинается и где
заканчивается подписанный файл. Рис. 7.6 представляет простую модель того,
как программа ставит на документ цифровую подпись.
Рис. 7.6.
Программа прикрепляет
к файлу подпись после
шифровки смешанного
значения
смешанное
значение
Личный ключ
зашифрованное
смешанное
значение
цифровая
подпись
Чтобы проверить цифровую подпись, получатель файла сначала запускает
процесс дешифровки смешанного значения подписи, используя общий ключ
отправителя. После дешифровки программа сохраняет смешанное значение во
временном хранилище. Рис. 7.7 показывает, как программа дешифровки
извлекает зашифрованное смешанное значение и снова преобразует его в
пригодную для использования форму.
Программа получателя затем пропускает файл через тот же смеситель, что
использовался передающей стороной для создания смешанного значения.
Вычисленное таким образом значение сравнивается с дешифрованным
смешанным значением, и если они совпадают, программа информирует пользователя,
что подпись правильна и, следовательно, сообщение подлинное. Рис. 7.8
показывает, как программа получателя вычисляет собственное смешанное
значение и сравнивает его с дешифрованным значением.
Основы шифровки
219
подписанный
файл
заголовок
содержимое
общий ключ
зашифрованное
смешанное значение
смешанное
значение
Рис. 7.7. Принимающая программа сначала извлекает и дешифрует смешанное значение
подписанный
файл
заголовок
J
однонаправленный
смеситель
дешифрованное
смешанное значение
I
сравнение
I
I
вычисленное
смешанное значение
Рис. 7.8. Принимающая программа вычисляет собственное смешанное значение и сравнивает
его с дешифрованным
ВНИМАНИЕ!
[bhj
Цифровые подписи — это не то же самое, что электронные подписи. Как
говорилось выше, цифровые подписи помогают идентифицировать
создателей и отправителей сообщений. В электронном сообщении цифровая
подпись имеет тот же вес, что и собственноручная подпись в печатной
корреспонденции. Однако в отличие от подписи, поставленной на бумаге,
цифровую подпись подделать практически невозможно. Цифровые
подписи являются динамическими — каждая подпись является уникальной
принадлежностью сообщения, которое она заверяет. Сами данные
сообщения, а также личный ключ, используемый отправителем для ее
шифровки, являются математическими компонентами конструкции подписи.
Электронные подписи, с другой стороны, являются просто копией
обычных (как, скажем, на факсе). Не путайте эти два понятия.
Алгоритм Райвеста-Шамира-Эйдлмана (RSA)
Важно понять, что хотя криптография с общим ключом применяется по
большей части для шифровки текстовых сообщений, компьютер реализует
220
Глава 7
ее, производя ряд математических действий над данными. Программисты
называют весь этот набор математических операций алгоритмом шифровки.
Одним из наиболее удачных и важных алгоритмов является алгоритм RSA.
На его основе строились ранние версии Pretty Good Privacy (это одна из самых
известных программ шифровки), а также многие другие разновидности
программ шифровки и шифрованной связи.
Когда вы рассматриваете процесс шифровки, следует отличать алгоритм,
являющийся математической конструкцией, от программы вроде CryptoNotes
(с ее работой вы познакомитесь позже в этой главе), применяющей алгоритм
для выполнения своей задачи. Другими словами, алгоритм — как молоток: без
руки (программы, такой как CryptoNotes), которая его держит, он бесполезен.
Алгоритм RSA создали три математика из Массачусетского
технологического института. Алгоритм случайным образом генерирует очень большое простое
число (общий ключ). После этого он использует данный общий ключ для
вычисления другого большого простого числа (личного ключа), применяя достаточно
сложные математические функции. В одном из следующих разделов главы мы
расскажем об этой математике более подробно. Пользователи затем могут
использовать полученные ключи для шифровки документов, которыми они
обмениваются, и для дешифровки документов после получения их адресатом.
Свойства алгоритма RSA
Четырьмя фундаментальными свойствами алгоритма RSA, как он
определен Райвестом, Шамиром и Эйлдманом, являются следующие:
1. Дешифровка шифрованной формы сообщения дает исходную его форму.
Символически это можно записать так:
D(E(M)) * М
В этом выражении D означает операцию дешифровки, Е операцию
шифровки и М — действительное сообщение.
2. Е и D вычисляются сравнительно просто.
3. Открытое опубликование Е не означает, что тем будет открыт какой-то
простой путь к вычислению D. Таким образом, только пользователь,
обладающий значением D, может расшифровать сообщение,
закодированное с помощью Е.
4. Дешифровка сообщения М, а затем его шифровка дает снова М. Другими
словами, приведенную выше формулу можно обратить:
E(D(M) ) = М
Как указывают авторы алгоритма, если кто-то при передаче данных
использует процедуру, удовлетворяющую третьему из перечисленных свойств,
то другому пользователю, который хочет расшифровать сообщение, придется
перебрать все возможные ключи, пока он не найдет тот, для которого
выполняется условие Е(М) = D. Такой перебор выполнить относительно несложно,
если числа-ключи представляются 10 или даже 20 цифрами. Однако
классические схемы шифровки RSA применяли значения размером до 512 бит как для
общего, так и для личного ключей — это соответствует 154 цифрам
десятичного представления этих значений. Вдобавок, это очень большие простые числа,
и операции над ними требуют чрезвычайно больших вычислительных
мощностей. На самом деле до сих пор никому не удалось раскрыть 512-битный ключ
с помощью грубой силы (т. е. прямого перебора всех возможных значений).
Основы шифровки 221
А в последнее время программы начинают использовать ключи размером до
4096 бит.
Функцию, удовлетворяющую условиям с 1-го по 3-е алгоритма RSA,
называют односторонне закрытой функцией, поскольку, как вы уже знаете, ее легко
можно вычислить в прямом, но не в обратном направлении. Выражение
закрытая подразумевает, что вычисление обратной функции не составит труда, если
вам известна некоторая конфиденциальная (закрытая) информация о ней.
Алгоритм RSA как таковой
Алгоритм RSA концептуально сравнительно несложен. Процесс его
применения похож на то, что описывалось в предыдущих разделах, причем
реализующая его программа выполняет еще некоторые дополнительные действия
для гарантии того, что файл будет зашифрован корректно. Ключ шифра,
обозначенный выше как Е, ассоциируется с некоторой константой п. Эта
константа определяет предельную длину шифруемого блока (т. е. число байтов
данных, которое может содержаться в одном зашифрованном блоке). Шифровка
производится в три этапа:
1. Программа, реализующая алгоритм RSA, преобразует текстовое
сообщение в целое число из диапазона от 0 до (п-1). Метод преобразования текста
в целое может варьироваться в различных программах. Большие
сообщения (настолько большие, что целое число, меньшее п-1, уже не может
служить их адекватным представителем) разбиваются на ряд блоков,
каждый из которых представляется своим значением, меньшим п-1.
2. Программа шифрует сообщение, возводя каждое из этих целых значений
в степень Е. Затем она применяет к результату арифметику по модулю п
(в арифметике по модулю в расчет принимается только остаток от
результата операции деления), деля полученные значения на п и сохраняя
их остаток в качестве зашифрованного сообщения. Сообщение теперь
является шифротекстовым документом С. Шифротекстом называют
текст, полученный в результате процесса шифровки, т. е. попросту
зашифрованный текст.
3. Для дешифровки шифротекстового документа С приемник сообщения
возводит его в степень D, причем результат берется по модулю п.
Полученный ряд значений служит представлением блоков дешифрованного
документа. Затем программа преобразует эти блоки снова в текстовую
форму, применяя тот же метод, что использовался для преобразования
исходного текста в численное представление.
Пользователь публикует ключ шифровки (Е, п) в качестве открытого и
хранит у себя личный ключ дешифровки (D, п).
Математика алгоритма
Вы познакомились с основными этапами вычислений, выполняемых
алгоритмом RSA. Однако алгоритм более сложен, чем это представлено в
упрощенном описании предыдущего раздела. Математика работы алгоритма RSA
состоит в следующем:
222
Глава 7
1. Найти два очень больших простых числа р и q.
2. Найти значение п (открытый модуль), определяемое формулой п = р * q.
В 256-битной криптосистеме п состоит из 300 и более цифр.
3. Выбрать значение Е (открытую степень), такое, что Е < п и является
взаимно простым с (р-1) * (q—1). Взаимно простыми называются числа,
не имеющие общих делителей (кроме единицы).
4. Вычислить значение D (личную степень), такое, что ED = 1 по модулю
((Р-1) * (q-D).
Открытым ключом является пара (Е, п), а личным — (D, п). Пользователь
никому не должен сообщать значения р и q; лучше всего их уничтожить.
Большинство пакетов программного обеспечения для систем с открытыми
ключами будет сохранять значения р и q для внутренних операций, но эти значения
будут храниться в зашифрованном файле и никто (даже сам пользователь) не
сможет получить к ним непосредственный доступ.
Чтобы получить более подробные сведения о алгоритме RSA, обратитесь к
Web-станции RSA Corporation по адресу http://www.rsa.com.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Когда вы работаете с CryptoAPI, RSA-сервер (RSA Cryptographic Service
Provider) выполняет за вас все лежащие в основе алгоритма
математические операции. Использование CryptoAPI позволяет вызывать
одни и те же функции для кодирования буфера с применением
различных серверов — ответственность за корректную обработку вызовов
CryptoAPI лежит на сервере.
Смешанные значения
Как вы уже знаете, hash-функция (в криптографии), или смеситель, — это
математическая функция, генерирующая уникальное значение из заданных
входных байтов, полученных из строки, файла или другого источника
двоичных данных. Более того, эта функция вычисляет такое значение, по которому
нельзя восстановить исходную информацию.
Вы можете рассматривать смешанное значение как уникальное число,
представляющее действительное содержимое входного потока данных. Всякий
раз, когда вы пропускаете через смеситель тот же самый поток данных
(например, файл), получается одно и то же смешанное значение.
API шифровки системы Windows
Так как шифры используются многими пользователями компьютеров,
корпорация Microsoft встроила криптографическую поддержку в Windows NT 4,0;
встроенная поддержка криптографии имеется также в Internet Explorer, в
Windows 95 (OEM Service Release 2) и в Windows 98. Этот криптографический
интерфейс прикладного программирования (CryptoAPI) представляет собой
набор функций, которые вы можете вызывать из программ на Visual C++, Vi-
Основы шифровки
223
sual Basic и других языках программирования» предоставляя пользователям
ваших программ услуги по шифровке/дешифровке данных.
Программный интерфейс CryptoAPI позволяет вам ввести в свои
программы криптографические функции. Он использует архитектуру открытых услуг
Windows (WOSA) и предусматривает три набора функций:
♦ Сертификатные функции
♦ Упрощенные криптографические функции
♦ Базовые криптографические функции
Рис. 7.9 представляет модель, которой вы будете следовать при реализации
этих функций.
Рис. 7.9.
Модель использования CryptoAPI для поддержки
криптографии в прикладных программах
приложение
Crypto API
1 ,
сервер
i
1
сервер
RSA
.
'
специальный
сервер
Упрощенные криптографические функции включают в себя функции
высокого уровня для создания ключей и шифровки/дешифровки информации.
Сертификатные функции предоставляют вам средства извлечения, сохранения и
проверки сертификатов цифровых подписей, которыми могут сопровождаться
передаваемые документы, а также сопровождения сертификатов,
сохранявшихся на машине ранее. На нижнем уровне расположены базовые
криптографические функции. Вы должны избегать вызова этих функций из своих
программ, поскольку это может привести к конфликтам, связанным с удалением
из системы каких-то криптографических серверов (CSP), запросами других
программ к данному CSP и т. д. CSP обсуждаются в следующем разделе.
Криптографический API поддерживает использование нескольких
криптографических серверов. Например, вы можете кодировать некоторую
информацию с применением алгоритма RSA, подписывая какие-то другие данные в
соответствии со Стандартом цифровой подписи (DSS). В таблице 7.1
перечислены различные CSP с указанием применяемых ими типов шифровки.
Таблица 7.1. Криптографические серверы (CSP)
Криптографический сервер
PROV RSA FULL
PROV RSA SIG
PROV_DSS
Шифровка
RC2, RC4
нет
нет
Подпись
RSA
RSA
DSS
224
Глава 7
Криптографический сервер
PROV FORTEZZA
PROV SSL
PROV MS EXCHANGE
Шифровка
Skipjack
RSA
CAST
Подпись
DSS
RSA
RSA
CryptoAPI использует ключевую базу данных для хранения паролей. Когда
вы разрабатываете приложение, обращающееся к CryptoAPI, то прежде
должны создать для него необходимые ключевые базы данных. Чтобы
гарантировать, что при работе приложения ему будет доступна ключевая база данных, с
которой оно сможет работать, лучше всего проверять существование
последней при каждом запуске приложения, создавая и инициализируя новую
ключевую базу данных, если ее еще не существует.
Криптографические серверы (CSP)
Типичный CSP (Cryptographic Service Provider) состоит из DLL и файла
подписей, который CryptoAPI использует для проверки целостности и
идентификации пользователей, обращающихся к серверу. Иногда CSP может
прибегать к аппаратной реализации каких-то своих функций, чтобы предотвратить
возможность «взлома» или повысить производительность. Если
сформулировать это несколько иначе, то CSP — это сервер, способный выполнять
стандартный набор задач, инициируемых вызовом функций CryptoAPI.
Приложение не должно предполагать, что CSP имеет в своем распоряжении какие-то
возможности, выходящие за рамки известного стандарта. CSP — просто
сменный модуль программного обеспечения. По своей роли он очень напоминает
драйверы Windows, ответственные, например, за печать или управление
графикой. Как только вы зарегистрировали CSP, вы можете сразу использовать
его, не внося никаких изменений на уровне приложения.
Чтобы обеспечить нужную конфиденциальность, все данные, которыми
манипулирует CSP (особенно ключи), возвращаются в вызывающую программу в
форме безликих дескрипторов и остаются недоступными на уровне
приложения. Более того, приложения никак не могут повлиять на способ
действительного кодирования данных. Роль программы ограничивается передачей данных
серверу и указанием требуемого вида шифровки. Сервер всегда может
возвратить тип, сообщающий, что он может делать и каким именно образом.
Различные серверы могут использовать одинаковые алгоритмы, но должны
принимать различную логику заполнения и различные размеры ключей.
С CryptoAPI SDK поставляется CSP по умолчанию, называемый Microsoft
RSA Base Provider, реализуемый библиотекой rsabase.dll. Это сервер типа
PROV_RSA_FULL, поддерживающий алгоритм RSA с общим ключом для
обмена ключами и для подписей. (Ключ имеет длину 512 бит.) RSA Base
Provider применяет также алгоритмы RC2 и RC4 для шифровки с 40-битным
ключом. Приложения не должны полагаться на эти конкретные цифры,
поскольку CSP может быть заменен, а приложение не будет об этом знать.
Каждый CSP имеет ассоциированную базу данных для ключевых
контейнеров, которая содержит все личные и общие ключи всех пользователей,
имеющих доступ к данной машине. Каждый контейнер имеет уникальное имя, от-
Основы шифровки
225
крывающее двери в мир программирования CryptoAPI. Без ключевой базы
данных любые вызовы CryptoAPI приняты не будут. База данных обычно
имеет контейнер по умолчанию с регистрационными именами всех пользователей.
Однако конкретное приложение может во время установки создать
специальный ключевой контейнер и пары ключей, присвоив им свое собственное имя.
Поскольку тип сервера влияет на поведение криптографических функций, два
связанных между собой приложения должны пользоваться одним и тем же
CSP или, по крайней мере, серверы с общим подмножеством функций.
Рис. 7.10 показывает упрощенную модель ключевой базы данных.
Рис. 7.10.
Упрощенная модель базы данных CryptoAPI
ART
Кл
ючи подписей
HERB
Ключи подписей
Ключи обмена
LARS
Ключи подписей
Ключи обмена
Ключи шифра
•
•
•
•
Программная модель CryptoAPI
Перед тем как вводить криптографию в свои реальные приложения, вы
должны познакомиться с такими понятиями, как контекст, ключи сеанса,
ключи обмена и ключи подписей.
Контекст представляет установленный сеанс взаимодействия между
CryptoAPI и приложением-клиентом. Чтобы начать работу, вы должны
сначала получить контекст. Для этого вы передаете имя ключевого контейнера и
имя сервера, с которым хотите установить связь. Полученный дескриптор
контекста должен использоваться во всех последующих вызовах процедур
CryptoAPI.
Ключ сеанса вступает в игру, когда дело доходит до шифровки или
дешифровки данных. Ключи сеанса являются нестабильными объектами, чьи
действительные значения, по соображениям конфиденциальности и безопасности,
никогда не покидают CSP. Ключ сеанса определяет, как файл должен быть
закодирован и размещен в конечном шифрованном файле, чтобы его можно
было декодировать. Если вам нужно извлечь ключ сеанса из CSP в целях
обмена или хранения, вы используете ключевые блоки (key blobs, blob означает
Binary Large Object — большой двоичный объект). Ключевой блок
представляет собой конгломерат двоичных данных, который может рассматриваться как
зашифрованный вариант собственно ключа, пригодный для экспорта.
Механизм защиты завершают ключи обмена. Это пары ключей (один
общий и один личный), которые отвечают за шифровку ключей сеанса в ключе-
8 Зет 1208
226
Глава 7
вых блоках и обработку цифровых подписей. Ключ сеанса создается
динамически на основе информации, хранящейся в ключевом контейнере
пользователя. Как только вы получили ключ сеанса, вы можете делать вызовы для
шифровки файлов.
Имейте в виду, что поддержка криптографии в ваших приложениях на
самом деле не требует от вас знакомства с деталями RSA, DES (стандарта
шифровки данных) или любого другого распространенного алгоритма, или с
такими тонкими понятиями, как общие и личные ключи.
Вызов базовых функций CryptoAPI
Если вы хотите воспользоваться услугами CryptoAPI, вам придется
обращаться в своих программа к базовым функциям интерфейса, которые
поддерживают три различных рода деятельности, описанных ранее; это шифровка,
смешивание (hashing) и цифровая подпись.
Разработчики CryptoAPI распределили его функции по четырем главным
областям применения: управление CSP, ключи, смешивание и подписи. В
шифрующем приложении вы обычно прежде всего вызываете CryptAcquireContext();
она позволяет вам выбрать или получить доступ к криптографическому
серверу- Функция объявлена в заголовке wincrypt.h следующим образом:
BOOL WINAPI CryptAcquireContext(HCRYPTPROV *phProv,
LCTSTR pszContainer, LPCTSTR pszProvider,
DWORD dwProvType, DWORD dwFlags);
Эта функция возвращает (в первом параметре) длинное 32-битное значение,
сообщающее остальным функциям CryptoAPI, что CryptAcquireContext()
открыла рабочий криптографический сеанс. В параметрах pszProvider и
pszContainer вы можете указать конкретный CSP и ключевой контейнер.
После того, как вы получили дескриптор успешно открытого сеанса, можно
вызывать функции, перечисленные в таблице 7.2, для выполнения в вашей
программе различных операций шифровки.
Таблица 7.2. Функции CryptoAPI
Функция CryptoAPI
CryptAcquireContext{)
CryptCreateHash()
CryptDecrypt()
CryptDeriveKeyO
CryptDestroyHash()
CryptDestroyKeyO
CryptEncryptO
Описание
Возвращает дескриптор ключевого контейнера в
криптографическом сервере (CSP).
Создает hash-объект (численную интерпретацию значений).
Расшифровывает содержимое буфера с помощью ключа
дешифровки.
Выводит из hash-объекта ключ шифра. Обычно вы смешиваете
пароль или другую специфическую строку, получая
hash-объект, а затем выводите из него ключ.
Уничтожает объект, созданный CryptCreateHash().
Уничтожает ключ, импортированный CryptlmportKeyO или
созданный CryptDeriveKeyO.
Зашифровывает содержимое буфера с помощью ключа
шифровки.
i Основы шифровки 227
Функция CryptoAPI
CryptExportKeyO
CryptGenKeyO
CryptGetHashParamO
CryptGetKeyParam()
CryptGetProvParam()
CryptGetUserKey()
CryptHashData()
CryptlmportKeyO
CryptReleaseContext()
CryptS et Pro vParam()
CryptSetProvider()
CryptSignHash()
CryptVerifySignatureO
Описание
Возвращает ключевой блок для ключа. Кеу_ЫоЬ является
шифрованной копией ключа, которую вы передать получателю.
Обычно ключевые блоки используются для кодирования
одиночного ключа, передаваемого вместе с зашифрованным
этим ключом документом.
Генерирует случайные ключи для использования с CSP.
Получает данные, ранее ассоциированные с hash-объектом.
Получает данные, ранее ассоциированные с ключом.
Получает данные, ранее ассоциированные с CSP.
Возвращает дескриптор подписи или ранее определенного
ключа (в противоположность ключу, который выводится из
смешанного объекта).
Смешивает поток данных.
Извлекает ключ из ключевого блока.
Освобождает дескриптор ключевого контейнера.
Настраивает операции CSP.
Устанавливает CSP по умолчанию.
С помощью ключа шифровки ставит цифровую подпись на
поток данных. (О цифровых подписях мы подробнее расскажем
в дальнейших разделах главы.)
Проверяет цифровую подпись hash-объекта.
Каждая из функций обращается к CryptoAPI с запросом, суть которого
выражена в ее имени. Например, экспорт ключа производится в два этапа.
Сначала нужно получить от CSP общий ключ пользователя, вызвав CryptGetTJser-
Кеу(). Затем полученный дескриптор ключа может быть использован в вызове
CryptExportKeyO, возвращающем ключевой блок. Этот блок вы уже можете
передать другому пользователю, который с помощью функции
CryptlmportKeyO извлечет из него ключ.
Возвращенный дескриптор HCRYPTPROV является свидетельством того,
что между приложением и CSP установлен рабочий сеанс. Можно указать
конкретный CSP, с которым вы хотите связаться, а также контейнер ключей, к
которому хотите получить доступ. Эти спецификации передаются в
параметрах pszProvider и pszContainer. Можно ограничить свои требования
указанием типа сервера. Функция пытается сначала найти сервер с данным именем и
характеристиками. Если поиск успешен, функция ищет в данном CSP
контейнер pszContainer. Параметр dwFlags позволяет вам создать новый ключевой
контейнер или удалить существующий. Полученный от функции дескриптор
должен освобождаться вызовом CryptReleaseContextQ.
Следующий фрагмент, например, показывает типичную процедуру
получения контекста:
HCRYPTPROV hProv - NULL;
CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, 0);
8*
228
Глава 7
Этот вызов, в котором оба аргумента pszProvider и pszContainer равны
нулю, соединяет ваше приложение сервером RSA по умолчанию и его
ключевым контейнером по умолчанию. Функция CryptSetProvider() позволяет
изменить сервер по умолчанию, но Microsoft не рекомендует этого делать.
Никакие из функций, имеющих дело с CSP (кроме CryptAcquireContext(»,
не должны вызываться на уровне приложения; они могут вызываться только
из системных и административных инструментов. CryptAcquireContext()
передает вам дескриптор криптографического сервера. Его вы затем используете
для создания или порождения ключей.
Есть два метода генерирования ключей сеанса: случайный, посредством
вызова CryptGenKeyO, и hash-метод с вызовом Crypt Deri veKey(). CryptGenKeyO
принимает четыре параметра:
BOOL CryptGenKey{HCRYPTPROV hProv, ALG_ID algid,
DWORD dwFlags, HCRYPTKEY *phKey);
Первый параметр является дескриптором выбранного CSP, а последний —
контейнером, который будет содержать действительный дескриптор ключа.
Аргумент algid специфицирует алгоритм, который должен применяться при
создании ключа. RSA Base Provider предлагает два варианта выбора: CALG_RC2 и
CALG_RC4. Ключ может быть или не быть экспортируемым.
Как упоминалось выше, ключ сеанса является нестабильным объектом,
если только вы не присваиваете ему атрибут CRYPT_EXPORTABLE. Этот флаг
позволяет ключу порождать ключевой блок, являющийся не чем иным, как
просто закодированным его вариантом. Часто такой ключевой блок
присоединяется к сообщению или файлу.
Процедура экспорта ключа состоит из двух этапов и связана с
использованием другого специального объекта — ключа обмена. Первый шагом является
запрос у CSP открытого ключа пользователя вызовом CryptGetUserKey().
Получив дескриптор ключа обмена, вы можете затем образовать ключевой блок.
Следующий фрагмент кода выполняет эти шаги и возвращает ключевой блок в
переменной pBuf:
HCRYPTKEY hXKey - NULL;
LPBYTE pBuf = NUIA;
CryptGetUserKey(hProv, AT_KEYEXCHANGE, fihXKey);
CryptExportKey(hKey, hXKey, SIMPLEBLOB, 0, NULL, SdwSize);
pBuf = (LPBYTE) malloc(dwSize);
CryptExportKey(hKey, hXKey, SIMPLEBLOB, 0, pBuf, fidwSize);
Функция CryptExportKey() возвращает ключевой блок, записанный в
буфер pBuf. Если этот параметр нулевой, CryptExportKeyO возвращает размер
требуемой памяти. Чтобы декодировать ключевой блок и, наконец,
расшифровать документ, получатель вызывает CryptlmportKeyO, передавая ей
ключевой блок и получая взамен дескриптор HCRYPTKEY.
Ключ сеанса может быть генерирован и посредством смешивания. Вам
нужно только предоставить для этого некоторый поток данных и
инициализированный ранее hash-объект. Сначала вызывается функция CryptCreateHash(),
создающая новый hash-объект; затем — CryptHashData(), которая смешивает
в нем переданные значения; и, наконец, CryptDeriveKey() генерирует из
смешанного объекта ключ и возвращает его дескриптор HCRYPTKEY. Эта
процедура иллюстрируется следующим фрагментом кода:
Основы шифровки
229
CryptCreateHash(hProv, CALG_MD5, 0, О, £hHash);
CryptHaahData(hHash, szData, lstrlen(azData));
CryptDeriveKey(hProv, CALG_RC4, hHash, 0, hKey);
Параметр szData представляет строку, чье содержимое смешивается с
применением алгоритма MD5. Обычно в качестве szData указывают пароль. У
всех созданных объектов имеются деструкторы, которые должны вызываться
по завершении обработки.
Цифровые подписи в CryptoAPI
Если вы хотите снабдить документ цифровой подписью, нужно
воспользоваться hash-функциями CryptoAPI. Для подписи и проверки документа
создается смешанный объект, в нем смешиваются данные (как это описано выше) и
затем вызывается CryptSignHash(). Следующий фрагмент кода выполняет все
эти действия, формируя подпись в переменной pBuf:
CryptCreateHashfhProv, CALG_MD5, 0, 0, fihHash);
CryptHashData(hHash, szDocContent, sizeOfDoc));
CryptSignHash(hHash, AT_SIGNATURE, szDesc, 0, NULL, SdwSize);
pBuf = (LPBYTE) malloc(dwSize);
CryptSignHash(hHash, AT_SIGNATURE, szDesc, 0, pBuf, SdwSize);
В этом примере pBuf содержит информацию подписи, обычно
записываемую в отдельный файл, a szDesc является строкой описания. Те же самые
параметры участвуют в идентификации подписи, выполняемой функцией Crypt-
VerifySignature(). Кроме того, функции Crypt Sign HashO требуются
смешанный исходный файл и открытый ключ для подписи файлов. Этот ключ
возвращается CryptGetUserKey() с флагом AT_SIGNATURE.
Поддержка CryptoAPI в программах MFC
Ясно, что CryptoAPI предоставляет программисту мощные средства для
управления шифровкой. Они сильно упрощают оснащение ваших приложений
элементами криптографии. Например, прилагаемый диск содержит
программу CryptoNotes. Она написана на основе каркаса, генерированного с помощью
MFC AppWizard.
Ядром этой программы является класс документа CCryptoDoc. Он
производится от CDocument и переопределяет два метода, OnOpenDocument() и OnSa-
veDocument().
CDocument реализует методы OnOpenDocument() и OnSaveDocument(),
которые вызываются при выборе пользователем команд «открыть* и «сохранить
файл». Рис. 7.11 показывает, как CCryptoDoc изменяет поведение этих методов.
Если нажать кнопку Open на инструментальной панели приложения и
выбрать имя существующего файла, будет вызвана On Open Document О с
выбранным именем в качестве аргумента (аналогичные действия выполняются для
команд Save и Save As). Вся существенная обработка в приложении
CryptoNotes происходит внутри класса CCryptoDoc, код которого мы сейчас разберем.
Однако в целях экономии места мы рассмотрим только две феализации
функций класса — OnSaveDocument() и EncryptFile().
230
Глава 7
Рис.7.11.
Процессы открытия и
сохранения, выполняемые
классом документа
CCryptoDoc
OnSaveDocument()
TempFile
TempFile
дешифровка
шифровка
шифрованный файл
шифрованный файл
OnOpenDocument(
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Из-за особенностей строения заголовочного файла <wincrypt.h> в
Visual C++ 6.0 вы не сможете корректно компилировать программу кроме
как в Windows NT 4.0, если только не внесете в заголовок некоторые
изменения. Проще всего модифицировать этот заголовок,
закомментировав условную директиву проверки операционной системы в третьей
строке файла. Не забудьте закомментировать и директиву #endif в
конце условного блока.
Код класса CCryptoDoc
Вот код реализации функций OnSaveDocument() и EncryptFileO класса
CCryptoDoc.
. V/ CryptoDoc.cpp : реализация класса CCryptoDoc
,#include "stdafx.h"
•■ttinclude "CryptoDoc.h"
" -""ttinclude "PswdDlg.h"
.'■ ftdefine TEMP T("c:\\temp.ctx")
*■ '(
BOOL CCryptoDoc:lOnSaveDocument(LPCTSTR IpszPathName)
TCHAR szPswd[PSWD_MAXSIZE];
BOOL b, bPswd;
if(!m_bCryptoEnabled)
"> и return CDocument::OnSaveDocument(IpszPathName);
CDocument::OnSaveDocument(TEMP);
Основы шифровки 231
bPswd = GetPassword(IpszPathName, szPswd);
b = EncryptFile(TEMP, IpszPathName, (bPswd ?szPswd :NULL));
DeleteFile(TEMP);
return b;
}
I
j
1
'BOOL CCryptoDoc::EncryptFile(LPCTSTR szSource, LPCTSTR szTarget,
i LPCTSTR szPswd)
,<
HCRYPTPROV hProv=NULL;
HCRYPTKEY hKey=NULL, hXchgKey=NULL;
HCRYPTHASH hHash=NULL;
i PBYTE pbKeyBlob=NULL, pbBuffer=NULL;
] DWORD dwKeyBlobLen, dwNumOfBytes, dwBlockLen-1000;
BOOL bResult-FALSE, bOkay=FALSE, bEof=FALSE;
HFILE hSource = _lopen(szSource, OF_READ);
if (hSource=HFILE_ERROR)
return FALSE;
HFILE hTarget = _lcreat(szTarget, 0);
1 if (hTarget—HFILE_ERROR)
I goto exit;
, bOkay = CryptAcquireContext(&hProv, NULL, NULL,
PROV_RSA_FULL, 0) ;
if(!ЬОкау)
goto exit;
if (szPswd=NULL) {
bOkay = CryptGenKey(hProv, ENCRYPT_ALGORITHM,
CRYPT_EXPORTABLE, fihKey);
if('bOkay)
goto exit;
bOkay = CryptGetUserKey(hProv, AT_KEYEXCHANGE, fihXchgKey);
if(!bOkay)
goto exit;
bOkay - CryptExportKey(hKey, hXchgKey, SIMPLEBLOB, 0,
NULL, SdwKeyBlobLen);
if(!bOkay)
goto exit;
pbKeyBlob ■ (PBYTE) malloc(dwKeyBlobLen);
if(pbKeyBlob==NULL)
goto exit;
bOkay = CryptExportKey(hKey, hXchgKey, SIMPLEBLOB,
0, pbKeyBlob, SdwKeyBlobLen);
if(JbOkay)
goto exit;
CryptDestroyKey(hXchgKey);
hXchgKey = NULL;
INT iFilelD = CTX_RANDOM_KEY;
__hwrite(hTarget, (LPCSTR7siFileID, sizeof(INT));
dwNumOfBytes = _hwrite(hTarget, (LPCSTR)fidwKeyBlobLen,
sizeof(DWORD));
if(dwNumOfBytes != sizeof(DWORD))
goto exit;
dwNumOfBytes = _hwrite(hTarget, (LPCSTR)pbKeyBlob,
dwKeyBlobLen);
if(dwNumOfBytes != dwKeyBlobLen)
goto exit;
}
else (
ЬОкау = CryptCreateHash(hProv, CALG_MD5, 0, 0, thHash);
if(!bOkay)
goto exit;
ЬОкау = CryptHashData(hHash, (PBYTE) szPswd,
lstrlen(szPswd), 0);
if(!bOkay)
goto exit;
ЬОкау = CryptDeriveKey(hProv, ENCRYPT_ALGORITHM, hHash,
0, ShKey);
if(!bOkay)
goto exit;
CryptDestroyHash(hHash);
hHash = NULL;
INT iFilelD = CTX_HASH_KEY;
INT iPswdLen = lstrlen(szPswd);
TCHAR szBuf[PSWD_MAXSIZE];
Istrcpy(szBuf, szPswd);
_hwrite(hTarget, (LPCSTR)eiFilelD, sizeof(INT));
_hwrite(hTarget, (LPCSTR)SiPswdLen, sizeof(INT));
for(int i=0; KiPswdLen; i++)
szBuffil = 255-szBuf[i];
_hwrite(hTarget, szBuf, iPswdLen);
}
pbBuffer = (PBYTE) malloc(dwBlockLen);
if(pbBuffer==NOLL)
goto exit;
do {
dwNumOfBytes = _hread(hSource, pbBuffer, dwBlockLen);
if <dwNumOfBytes==HFILE_ERROR)
goto exit;
bEof = (dwNumOfBytes < dwBlockLen);
Основы шифровки
233
r-jJ.
ЬОкау = CryptEncrypt(hKey, NULL, bEof, 0, pbBuffer,
SdwNumOfBytes, dwBlockLen) ;
if(IbOkay)
goto exit;
DWORD dwWritten = _hwrite(hTarget, (LPCSTRJpbBuffer,
dwNumOfBytes);
if <dwWritten==HFILE_ERROR)
goto exit;
> while(IbEof);
bResult = TRUE;
// очистка файлов, ключей и сервера
exit:
if(hSource)
_lclose(hSource);
if(hTarget)
_lclose(hTarget);
if(pbKeyBlob)
free(pbKeyBlob);
if(pbBuffer)
free(pbBuffer);
if(hKey)
CryptDestroyKey(hKey);
if(hXchgKey)
CryptDestroyKey(hXchgKey);
if(hHash)
CryptDestroyHash(hHash);
if(hProv)
CryptReleaseContext(hProv, 0);
return bResult;
}
| ПР1/
ПРИМЕЧАНИЯ
Ниже следуют пояснения к коду двух упомянутых функций класса
CCryptoDoc; мы начнем с OnSaveDocument().
BOOL CCryptoDoc::OnSaveDocument(LPCTSTR IpszPathName)
{
TCHAR szPswd[PSWD_MAXSIZE];
BOOL b, bPswd;
if(!m_bCryptoEnabled)
return CDocument::OnSaveDocument(IpszPathName);
Функция OnSaveDocumentO принимает в качестве параметра строку,
указывающую маршрут файла. Затем она объявляет некоторые локальные
переменные и проверяет, активирована ли криптография. Если нет, происходит
немедленный выход (с вызовом стандартной функции сохранения из CDocument).
CDocument::OnSaveDocument(TEMP);
bPswd ~ GetPassword(IpszPathName, szPswd);
b - EncryptFile(TEMP, IpszPathName, (bPswd ?szPswd :NULL));
234
Глава 7
DeleteFile(TEMP);
return b;
}
Затем функция создает временный документ, используя для его имени
константу TEMP. Если пользователь указал, что файл должен сохраняться с
паролем, программа получает этот пароль. Всю эту информацию она передает
функции EncryptFile(), генерирующей зашифрованный файл. OnSaveDocu-
ment() заканчивает свою работу, удаляя временный файл и возвращая
управление вызывающей программе.
Функция EncryptFile() принимает в качестве параметров исходный файл,
целевой файл и необязательный пароль. Если пароль указан, он используется
для генерирования шифрующего ключа; в противном случае программа
получает ключ от CryptGenKey().
Зашифрованный файл, естественно, должен допускать дешифровку,
поэтому ключ нужно где-то сохранить. Здесь наконец находит себе применение
концепция ключевого блока. Создавая ключ, вы делаете его экспортируемым и
сохраняете ключевой блок вместе с размером последнего в начале целевого
файла. В Microsoft RSA Base Provider размер ключевого блока равен 76 байтам.
Чтобы упростить дело, CryptoNotes записывает в начало файлов .ctx короткий
заголовок, указывающий, использовался ли при их кодировании пароль.
Первые четыре байта представляют собой просто идентификатор файла (значение
типа LONG) с так называемыми «магическими числами».
Если файл сохраняется с паролем, EncryptFile() создает смешанный
объект, кодирует данные и сбрасывает их на диск. Хотя случайный ключ
необходимо сделать сохраняемым, записывать пароль вместе с данными файла в
принципе нет необходимости. Пароль — это просто слово, хотя, возможно,
довольно странное. Его можно как-то передать или запомнить. Ключ, с другой
стороны, является нерегулярной последовательностью битов, которые гораздо
труднее держать в памяти. Для упрощения формата файла пароль шифруется
просто применением операции NOT, а не средствами CryptoAPI.
Функция-элемент GetPassword() исследует данный файл и возвращает
пароль, если таковой имеется. DecryptFile() загружает данные открываемого
файла. Если файл .ctx зашифрован паролем, перед дешифровкой потребуется
его ввести. Если вы попытаетесь выполнить дешифровку с неправильным
смешанным значением, то получите бессмысленный набор байтов.
Давайте теперь рассмотрим функцию EncryptFile(). Ниже показан ее
заголовок и локальные переменные:
BOOL CCryptoDoc::EncryptFile(LPCTSTR szSource, LPCTSTR szTarget,
LPCTSTR szPswd)
{
HCRYPTPROV hProv=NULL;
HCRYPTKEY hKey=NULL, hXchgKey=NULL;
HCRYPTHASH hHash=NDLL;
PBYTE pbKeyBlob=NOLL, pbBuffer=NULL;
DWORD dwKeyBlobLen, dwNumOfBytes, dwBlockLen=1000;
BOOL bResult=FALSE, bOkay=FALSE, bEof=FALSE;
Функция принимает параметры, соответствующие имени файла, который
должен быть зашифрован, новое имя, под которым будет сохраняться его
шифрованный вариант, и пароль файла, если пользователь его указывает. Сле-
Основы шифровки
235
дующие несколько строк объявляют дескрипторы различных
криптографических компонентов, а последние строки определяют переменные для хранения
шифруемой информации и флагов завершения.
Далее открываются два файла: исходный, содержащий незашифрованный
текст, и целевой, в который будет записана зашифрованная информация. Если
какой-то из файлов открыть не удается, функция немедленно завершается.
HFILE hSource = _lopen(szSource, OFJREAD);
if(hSource==HFILE_ERROR)
return FALSE;
HFILE hTarget = __lcreat(szTarget, 0);
if(hTarget==HFlLE_ERROR)
goto exit;
Затем программа запрашивает контекст сервера RSA. Контекст
возвращается в виде дескриптора hProv, а код успешности операции записывается в
локальную переменную ЪОкау. Если выделить контекст не удалось, программа
завершается.
ЪОкау = CryptAcquireContext(fihProv, NULL, NULL,
PROV_RSA_FULL, 0);
if(!bOkay)
goto exit;
Первый из показанных ниже операторов if проверяет, выбрал ли
пользователь пароль для сохранения файла. Если пароль указан, управление
передается блоку else, где криптографический ключ генерируется из пароля. Если
пользователь не выбрал пароль, программа создает ключ случайным образом,
применяя алгоритм RSA.
if (szPswd=NULL) {
bOkay * CryptGenKey(hProv, ENCRYPT_ALGORITHM,
CRYPT_EXPORTABLE, fihKey);
if(tbOkay)
goto exit;
В следующих нескольких строках программа создает из базового ключа
пользовательский ключ и определяет, сколько места потребуется для его
экспорта в ключевой блок (как вы помните, ключевой блок — это blob, «большой
двоичный объект*). Опять же, если любая из операций терпит неудачу,
управление переходит к процедуре завершения функции.
bOkay = CryptGetUserKey(hProv, AT_KEYEXCHANGE, fihXchgKey);
if(IbOkay)
goto exit;
bOkay = CryptExportKeyfhKey, hXchgKey, SIMPLEBLOB, 0,
NULL, fidwKeyBlobLen);
if('ЪОкау)
goto exit;
После определения необходимого размера памяти предыдущим вызовом
CryptExportKeyO программа выделяет эту память локальной переменной
pbKeyBlob и экспортирует в нее ключевой блок:
236
Глава 7
pbKeyBlob = (PBYTE) malloc(dwKeyBlobLen);
if (pbKeyBlob=WLL)
goto exit;
bOkay = CryptExportKey(hKey, hXchgKey, SIMPLEBLOB,
0, pbKeyBlob, fidwKeyBlobLen);
if(IbOkay)
goto exit;
Затем ключ, использованный для генерирования ключа пользователя и
ключевого блока, уничтожается:
CryptDes troy Key (hXchgKey) ;
hXchgKey - NULL;
Теперь программа готова зашифровать и сохранить файл.
Константа CTX_RANDOM_KEY показывает, что ключ был генерирован
случайным образом — т. е. без пароля — и потому, когда программа снова
будет читать файл, она определит, нужно ли выводить подсказку с запросом
пароля или следует использовать записанный ключевой блок. Программа
записывает константу и размер ключевого блока в начало файла.
INT iFilelD - CTX_RANDOM_KEY;
_hwrite(hTarget, (LPCSTR)SiFilelD, sizeof(INT));
dwNumOfBytes = _hwrite(hTarget, (LPCSTR)fidwKeyBlobLen,
sizeof(DWORD));
if(dwNumOfBytes != sizeof(DWORD))
goto exit;
После этого заголовка в файл записывается сам ключевой блок. На этом
выполнение внешнего условного оператора заканчивается, и программа переходит
в конец функции, где происходит действительная шифровка и запись файла.
dwNumOfBytes = _hwrite(hTarget, (LPCSTR)pbKeyBlob,
dwKeyBlobLen);
if(dwNumOfBytes !- dwKeyBlobLen)
goto exit;
)
Если пользователь вводит пароль, процесс обработки файла выглядит
несколько иначе. Блок else условного оператора, ответственный за этот случай,
начинается с создания смесителя, через который программа пропускает
пароль. (Как вы помните, смеситель — это функция, формирующая уникальное
значение из входных данных.)
else {
bOkay = CryptCreateHash(hProv, CALG_MD5, 0, 0, fihHash);
if(!bOkay)
goto exit;
bOkay = CryptHashData(hHash, (PBYTE) szPswd,
lstrlen(szPswd), 0);
if(!bOkay)
goto exit;
Основы шифровки
237
Программа вызывает CryptHashData(), чтобы пропустить пароль через
смеситель и сохранить результат в дескрипторе hHash. Как и прежде, неудачное
завершение операции приводит к выходу из функции.
ЬОкау - CryptDeriveKey(hProv, ENCRYPT_ALGORITHM, hHash,
0, fihKey);
if(!bOkay)
goto exit;
В отличие от блока if здесь для получения ключа, который будет
использован для шифровки текста, вызывается CryptDeriveKeyO, которая выводит
ключ из переданного ей дескриптора смешанного объекта,
CryptDestroyHash(hHash);
hHash = NULL;
INT iFilelD - CTX_HASH_KEY;
INT iPswdLen = lstrlen(szPswd);
TCHAR SzBuf[PSWD_MAXSIZE];
lstrcpy(szBuf, szPswd);
_hwrite(hTarget, (LPCSTR)fiiFilelD, sizeof(INT));
_hwrite(hTarget, (LPCSTR)fiiPswdLen, sizeof(INT));
Здесь снова программа выполняет необходимую очистку, уничтожая
смешанный объект — он больше не нужен, поскольку ключ уже генерирован.
Затем в файл записывается константа CTX_HASH_KEY, указывающая
программе при обратном чтении файла функцией OnOpenDocument(), что ключ
шифра для текста получен из пароля. После константы записывается длина
пароля, чтобы программа знала, сколько символов, составляющих пароль, нужно
будет считать из файла.
for{int 1=0; KiPswdLen; i++)
szBuf[i] = 255-szBuf[i];
_hwrite(hTarget, szBuf, iPswdLen);
}
Цикл for записывает символы пароля в файл, шифруя их посредством
простой инверсии битов. Программа переходит затем к процедуре, записывающей
собственно зашифрованный файл.
Код выделяет буфер размером в 1000 символов (значение dwBlockLen),
который будет использоваться для поблочного чтения файла и шифровки
прочитанных данных. Этот размер можно уменьшить или увеличить, но 1000
представляется довольно разумным числом.
pbBuffer - (PBYTE) malloc(dwBlockLen);
if(pbBuffer==NULL)
goto exit;
Действия следующего цикла достаточно очевидны — он читает блок
данных из файла и вызывает CryptEncrypt() для их шифровки. Затем уже
закодированный блок пишется в целевой файл. На каждом шаге программа
проверяет, не произошло ли ошибки, чтобы гарантировать нормальное протекание
всего процесса.
do {
dwNumOfBytes = __hread(hSource, pbBuffer, dwBlockLen);
if (dwNumOf Bytes—HFILE_ERROR)
goto exit;
238
Глава 7
bEof = (dwNumOfBytes < dwBlockLen);
bOkay = CryptEncrypt(hKey, NULL, bEof, 0, pbBuffer,
SdwNumOfBytes, dwBlockLen);
if(IbOkay)
goto exit;
DWORD dwWritten = _hwrite(hTarget, (LPCSTR)pbBuffer,
dwNumOfBytes);
if (dwWritten=HFILE_ERROR)
goto exit;
} while(!bEof);
Если программа дошла до этой точки нормальным образом, значит, все в
порядке и теперь осталось только очистить выделенные ресурсы. Если же
программа оказалась здесь в результате перехода goto, то bResult будет равно false
и программа просто выполнит необходимую очистку и выйдет из функции.
bResult = TRUE;
exit:
Следующий ряд операторов достаточно понятен; каждый из них проверяет,
существует ли некоторый дескриптор. Если существует, код выполняет его
очистку. В случае дескрипторов файлов они закрываются.
if(hSource)
_lclose(hSource);
if(hTarget)
_lclose(hTarget);
if (pbKeyBlob)
free(pbKeyBlob);
if(pbBuffer)
free(pbBuffer);
if(hKey)
CryptDestroyKey(hKey);
if(hXchgKey)
CryptDestroyKey(hXchgKey);
if(hHash)
CryptDestroyHash(hHash);
if(hProv)
CryptReleaseContext(hProv, 0);
return bResult;
}
Когда вы сохраняете файл, в окне программы CryptoNotes вы увидите
обычный текст. Однако если открыть файл какой-то другой программой
(например, Notepad), его содержимое предстанет в совершенно ином виде.
Рис. 7.12 показывает простой текстовый файл в окне CryptoNotes и в
редакторе Notepad.
Последние замечания о CryptoNotes и шифрах
Итак, вы увидели, как программа CryptoNotes использует средства
интерфейса криптографии Windows для шифровки файла при его сохранении. Те же
самые принципы лежат в основе процедуры открытия файла. Когда вы выби-
Основы шифровки
239
Рис. 7.12.
Зашифрованный
файл не может
быть прочитан
программами без
поддержки
соответствующей
криптографии
_"Х.* .■->..- - Л-л* *
^£:>*3*-*L*&££*>*• 1:; >;«л*,„С>л-л^;.- "-7* "- "
>!*«<
Vt3
*>iS'f „VJj-V
rff".
rt-«i-tlr**<
.* J-jUfjieJSiSjWS'^^tfl*»".'- '-3?- ''a'v-*4sy - - ' *j'i tffcf* wV' .'ft,';" •'*«*■***,
'■'V W/. iff ,-.3~?&<WV !,.'«>''
раете имя для сохранения файла, класс прежде всего сбрасывает его на диск
под временным именем. Действительное кодирование и сохранение
корректного файла .ctx происходит на следующем шаге.
Создание временного файла, хотя он и может быть полезен, в
действительных реализациях приложений выглядит несколько громоздко. К сожалению,
иной подход невозможен, если вы хотите сохранить интерфейс высокого
уровня с приложениями MFC, главным образом из-за ориентированного на файлы
характера сериализации в MFC.
Помимо метода OnSaveDocumentQ и функции EncryptFile(), класс CCrypt-
Doc экспортирует два открытых метода OnOpenDocument() и OnSaveDocu-
240
Глава 7
mentWithPswd(). Имеются также две защищенных функции-элемента,
DecryptFileO и GetPassword().
Программа CryptoNotes оснащена стандартными пунктами меню MFC,
дополнительной командой Save With Password и меню Options с помечаемым
пунктом Encryption. Он позволяет активировать или отключить
криптографические возможности вашего приложения, вызывая CCryptDoc::EnableEncryp-
tion(). Если шифровка отключена, управление передается стандартным
методам базового класса.
Какой же итог можно подвести, когда все уже сказано? Каждое из
решений — поточная, блочная, шифровка с общим ключом — имеет свои
достоинства и ограничения, так что тот или иной подход в специфической ситуации
может оказаться наиболее приемлемым и выгодным. Перед принятием
окончательного решения вы должны тщательно продумать, как ваши программы
будут применять криптографию. И не следует думать, что в конкретной
программе вы.связаны единственной методикой — для различных задач
программа может прибегать к различным методам, наиболее подходящим для той или
иной задачи.
It «,
Token.h
Token.cpp
Scanner.h
Scanner.cpp
CodeMaintCPP.h
CodeParser.h
CpdeParser.cpp
SCodeMnt.cpp
.таЗаИ
' ^
J*
Л1Д1
.-^.,-.«
242
Глава 8
Обработка исходного кода является одной из самых интересных задач,
встающих сегодня перед программистами. По мере того как программы
становятся все более длинными и сложными, нам нужно как-то
организовывать их и управлять возрастающими объемами кода. В этой главе мы
исследуем три утилиты, которые помогут вам контролировать состояние ваших
исходных файлов. Но прежде чем начать, попробуем точно сформулировать, что мы
имеем в виду, когда говорим об управлении исходным кодом.
Вообще управление исходным кодом подразумевает два рода деятельности:
анализ и манипулирование. Анализ исходного кода может дать полезную
информацию, которая может не быть непосредственно очевидной или легко
доступной. Например, когда различные проекты разделяют одни и те же файлы,
трудно проследить, какие файлы зависимы от других. Утилита управления
исходным кодом может исследовать файл кода и генерировать список
дополнительных файлов, которые нужны разделяемому файлу. Изощренная программа
сопровождения может произвести синтаксический разбор кода и генерировать
документацию по каждому классу, его элементам данных и функциям.
Исходный код можно модифицировать, чтобы его форма соответствовала
стандартизованным стилям, или для того, чтобы более отобразить поток
управления программы. Например, стандартный формат отступов может
облегчить совместную работу с файлами в рабочей группе программистов.
Поддержание структуры кода в группе — очень трудная и отнимающая много
времени задача, поскольку программистам приходиться менять привычный
стиль написания кода. Один может настаивать на том, что открывающая
фигурная скобка блока оператора if должна стоять на отдельной строчке, в то
время как другой предпочитает ставить ее сразу за условным выражением, в
строке с if. Форматер исходного кода предлагает простое решение этой
проблемы.
В этой главе мы спроектируем и напишем программу управления исходным
кодом, которая сканирует код и выводит его в несколько ином формате.
Программа исполняет три основных команды:
♦ Первая, /include, предписывает программе сканировать файл C++ и
распечатать все имена файлов, включенных в сканируемый файл.
♦ Вторая команда, /html, генерирует «расцвеченный* вариант входного
файла на HTML. После синтаксического разбора входного файла в новый
файл дописываются ярлычки HTML, в которые заключаются различные
синтаксические элементы, так чтобы элементы различных типов
выделялись в HTML-программе просмотра различным цветом.
♦ Команда /ai формирует отступы строк в файле в соответствии с
определенными для языка правилами.
Важной особенностью двух последних команд является то, что они не
ограничены привязкой исключительно к языку C++. Хотя модули программы
написаны специально для сопровождения кода на C++, они все же достаточно
общи, чтобы при небольшой модификации работать со многими языками
программирования .
Управление исходным кодом
243
Проектирование программы управления кодом
Многие задачи являются общими для «внешнего слоя* компилятора и
модулей программы сопровождения кода. Они получают в качестве входных
данных исходный код, разбивают код на лексемы и затем выводят эти лексемы в
новом формате. Выходные данные компилятора — это машинный или р-код.
Вывод программы сопровождения зависит от конкретной задачи,
выполняемой тем или иным модулем. В некотором отношении программы управления
кодом напоминают упрощенные версии компиляторов. Однако им недостает
сложности, присущей модулям генерации кода в компиляторах, и вообще к
ним не предъявляется требований по производительности, обычно
подразумеваемых для компиляторов. Читатели, знакомые с устройством компиляторов,
легко разберутся в разработке программ сопровождения кода из этой главы.
Базовая схема процесса сопровождения кода показана на рис. 8.1.
Главная
программа
Синтаксический
анализатор
Сканер I
Parse(istream&,ostream&)
GetNext(TokenO)
WriteCurrent(TokenO)
GetNext(TokenTypeO)
CreateToken(eTokenType)
GetNext(TokenO)
WrlteCurrent(Token(})
Ge tNext( Token Type())
CreateToken(eTokenType)
Рис. 8.1. Процесс сопровождения кода
Наиболее элементарный уровень любого языка программирования
представлен лексемами (tokens). Лексемы являются наименьшими
распознаваемыми единицами программы; они образуют строительные блоки, из которых
создаются осмысленные операторы. Компиляторы обычно не работают с чем-либо
меньшим лексемы. Примерами лексем являются комментарии, константы,
идентификаторы, числа, знаки пунктуации и строковые литералы. Задача
244 Глава 8
чтения кода и разбиения его на эти элементы возложена на сканер, который
затем возвращает их синтаксическому анализатору. В процессе разбиения
кода сканер также идентифицирует тип возвращаемых лексем.
Синтаксический анализатор запрашивает у сканера последовательные
лексемы и предпринимает соответствующие действия перед тем, как
запросить следующую лексему. На рис. 8.1 действие анализатора состоит в выводе
лексемы.
Утилита управления исходным кодом, разрабатываемая в этой главе,
базируется на трех группах классов: лексемах, сканерах и анализаторах. Для
каждой группы имеется файл объявлений, в котором определяются классы, и
файл реализации, содержащий код. Мы разберем каждую из групп по
отдельности; каждая группа реализует функциональные свойства предыдущей
группы, поэтому рассматриваться они должны в определенном порядке.
Классы лексем
Чтобы упростить адаптацию к особенностям конкретных приложений, код
класса лексем был реализован в виде отдельных классов для каждого типа
лексем. Хотя это увеличивает общий объем кода, пользователям проще
приспособить классы к своим потребностям. Такой объектно-ориентированный
подход позволяет инкапсулировать специфику типов лексем и обеспечивает
большую гибкость в плане дальнейшей разработки.
Код
Показанный ниже файл Token.h определяет класс CToken и производные
от него подклассы.
// Token.h: Определяет? различные классы лексем.
///////////////////////////////////////////////////////////////////
#if !defined(TOKEN_H_INCLUDED)
#define TOKEN_H_INCLUDED
//
// Идиосинкразии Microsoft.
//
#if _MSC_VER > 1000
// Отключить предупреждение С4786:
// symbol greater than 255 character, okay to ignore (MSVC)
ttpragma warning(disable: 4786)
#pragma once
#endif // _MSC_VER > 1000
//
// Включаемые файлы библиотек.
// -- _
#include <istream>
#include <string>
#include <set>
Управление исходным кодом
245
а
using namespace std;
// -
// Объявления typedef
// - —
typedef set<string> TokenTextList;
// — -
// Объявления enuni
// -
// Каждая лексема идентифицируется как специфический тип.
enum ETokenTypef
// Тип не был установлен.
eTokenTypeUnknown = 1,
// Конец файла.
eTokenTypeEOF,
// Конец строхи.
eTokenTypeEOL,
// Символ табуляции или пробел. Заметьте, что символы конца
// строки принадлежат к собственному типу лексем.
eTokenTypeWhiteSpace,
// Строка.
eTokenTypeString,
// Строка символа ('\п').
eTokenTypeCharacter,
// Число.
eTokenTypeNumeric,
// Комментарий.
eTokenTypeComment,
// Специальный вариант eTokenTypeComment, использующий по умолчанию
// ' \п' как идентификатор конца комментария.
eTokenTypeEOLComment,
// Любой набор операций. Заметьте, что операция и знак пунктуации
// являются одинаковыми лексемами (имеют одинаковое значение).
eTokenTypePunctuation,
eTokenTypeOperator = eTokenTypePunctuation,
// Слово. Обычно имя переменной или функции,
еТокеnTypeWord,
// Показывает, что оператор завершен.
// Замечание:
// В некоторых языках, например, vb, это просто
246
Глава 8
I // новая строка, так что таких лексем не будет. В C++,
-I // однако, используется ' ; ' .
t eTokenTypeStatementEnd,
: // Позволяет оператору продолжаться на следующей строке,
' // как если бы символ новой строки отсутствовал.
eTokenTypeLineContinuation,
1
j //Ни один из предыдущих.
eTokenTypeOther,
// Число типов лексем. Это минимальное значение, доступное
// для определения пользовательских типов лексем.
eTokenTypeCount
>;
I.
i
j//
\/1 Различные константы.
//
#define EOL_CHARACTER '\n'
// Следующие символы могут входить в представление чисел:
// а, Ь, с, d, е, £ для шестнадцатеричных, и для беззнаковых,
// 1 для длинных и i для 64-битных, е может представлять экспоненту
#define POSSIBLE_NUMERIC_CHARACTERS
"0123456789abcdefxABCDEFXuUlLi."
/
- '///////////////////////////////////////////////////////////////////
j// CToken
; .j///////////////////////////////////////////////////////////////////
t . |class CToken : virtual public string
//
// Конструктор/деструктор.
//
^public:
CToken ();
CToken(int nType);
[ -l virtual -CToken();
I
j//
.- <// Защищенные элементы данных.
; |//
, jprotected:
i ] // Тип лексемы. Обычно представлен типом ETokenType.
s int m_nType;
: \u
// Функции доступа к закрытым данным.
jj//
Управление исходным кодом
247
public:
virtual int GetType{) const
return m_nType;
virtual void SetType(const int mjnTokenType)
m_nType = m_nTokenType;
virtual string GetTokenText() const
return *this;
virtual void SetTokenText(const string sNewText)
this->assign{sNewText);
virtual void SetTokenText(const char cNewText)
this->erase() += cNewText;
***i
//
|// Вспомогательные функции.
i Ml
l? ■'protected:
, // Показывает, что символ с является буквой или подчеркиванием.
// Замечание: Visual C++ поддерживает встроенную функцию iscsymf
static int isalpha_(const char с)
{
}
return (isalpha(c) || с
);
a // Замечание: Visual C++ поддерживает встроенную функцию iscsymf
' ^ static int isalnum_(const char c)
{
return (isalnum(c) |j с = ' ');
// Поскольку istream.eof становится true только после
// попытки чтения за концом файла, необходимо "заглянуть
// вперед" перед вызовом istream.eof. Следующие функции
// возвращают корректные значения, так как производят
// это опережающее чтение.
static bool IsEOF(istream& input);
static bool IsNotEOF(istream& input);
// Следующие функции очищают бит eof входного потока.
// Это может быть необходимо, если при проверке
248
Глава 8
-• д // следующего символа встретился eof,
// даже если вы не хотите производить каких-либо
// модификаций на входном потоке.
static void CiearEOF(istream& input);
public:
// Следующие функции проверяют, не является ли переданная
// строка тем, на что указывает функция get() входного потока,
static bool IsSpeci£iedString(
// Проверяемый входной поток.
istreams input,
// Искомая строка.
const string sSearchFor);
M
.■l
static bool IsSpecifiedString{istream& input,
const char cSearchFor);
// Передвинуть указатель get потока istream на символ
// непосредственно после sGet. Предполагается, что текущий
// указатель get ссылается на начало строки, равной sGet.
// Если это не так, возвращается false.
static bool GetSpecifiedString(istream& input,
const string sGet);
static bool GetSpecifiedString(istream& input,
const char cGet);
// Возвращает индикатор того, что в строке больше не осталось
// значимых лексем. значимыми считаются лексемы, не
// являющиеся пробельными символоми.
static bool IsOnlyWhiteSpaceLeft(istreamfi input);
// Возвращает true, если в строке остались только комментарии
// или пробельные символы.
static bool IsOnlyWhiteSpaceOrCommentsLeft(istreamfi input,
const char* sBeginCommentldentifier,
const char* sEndCommentidentifier,
const char* sBeginEOLCommentldentifier);
■ib-
y/ EOFToken
Л class CEOFToken : public CToken
i? //
// Конструктор/деструктор.
//
public:
CEOFToken(istreamfi input);
//
// Функции идентификации.
//
Управление исходным кодом 249
, public:
! j static bool IsA(const string);
' static bool IsA(const char c);
static bool IsA(istream& input);
///////////////////////////////////////////////////////////////////
// CEOLToken
///////////////////////////////////////////////////////////////////
class CEOLToken : public CToken
<
//
// Конструктор/деструктор.
//
public:
CEOLToken(istreams input);
// Конструирует лексему EOL без наличного
// входного потока;
CEOLToken();
ь
}
L
I-
I.
\
I
//
// Функции идентификации.
// — -
public:
static bool IsA(const char c);
static bool IsA(istream& input);
);
///////////////////////////////////////////////////////////////////
// CWhiteSpaceToken
///////////////////////////////////////////////////////////////////
class CWhiteSpaceToken : public CToken
t
//
// Конструктор/деструктор.
// _— -
public:
CWhiteSpaceToken(istreamS input);
//
// Функции идентификации.
//
public:
static bool IsA(const char c);
static bool IsA(const string);
static bool IsA(istreamS input);
};
J l///////////////////////////////////////////////////////////////////
,|..j// CStringToken
250
Глава 8
* *
Г 1
>
'///////////////////////////////////////////////////////////////////
class CStringToken : public CToken
i
//
// Конструктор/деструктор.
//
public:
// cEscapeCharacter используется для идентификации esc-синвола.
// В C++, например, это был бы 'V . Он необходим для отображения
// ограничителя строки внутри строки, как в следующем случае:
// "Elisabeth said, \"Hi!\""
CStringToken::CStringToken(istream& input,
const char cStringldentifier,
const char cEscapeCharacter);
//
// Защищенные элементы данных.
//
protected:
char zo_cEscapeCharacter;
char m_cStringIdentifier;
// —
// Функции доступа к защищенным элементам.
//
public:
// Note that these values could be NULL
const char GetStringldentifier()
{ return m_cStringIdentifier; }
const char GetEscapeCharacter()
{ return m_cEscapeCharacter; }
//
// Функции идентификации.
// :
public:
// Проверяет, является ли следующая лексема входного
// потока строкой. Проверяется только первый символ,
static bool lsA(istreamfi input,
const char cStringldentifier,
const char cEscapeCharacter);
};
///////////////////////////////////////////////////////////////////
П CCharacterToken
///////////////////////////////////////////////////////////////////
class CCharacterToken : public CStringToken
{
//
// Конструктор/деструктор.
//
public:
Управление исходным кодом
251
// cEscapeCharacter используется для идентификации esc-синвола.
// В C++, например, это бьш бы 'V ■ Он необходим для отображения
// идентификатора символа внутри строки, как в следующем случае:
// '\"
CCharacterToken::CCharacterToken(istream£ input,
const char cCharacterldentifier,
const char cEscapeCharacter);
const char GetCharacterldentifier()
{ return m_cStringIdentifier; )
};
///////////////////////////////////////////////////////////////////
// CNumericToken
///////////////////////////////////////////////////////////////////
class CNumericToken : public CToken
{
//
// Конструктор/деструктор.
//
public:
// Заметьте, что текст не проверяется на соответствие
// корректному числу; например, ОXX тоже будет расценено
// как число. Компиляторы такого не пропускают.
CNumericToken::CNumericToken(istreams input);
//
// Защищенные элементы данных.
* V/
, «protected:
t 1
//
// Функции доступа к элементам данных.
//
public:
\v
V
£.i
// -
// Функции идентификации.
//
public:
// Проверяет, что вся строка sText является числом,
static bool IsA(const string sText);
// Проверяет, что следующая лексема входного потока
// является числом. Проверяется только первый символ,
static bool IsA(istream& input);
};
///////////////////////////////////////////////////////////////////
// CCommentToken
»J///////////////////////////////////////////////////////////////////
252
Глава 8
* '\
class CConunentToken : public CToken
{
//
i// Конструктор/деструктор.
//
public:
CCommentToken(i streams input,
const string sBeginCommentldentifier,
const string sEndCommentldentifier);
//
:t Jf Защищенные элементы данных.
у
pro :;ected:
ч] string m_sBeginCommentIdentifier;
string m_sEndCommentXdentifier;
j//
// Функции доступа к элементам данных.
// „_
public:
const char* GetBeginCommentldentifier()
{ return m_sBeginCommentIdentifier.c_str() ; )
const char* GetEndCommentldentifier()
{ return m_sEndCommentIdentifier.c__str() ; }
// _
// функции идентификации.
//
public:
// Возвращает true, если следующая лексема является
// комментарием, идентифицированным sBeginCommentldentifier
// и sEndCommentldentifier.
static bool IsA( istreams input,
const string sBeginCommentldentifier,
const string sEndCommentldentifier - "",
// Проверить, что крмментарий заканчивается прежде конца.
// файла.
const bool bCheckEnd = false);
ь-
ч ,
i
///////////////////////////////////////////////////////////////////
// CEOLCommentToken
///////////////////////////////////////////////////////////////////
class CEOLCommentToken : public CCommentToken
{
//
-i*va / / Конструктор /деструктор.
- '•//
"^
public:
*■ A // Заметьте, что конец строки закрывает лексему,
// даже если имеется символ LineContinuation.
///////////////////////////////////////////////////////////////////
иэчолрдомэ //
///////////////////////////////////////////////////////////////////
•Ч
•Hajrtredsuo волэвинв //
вэголои ojohVoxs ВМЭОМЭ1Г веЪкяЛНэшэ Oibfc 'jiBHdsaodii //
.' (q.xaj;s fiuxci^s ^suoojvsi t°oq эт^е^в
'иийвАьмнАц инехвне колкнвхгви //
MModio югоониэ ээа жгээ 'эпх^ iaefcredaeog //
.' (о доцэ qsuoo)vsi T°°4 ox:j.pq.s
•jurtiedsuo ХЕИ6 мея вэ<ьэвш*&ьер«ээ^ //
иоаниэ ипннгеЕхЛ мгээ 'эшг^ jJBEtoedaeog //
:OTxqnd
:_ //
ииЬеяифианэНи ииПянЛф //
_ //
.' {^ndux 9Uiee^sT)ue3[oj;uoT^6nq.3unjo: :иэх°Ли°Т^,впэ,эипдэ
:OTxqnd
//
■ dojksXdioett/doiMXdaiOHOji //
//
}
иэчою OTxqnd : иэхрхиотзспзэипзэ ssex3
■ шйгеЛлмнЛы ИИЕИЭЭХЭ1Г вэчлешкэ шЛИЛр „=<„ etfoda ихо&бэ оль //
q.E4^ ' эзозедэцч. 'роо^влэрип eq р-цпоцв //
'ЛИив а чдэми .ьэЛНэиэ Лнолеоц 'mattsdauo и шЛгеЛлмнЛи //
аохене xnpceir аоиоакиэ ей ако&эоэ ииЬвЛймнЛы миэомэ^ //
///////////////////////////////////////////////////////////////////
IIII////////////////////////////////////////////////////////lllllll
'■{
i (jeTjT^uopi^uaumioouTbeas ¥двчо ^suoo
-nxodbo Лэном члрмои //
илоокиИохдоэн idsh Оль мел 'joe вэчаршкэ ietrXg //
woIihom 'HMOdio иоаон вхгоаниэ лэн эйноя а шгээ ошь //
одь ' вэлэвявтэхДО edu 'виЗвднэниоя охгеьен лэвЗэвоЗц //
:oTtqnd
:_ //
■ииИвхифиянэКи иийянЛф //
//
'^ndux juresz^ST)иэ^охч-иэшшоэтоао
• BOieXdMdoHiiH //
XBHdPiHBHMOX a SUOTq.BtUITq.UCOeUT'I 'wOfiBdQO РЧИЯЕХ //
esz
wot/o> w/wtfoxow эинэиаейил
254
Глава 8
;-i:
iclass CWordToken : public CToken
J{
\lI Конструктор/декструктор.
//
n jpublic:
'i
i CWordToken::CWordToken(istreamfi input,
// Indicates that an underscore, '_', is
// allowable as the first character
const bool bAllowPreUnderscore - true,
const TokenTextList* pReservedWords = NULL);
//
// Защищенные элементы данных.
//
protected:
// Указатель на список зарезервированных слов.
const TokenTextList* m_pReservedWords;
// Показывает, что уже вызывалась IsReservedWord
// и m_bReservedWord, таким образом, уже установлена.
// Это означает, что при следующем вызове
// IsReservedWord набор слов не нужно просматривать
// заново.
bool m_bReserveWordChecked;
bool m_bReservedWord;
//
// Функции доступа к элементам данных.
//
public:
'i bool IsReservedWord();
.4
и,
// Функции идентификации.
li
j/,
■ipublic:
3 // Проверяет, что вся строка sText является словом.
■ static bool IsA(const string sText,
t // Показывает, что подчеркивание, '__' , допустимо
1 //в качестве первого символа.
const bool bAllowPreUnderscore = true);
// Проверяет, что первая лексема входного потока
// является словом.
static bool IsA(istreamfi input,
// Показывает, что подчеркивание, '_', допустимо
// в качестве первого символа.
const bool bAllowPreUnderscore = true);
};
'хтаннрИ ллнэнэие эпннэЪвйпвб //
//
'q.ndiiT 5ureaj^.ST)u33[Ojipuaq.u3maq.B4S0
:oTxqnd
:_ //
• doiMXdioott/doiMAcLbOHO^ //
//
}
11 iiiiiiiiiiiiuiiiii if iiiiiiiiiniu и urn mi шипит mini
IllllllUlllllfllllllllllllUllflllllllUIIIIIIIIIIIIIIIIIIIIIIIIII
'-{
i (дэт^т^иэр1иох^впит^иоээитг[8 Витд^в ^suoo
'q.nduT 9uresaq.sT) vsi Tooq OT^eqs
:oTxqndl
:_ //
*иикгемифианэИи ииЬмнЛф //!
//
{ .' ()x^s~о*aexiT^uspiuoT^Bnux^uooeuT'is-ш uznq.93 }
() 3taTjT^uapiuox4.enuTq.uoo9UTT:^e9 *децэ ^suod
roTxqndj
__ //
'хпнне^ мв!нэмЭ1ге м еиЛаэок ииТганЛф //
/у]
.'JSTgT^uepiuox^Bnuxq.uooeux'is-ш битд^в
:p9^oe^oadj
//
'XHHHEtf ИЛНЭИЭ1Г6 ЭПННЭ1ПИ)1ГЕ£ //]
//
.'<
JSTjTq.uapiuoTq.'enu'c^uooetiTis *дсцэ ^suoo
'ихо&ьэ »*H3«irotfodii хиЬкжА£и*1ифИ|ЬнэНи //
'яоионниэ do9^H «zaettsdau омиИохроэн 'iraodio KHHeacirotfodn //
И01ГОЯНИО РИЭОМЭ1Г KBlmMXtteiro hit кэаэкияв' члиновяя npoj>h //
'q.tidux 9«reejq.sT) иэ^о^иотч.'впит^иоээит'ю
:oxTqndj
:_ //
•doiMAdibDett/doiMAdiOHOii //
//I
}
lillllllillililllllllllllllillllillffliiiliililllllllliliiUIIUill\
иээ(0£иотчвпитч.иоээит1Э //
uuuiuuiuuuuuuiuuuuuuuuuuuiuuu'uuuuiiuA
Q9Z
wotfox w9Ht?oxoH эинэ1/веби/{
256
Глава 8
и-
J"
//
protected:
const char m_cStatementEndIdentifier;
//
// Функции доступа к элементам данных.
//
public:
const char GetStatementEndldentifier()
{ return m_cStatementEndIdentifier; }
//
// Функции идентификации.
//
public:
static bool IsA(const char c, const char
cStatementEndldentifier);
static bool IsA(istream& input, const char
cStatementEndldentifier);
);
///////////////////////////////////////////////////////////////////
t // COtherToken
///////////////////////////////////////////////////////////////////
class COtherToken : public CToken
{
j/,
, // Конструктор/деструктор.
► , //
public:
I COtherToken::COtherToken(istreamfi input);
it//
j' |// Функции идентификации.
N
ill
! jpublic:
■ • static bool lsA(istream& input);
L Jtfendif // 'defined(TOKENHINCLUDED)
( UPV
ПРИМЕЧАНИЯ
Класс CToken представляет обобщенную лексему. Он является
производным от std::string: тем самым он может вести себя как стандартная строка,
наследуя все ее операции и функции-элементы. Рис. 8.2 показывает
иерархическую структуру для классов CToken, CCommentToken, CEOLCommentToken и
CWhiteSpaceToken.
Управление исходным кодом
257
Рис. 8.2.
Фрагмент иерархии
СТокеп
std::string
СТокеп
♦m_TokenType : int
♦СТокепО
♦~СТокеп()
♦GetTokenTextO
♦SetTokenText()
♦GetType()
♦SetTypeO
♦GetSpecifiedStringt)
♦IsSpecifiedStringO
I
CCommentToken
♦m_sBeginCommentldentifier: string
♦m_sEndCommentldentifier: string
♦ GetBeginCommentldentifierO
♦ GetEndCommentldentlfier()
♦ IsCommentldentifierO
CWhiteSpaceToken
* IsWhiteSpaceTokenf)
CEOLCommentToken
♦ IsEOLCommentTokenO
Кроме трех производных классов, показанных на рис. 8.2, СТокеп имеет
ряд других подклассов, перечисленных в таблице 8.1.
Таблица 8.1. Классы лексем, производные от СТокеп
Имя класса
CEOFToken
CEOLToken
CWhiteSpaceToken
CEOLCommentToken
CCommentToken
CStringToken
CCharacterToken
CNumericToken
Описание
Конец файла
Конец строки
Пробельный символ (пробел или табуляция)
Конец строки комментария
lnline-комментарий (например, /* my comment */)
Строковый литерал (например, "this is a string")
Символьный литерал (например,';')
Любое числовое значение
9 Зек. 1208
258
Глава 8
Имя класса
CPunctuationToken
CWordToken
CLineContinuationToken
CStatementEndToken
Описание
Знак пунктуации (например,'{' или '.')
Любая строка символов, не относящаяся ни к какой другой из
категорий, перечисленных в таблице. Зарезервированные
слова являются специальным случаем CWordToken, но не
имеют собственного класса. Переменные и функции
относятся к объектам CWordToken.
Эта лексема представляет символы, означающие
продолжение текущего оператора на следующую строку
(например, "\").
Обозначает конец оператора. В C++ он представляется
точкой с запятой. В других языках может и не быть такого
символа, а концом оператора считается конец строки.
Если вы внимательно посмотрите на код Token.h, то увидите, что все
методы в СТокеп объявлены виртуальными, что позволяет любым производным
подклассам переопределять эти методы. Кроме того, СТокеп не является
абстрактным классом (т. е. не содержит чисто виртуальных функций), поэтому его
можно использовать даже не определяя каких-либо производных классов. Это
позволяет создавать специальные пользовательские лексемы без
необходимости определения нового подкласса. Тем самым сканер может использовать
лексему для любого языка — даже для языков, не считающих пробельные
символы разделителями лексем.
[ ЗАМЕЧАНИЕ ПРОГРАММИСТА
Каждый класс лексемы идентифицирует свои объекты значением
ETokenType, хранящимся в т_пТуре. Чтобы создать из СТокеп
специальную лексему, присвойте значение элементу т_пТуре, вызвав SetTy-
pe(int m_nTokenType). Чтобы не возникло конфликтов с
существующими типами лексем, значение типа специальной лексемы должно
быть больше eToketTypeCount.
Каждая лексема реализует две группы функций:
♦ Статические функции IsA() определяют, относится ли следующая
лексема к данному типу. Все производные от СТокеп классы определяют
функцию IsA(). Это позволяет сканеру вызывать каждый из классов
лексем и таким образом определить, какой тип лексемы нужно создать и
возвратить анализатору синтаксиса. Например, когда сканер проверяет,
является ли следующая лексема концом файла, он просто вызывает
CEOFToken::IsA(). Специфика лексемы остается локальной для нее.
♦ Вторая группа функций — это конструкторы для каждого класса
лексем. Исключая СТокеп и CEOLToken, никакой из классов лексем не
может быть конструирован без параметра istream.
Приведенный ниже листинг Token.срр показывает реализацию классов
лексем.
Управление исходным кодом
259
// Token.cpp: реализация всех классов лексем.
///////////////////////////////////////////////////////////////////
//
// Локальные включаемые файлы.
|//
«include "Token.h"
//
// Библиотечные включаемые файлы.
//
j#include <istream>
«include <cassert>
«include <string>
«include <vector>
I#include <algorithm>
// —
// Отладочные макросы.
//
«define SAVE_STATE( rdstatein, rdstate ) int rdstatein = rdstate
«define VALIDATE_STATE( rdstatein, rdstate ) \
assert( rdstatein == rdstate )
///////////////////////////////////////////////////////////////////
// CToken
///////////////////////////////////////////////////////////////////
CToken::CToken()
: m_nType(eTokenTypeUnknown)
CToken::CToken(int nType)
: m_nType(nType)
CToken::«CToken()
bool CToken::IsEOF(istream& input)
SAVE_STATE(rdstatein, input.rdstate());
bool bRet;
// Состояние флага eof на входе в функцию,
bool bNotEOFIn = (input.eof() != 0);
// Заглянуть вперед, чтобы установить eof в случае,
// если мы уже в конце файла, но не знаем об этом,
input.peek();
260
Глава 8
Л
г*
bRet = (input.eof() != 0);
I A
// Если input.eof установлен вызовом реек,
// сбросить его. Мы пытаемся не менять состояние
// входного потока внутри функции is<a>.
if( bRet ££ IbNotEOFIn)
ClearEOF(input);
,. "i // Проверить, что input.eof тот же самый,
■ fc // что и на входе в функцию.
Т I assert( bNotEOFIn == (input.eof() != 0) );
■i
«' VALIDATE_STATE (rdstateln, input. rdstate ()) ;
■ л
. i return bRet;
)
bool CToken::IsNotEOF(istreamfi input)
{
* *1 return ! IsEOF (input) ;
)
1 void CToken::ClearEOF(istreamS input)
{
// Выделить бит eof, объединив его по И с текущими флагами,
//и применить исключающее ИЛИ с теми же флагами.
// Бит eof будет сброшен и состояние других флагов
i // н« нарушится.
■| input.clear( (ios::eofbit & input.rdstate()) A input.rdstate() );
v ..bool CToken: :IsSpecifiedString(istream& input,
\ ' const string sSearchFor)
1 ■{
) : SAVE_STATE(rdstateln, input.rdstate());
, j
"j // Итератор для символов, удаляемых из потока.
1- 'j char с;
, \ bool bRet;
' long nlncomingStreamPosition = input.tellg();
* bRet = IsNotEOF(input);
I, ■ // Двигаться вперед по потоку, проверяя каждый
I // символ на совпадение с указанной строкой,
f ■ for (int i = 0; (bRet && (i < sSearchFor.length())
| SS IsNotEOF(input) ); i++ )
{
с = input, get ();
bRet = ( sSearchFor[i] == c);
)
I7
" a input.seekg(nlncomingStreamPosition, ios::beg );
Управление исходным кодом
261
// Удостовериться, что указатель get не сдвинулся,
assert( nlncomingStreamPosition = input.tellgO' );
VALIDATE_STATE(rdstateln, input.rdstate());
return bRet;
bool CToken::IsSpecifiedString(istream& input,
const char cSearchFor)
{
bool bRet = false;
SAVE_STATE(rdstateln, input.rdstate());
// Сначала проверить EOF. Хотя bRet возвратит
// то же самое значение в любом случае, IsNotEOF
// не изменит состояние входного потока.
if( IsNotEOF(input) )
bRet = (input.peek() = cSearchFor);
j VALIDATE_STATE(rdstateln, input.rdstate());
return bRet;
}
%■'=?■
bool CToken::GetSpecifiedString(istream& input, const char cGet)
{
SAVE_STATE(rdstateln, input.rdstate()) ;
bool bRet = false;
if(IsSpecifiedString(input, cGet))
{
input.get();
bRet - true;
}
VALIDATE_STATE(rdstateln, input.rdstate());
return bRet;
)
bool CToken::GetSpecifiedString(istream& input, const string sGet)
SAVE_STATE(rdstateln, input.rdstate());
long nlncomingStreamPosition = input.tellg();
bool bRet;
bRet = IsSpecifiedString(input, sGet);
if (bRet)
input.seekg( nlncomingStreamPosition + sGet.length() );
262
Глава 8
a*, i
щ^' VALIDATE STATE(rdstateln, input.rdstate()) ;
i
\ J
return bRet;
.\iI Проверить, что в строке остались только пробельные символы.
-. ibool CToken: : IsOnlyWhiteSpaceLef t (istreams input)
-{
f \ SAVE STATE(rdstateln, input.rdstate());
1 i
^
J // Итератор для символов, удаляемых из потока.
char с;
bool bNotEOF;
•1
// Сохранить позицию указателя get, чтобы вернуться
// к нему при завершении функции.
long nlncomingStreamPosition ~ input.tellg();
bool bRet = false;
while( (bNotEOF = IsNotEOF(input))
£fi CWhiteSpaceToken::lsA(c = input.get()} );
if(bNotEOF)
// Вернуть в поток последний извлеченный символ.
input.putback(c);
// После пропуска всех пробельных символов
// убедиться, что следующей лексемой является EOL.
bRet = CEOLToken::IsA(input);
// После всех проверок вернуть поток к состоянию
// перед вызовом функции.
input.seekg(nlncomingStreamPosition);
VALIDATE_STATE(rdstateln, input.rdstate());
_ return bRet;
J
***\// Проверяет, что в строке остались только
"*■'" // пробельные символы или комментарии.
\lbool CToken::IsOnlyWhiteSpaceOrCommentsLeft(istreamfi input,
A const char* sBeginCommentldentifier,
const char* sEndCommentidentifier,
const char* sBeginEOLCommentldentifier)
! {
IS SAVE_STATE (rdstateln, input, rdstate ()) ;
bool bRet = true;
V.
* . i
■ 4-e
■
И
// Сохранить позицию указателя get, чтобы вернуться
* //к нему при завершении функции.
iJd long nlncomingStreamPosition = input.tellg();
Управление исходным кодом ___ 263
string();
while(bRet && IsNotEOF(input) &fi !CEOLToken::IsA(input))
<
. if( CWhiteSpaceToken::IsA(input) )
CWhiteSpaceToken::CWhiteSpaceToken(input);
i else if ( CCommentToken::IsA(input, sBeginCommentldentifier) }
4 CCommentToken::CCommentToken(input,
', \ sBeginCommentldentifier, sEndCommentidentifier) ;
! else if ( CEOLCommentToken::IsA{input,
sBeginEOLCommentldentifier) )
CEOLCommentToken::CEOLCommentToken(input,
sBeginEOLCommentldentifier);
else
bRet = false;
? i
}
// После всех проверок вернуть поток к состоянию
// перед вызовом функции.
input.seekg(nlncomingStreamPosition);
* J assert( nlncomingStreamPosition == input.tellg() );
f \ VALIDATE_STATE(rdstateIn, input, rdstate ()) ;
, J return bRet;
■ 1}
' I
* V//////////////////////////////////////////////////////////////////
// EOFTоken
///////////////////////////////////////////////////////////////////
CEOFToken::CEOFToken(istream& input)
// Инициализировать элементы.
: CToken(eTokenTypeEOF)
i
II Итератор для удаляемых из потока символов,
char с;
• i
+ н // Удалить лексему из потока и записать удаленный
// символ в m_sTokenText.
assert( CEOFToken::IsA(input) );
// Заметьте, что следующий вызов установит флаги
// состояния потока fail и eof.
с = input.get();
SetTokenText(c);
}
bool CEOFToken::IsA(istreamu input)
{
return CToken::IsEOF(input);
}
264
Глава 8
\///////////////////////////////////////////////////////////////////
// CEOLToken
///////////////////////////////////////////////////////////////////
[CEOLToken::CEOLToken()
// Инициализировать элементы.
: CToken(eTokenTypeEOL)
(
SetTokenText(EOL_CHARACTER);
}
[CEOLToken::CEOLToken(istreamfi input)
// Инициализировать элементы.
: CToken(eTokenTypeEOL)
{
// Итератор для удаляемых из потока символов,
char с;
с - input.get();
assert( CEOLToken::IsA(с) );
SetTokenText(с);
bool CEOLToken::IsA(const char c)
return ( (c = EOL_CHARACTER) >;
bool CEOLToken::IsA(istream& input)
return ( IsSpecifiedString(input/ EOL_CHARACTER) );
///////////////////////////////////////////////////////////////////
// CWhiteSpaceToken
///////////////////////////////////////////////////////////////////
[CWhiteSpaceToken::CWhiteSpaceToken(istreamfi input)
// Инициализировать элементы.
: CToken(eTokenTypeWhiteSpace)
{
SAVE_STATE(rdstateln, input.rdstate()) ;
// Итератор для удаляемых из потока символов,
char с;
bool bNotEOF;
string sTokenText;
// Удалить из потока пробельные символы и записать их в
// m_sTokenText.
while ( (bNotEOF = IsNotEOF(input)) SS IsA(c = input.get()))
{
Управление исходным кодом
265
sTokenText +- с;
}
if(bNotEOF)
// Возвратить в поток последний символ,
input.putback(c);
assert( CWhiteSpaceToken::IsA(sTokenText) );
SetTokenText(sTokenText);
VALIDATE_STATE(rdstateln, input.rdstate()) ;
:i
l .bool CWhiteSpaceToken::IsA(const char c)
' l{
j return( ICEOLToken::IsA(c) &&
\ ((isspace(c)!= 0)|| (c==0))
. s );
M
. Jbool CWhiteSpaceToken::IsA(const string sText)
j ' bool bRet = !sText.empty();
t 1 string::const_iterator c;
ь 1
* 1
a 3 for(c = sText.beginO; ( bRet && (c !- sText.end()) ); С++)
bRet - IeA(*c);
j return bRet;
:>
bool CWhiteSpaceToken::l8A(istream& input)
{
if( IsNotEOF(input) )
return CWhiteSpaceToken::IsA(input.peek());
else
return false;
}
///////////////////////////////////////////////////////////////////
// CStringToken
///////////////////////////////////////////////////////////////////
CStringToken::CStringToken(istreamS input,
const char cStringldentifier,
const char cEscapeCharacter)
// Инициализировать элементы.
: CToken(eTokenTypeString),
m_cStringIdentifier(cStringldentifier),
m_cEscapeCharacter(cEscapeCharacter)
{
SAVE_STATE(rdstateln, input.rdstate());
266
Глава 8
■*i *,
; // Итератор для удаляемых из потока символов.
' char с;
boo! bStringEnded = false;
-.' , string sTokenText;
«*J assert(IsSpecifiedString(input, cStringldentifier));
• ,4 GetSpecifiedString(input, cStringldentifier);
i
sTokenText += cStringldentifier;
A
if -j while( IsNotEOF(input) &&
*j ! (bStringEnded = IsSpecifiedString(input, cStringldentifier)) }
-*■ i
~1 с - input.get() ;
mfl if ((c == cEscapeCharacter) && IsNotEOF(input) )
sTokenText += c;
'* с = input, get ();
'•■■ j }
.1 sTokenText += c;
.1- // Нельзя гарантировать, что строка закончится до конца файла.
] .; // Если конец найден, добавить его идентификатор к лексеме.
'_-]< if ( GetSpecifiedString (input, cStringldentif ier) )
, ■■**■ // Добавить Stringldentifier к строке лексемы.
■*\ sTokenText += cStringldentif ier ;
■ :*
i SetTokenText(sTokenText);
ft\ VALIDATE_STATE(rdstateIn, input.rdstate());
i)
;- bool CStringToken::IsA(istream& input,
<< const char cStringldentifier,
■t-2 const char cEscapeCharacter)
$Ji SAVE STATE(rdstateln, input.rdstate());
' I bool bRet;
t'^\ long nlncomingStreamPosition = input.tellg();
■ i
ri, 1 // Проверить, является ли следующий символ лексемой строки.
* .■ bRet = IsSpecifiedString(input, cStringldentifier);
;*'| // Проверить, что предыдущий символ не являлся escape-символом.
* '■ // Это необходимо, потому что нам может встретиться, например,
': ] // символ Л"' ■
\f.\ if (bRet)
7 : {
I ,j // Исследовать предыдущий символ.
«MjaJ input. seekg(-l, ios::cur);
Управление исходным кодом 267
bRet - !IsSpecifiedString(input, cEscapeCharacter);
// Вернуться к начальной позиции.
input.seekg(nIncomingStreamPosition, ios::beg);
assert(nlncoraingStreamPosition == input.tellg());
}
VALIDATE_STATE(rdstateln, input.rdstate());■
return bRet;
}
r ,
J
1 \iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii паннаmiin inn 11
¥ \lI CCharacterToken
j \iiiiiiiiiiiiuiiiiiiiii(iiiiiiiiiiiiiititiiiiiiitiiiiii tin muni
iCCharacterToken::CCharacterToken(istreamfi input,
, j const char cCharacterldentifier,
| const char cEscapeCharacter)
!: CStringToken(input, cCharacterldentifier, cEscapeCharacter)
{
m nType = eTokenTypeCharacter;
)
llillllllllllliilllltltltlliiiiliititiiiiliiilillliltiiiiliiiiliili
It CNumericToken
IlltttlllllltlllllllllllltllflllllllllllllllUlltlllltltllltllltltl
CNumericToken::CNumericToken(istreamfi input)
: CToken(eTokenTypeNumeric)
{
SAVE_STATE(rdstateln, input.rdstate());
// Строка всех символов, которые могут встречаться
// в числах.
static string sPossibleNumericCharacters
= POSSIBLE_NUMERIC_CHARACTERS;
// Итератор для удаляемых из потока символов.
char с;
bool bNotEOF;
string sTokenText;
// Флаг продолжающегося извлечения символов,
bool bCont;
assert(IsNotEOF(input));
// Проверить, что первый символ является цифрой.
с = input.get();
assert(isdigit(с) || (с = '.'));
// Теперь мы уверены, что имеем дело с каким-то числом.
268
Глава 8
а bCont = true-
while (bCont)
{
sTokenText += с;
3 j bCont = (bNotEOF = IsNotEOF(input) ) ;
bCont =
// Мы не достигли еще конца файла И
ь - IsNotEOF(input) ££
// Символ является цифрой ИЛИ
(isdigit(c = input.get()) != 0) ||
// Символ, возможно, является цифровым символом ИЛИ
(sPossibleNumericCharacters.find(c)
!= string::npos) ||
// Символ -- '+' или '-' и далее идут цифры.
( ((с = ' + ') || (С = '-'))
&& IsNotEOF(input)
&£ (isdigit((input.peek()))!=0)
)
);
, \
r
I
1
)
if(bNotEOF)
// Возвратить в поток последний символ.
input.putback(c);
SetTokenText(sTokenText);
- :,
i1 VALIDATE_STATE(rdstateIn, input.rdstate()) ;
}
! ']// Проверить, что следующий символ — цифра
* \1I ют точка, '.', за которой идет цифра.
r 'bool CNumericToken::IsA( istreamfi input)
"{
SAVE_STATE(rdstateln, input.rdstate());
// Итератор для удаляемых из потока символов.
char с;
bool bRet = false;
(- i
if (IsEOF(input))
bRet = false;
else if( isdigit(input.peek()) != 0 )
bRet = true;
else if(input.peek() =='.')
<
// Проверить следующий символ.
с = input.get();
if ( IsNotEOF(input) )
bRet = ( isdigit(input.peek()) != 0 );
// Возвратить в поток последний символ.
Управление исходным кодом 269
■ | input.putback(c);
М »
i
I i VALIDATE_STATE(rdstateln, input.rdstate()) ;
i
return bRet;
)
///////////////////////////////////////////////////////////////////
// CCommentToken
///////////////////////////////////////////////////////////////////
t- CCommentToken::CCommentToken(istreamfi input,
I const string sBeginCommentldentifier,
const string sEndCommentldentifier)
// Initialize member variables.
: CToken(eTokenTypeComment),
m_sBeginCommentIdentifier(sBeginCommentldentifier},
m__sEndCommentIdentifier(sEndCommentldentifier)
<
SAVE_STATE(rdstateln, input.rdstate());
П
i
t
bool bNotEOF;
string sTokenText;
assert ( !sBeginCommentldentifier.empty() );
assert ( !sEndCommentldentifier.empty() );
// Проверить, что далее в потоке идет текст
// sBeginCommentldentifier.
assert( IsSpecifiedString(input, sBeginCommentldentifier));
GetSpecifiedString(input, sBeginCommentldentifier);
sTokenText += sBeginCommentldentifier;
while( (bNotEOF - IsNotEOF(input))
&£ !IsSpecifiedString(input, sEndCommentldentifier) )
<
sTokenText += input.get() ,*
}
// Возможно, что комментарий не закончился до конца
// файла. Если комментарий закончен,
// добавить к лексеме EndCommentldentifier.
if (bNotEOF)
i
assert( IsSpecifiedString(input, sEndCommentldentifier) );
GetSpecifiedString(input, sEndCommentldentifier);
sTokenText += sEndCommentldentifier;
}
SetTokenText(sTokenText);
270
Глава 8
Я Н
1 *
VALIDATE__STATE (rdstateln, input.rdstate ()) ;
}
bool CCoinmentToken: :IsA( istreams input,
const string sBeginCommentldentifier,
const string sEndCommentldentifier,
const bool bCheckEnd )
{
SAVE_STATE(rdstateln, input.rdstate()) ;
bool bRet = false;
long nlncomingStreamPosition;
assert (!sBeginCommentldentifier.empty() ) ;
if (!bCheckEnd || (sEndCommentldentifier.length() = 0))
bRet ~ IsSpecifiedString(input, sBeginCommentldentifier);
else
{
nlncomingStreamPosition - input.tellg();
bRet = GetSpecifiedString(±nput, sBeginCommentldentifier);
if (bRet)
while(IsNotEOF(input) &£
•(bRet = IsSpecifiedString(input, sEndCommentldentifier)) )
{
input.get();
}
// Возвратиться к начальной позиции входного потока,
input.seekg(nlncomingStreamPosition, ios::beg);
}
VALIDATE_STATE(rdstateln, input.rdstate());
return bRet;
///////////////////////////////////////////////////////////////////
// CEOLCommentToken
///////////////////////////////////////////////////////////////////
CEOLCommentToken::CEOLCommentToken(istreams input,
const char* sBeginCommentldentifier)
// Инициализировать элементы.
: CCoinmentToken(input, sBeginCommentldentifier, "\n")
? j // CCoinmentToken устанавливает тип ETokenTypeComment,
£ i // поэтому нужно присвоить ему eTokenTypeEOLComment.
. ..] ra_nType - eTokenTypeEOLComment;
I *
, J // Удалить конечный '\n'. Для EOLComments
Lb, // конец строки должен передаваться, лексемой EOL,
Управление исходным кодом 271
ВЯ // а не как часть самого комментария.
SetTokenText(GetTokenText().substr(0, length()-1));
)
bool CEOLCommentToken::IsA( istreamfi input,
const char* sBeginCommentldentifier)
{
return CCommentToken::IsA(input, sBeginCommentldentifier, "\n");
)
t
L ■
i
i
г
r
liiuuuuuuiuuuuuuuuuuuuuumuumuumuuu
// CFunctuationToken
///////////////////////////////////////////////////////////////////
CPunctuationToken::CPunctuationToken(istream& input)
// Инициализировать элементы.
: CToken(eTokenTypePunctuation)
(
SAVE_STATE(rdstateIn, input.rdstate());
// Итератор для удаляемых из потока символов,
char с;
// Удалить лексему из потока и записать удаленный
// символ в m_sTokenText.
if(IsNotEOF(input))
с = input, get О;
assert( CPunctuationToken::IsA(c) );
SetTokenText(c);
VALIDATE_STATE(rdstateIn, input.rdstate());
}
bool CPunctuationToken::IsA(const char c)
{
return (ispunct(c) != 0);
)
// Проверить, что все симаолы в строке являются знаками
// пунктуации.
bool CPunctuationToken::IsA(const string sText)
<
bool bRet = !sText.empty();
string::const_iterator c;
for( с ■ sText.begin() ; ( bRet && (c != sText.endO) ) ; C++)
bRet = (CPunctuationToken::IsA(*c)) ;
return bRet;
}
// Все, что должна проверить функция — это первый символ.
272
Глава 8
!// Даже если только он один является допустимым символом
// пунктуации, лексема является знаком пунктуации,
■(bool CPunctuationToken: :IsA(istream& input)
i if(IsNotEOF(input))
return CPunctuationToken::IsA(input.peek());
else
return false;
}
f ■///////////////////////////////////////////////////////////////////
i// CWordToken
* }///////////////////////////////////////////////////////////////////
J// Следующий конструктор CWordToken создает
!// лексему из всех символов, отграниченных
' щ\// "пробельными" символами. Последние включают в себя
."\f/ знаки пунктуации и операций.
5 J// В будущем конструктор будет более специализированным.
* jCWordToken::CWordToken(istrearafi input,
* j const bool bAllowPreUnderscore,
const TokenTextList* pReservedWords)
// Инициализировать элементы.
: CToken(eTokenTypeWord),
m_bReserveWordChecked(false),
4 -. J m_jpReservedWords (pReservedWords)
1<
SAVE_STATE (rdstateln, input, rdstate ()) ;
!
// Итератор для удаляемых из потока символов.
char с;
bool bNotEOF;
string sTokenText;
г I
I j assert(IsNotEOF(input));
I j
' -| // Получить первый символ слова.
I . с = input.peek();
■ *i // Проверить, что символ является допустимым
// первым символом слова.
И
j if(bAllowPreUnderscore)
и i assert(isalpha (c));
I assert(isalpha(c));
*
It Удалить лексему из потока и записать
// удаленные символы в m__sTokenText.
while ((bNotEOF = IsNotEOF (input))
b && isalnum (c = input.get()) != 0)
' : <
sTokenText += c;
!_J )'
Управление исходным кодом 273
* if(bNotEOF)
// Вернуть лишний извлеченный символ,
input.putback(с);
assert( CWordToken::IsA(sTokenText, bAllowPreUnderscore) );
SetTokenText(sTokenText);
VALIDATE_STATE(rdstateln, input.rdstate());
f
If
h-
u
Ъ
Г,
)
bool CWordToken::IsReservedWord()
{
if (m_bReserveWordChecked)
return m_bReservedWord;
else
m_j>ReservedWords->find(GetTokenText());
mJbReserveWordChecked = true;
return m_bReservedWord;
}
bool CWordToken::IsA(const string sText,
const bool bAllowPreUnderscore)
{
bool bRet = !sText.empty();
string::const_iterator c;
if ( bRet &fi bAllowPreUnderscore )
bRet = (isalpha_(sTextCO]) != 0);
for(c = sText.begin() ; bRet, с != sText.endO ; С++)
bRet = (isalnum_(*c) != 0);
return bRet;
>
// Все, что должка проверить функция — это первый символ.
// Даже если только он один является допустимым символом
// слова, лексема является словом,
bool CWordToken::IsA(istream& input,
const bool bAllowPreUnderscore )
{
bool bRet = IsNotEOF(input);
if (bRet)
{
if ( bAllowPreUnderscore )
bRet = (isalpha_(input.peek()) != 0);
else
bRet = (isalnum_( input.peek()) != 0);
}
return bRet;
>
' (puaRU9Uie^e^secWxue3(ojie)ue3[OXD : P"""1
(дэтЗт^иэр1ри5П.иэша^Е^з^> ^«4° q.suoo
'^nduT 9me3J^ST)uai[Oxpu3^ueme^E^SD' :uao[°;Lpumuauie4e3SD
///////////////////////////////////////////////////////////////////
иэ^охридоиэшэзвззэ //
///////////////////////////////////////////////////////////////////
{
.' (Oe^H^spj-^ndux 'uie^E^spj)aj,YiS~"aiVai'IYA
.' (uOT^TSO^tueea^sbuTiiioouiu) б^ээв ■ qnduT '
|
.' (^nduT)^jaieDBdseqT4M^TUOSi 95 ^a^q = Ч&&Ч. |
.' (asTj-c^uepiuOT^Bnux^uoo^uT'Is '}nduT)6uTJt:*sp©T3TDeds4eo = 3®H4 i *
t
• ()6TTsWidtiT = иот^твоашвэа^Ббитшоощи Ёиот ' *
-ииймнЛф иинэлкЗэеее ndu Лнэн я вэчдЛнЗэв hituoh // I *
пи продл 'q,afi ниэллеехЛ ocftmeou чвинвйхоэ // | *
: *
■^•ач т°°я i 1
.'(O^eq-spj-^nduT 'ихеавэврд) 3JlVIS~aAYS J *
>! ;
(jeTj-p^uapiuOT^BnuT^uooeuTis 6uxa^s ^suoo (
'q.nduT 5ureaaq,ST) vsi: :иа^охиот^епит^иоээит'Ю Т°°Я
{
.' (Оэ^в^врд-^nduT 'uie^B^spaJaiiYiS ЗЫКПТСЛ
.' (автзт^иэр1иот^впит^иоээит18) зхэхиэдохзэг
.' ( (^nduT)^jeiaoBdsa^T4M^TUosi )^jcasse
'пионнио 3raH*nre9odu омчиоа чаигойэо sarodiLO а ojjh '^LMdaaodu //
.' (^3Tjxq.uapiuoTq.cnuT^uooauTis '^ndirr) EUT^spaTJT^ads^ao
.'(()a^B^sp3f^ndux /uia^B^spa)aiVXS~aAYS
}
(39T3:'Tq.uapiuOTq.BnuTq.uooauTT:s) агэт^тзиэрцгот^впитч.иоээитче ш
' (uoT^FnuTquooauxiBdAxua^oxa) иэ^охэ :
' №ЬНЭИЭ1Гв ЧйВао£ивИ1ГВИ11ИНИ / /
(
jcaTjT^uapiuoT^enuT^uooauTis *звт{э ^suod
-XxodiDO cuanaXtfairo вн Himancirottodu exBHeudu //
1Гоанио «xjiBttoddii оииНохроэн 'ихо&ьэ Bimsxirotfodii //
Хиэохэи хвх Хкээхэи otXtaouttfairo н&ваоЗиктфи.ьнэЬ'и ngoiLh //
' qnduT 9ureaocq.sT) иээсохиот^епит^иооаитчэ: : иэа(охиотч.впит4иоэвит1Э
///////////////////////////////////////////////////////////////////
иээ(01иот^впитэиоээит1э //
,1
в GBBt/J
PIZ
Управление исходным кодом 275
г
m cStatementEndldentifier(cStatementEndldentifier)
// Проверить, что следующим в потоке идет
// cStatementEndldentifier.
assert( IsSpecifiedString(input, cStatementEndldentifier) );
// Извлечь cStatementEndldentifier из потока.
GetSpecifiedString(input, cStatementEndldentifier);
SetTokenText(cStatementEndldentifier);
i
bool CStatementEndToken::IsA(const char c,
const char cStatementEndldentifier)
{
return (c = cStatementEndldentifier);
)
bool CStatementEndToken::IsA(istream& input,
const char cStatementEndldentifier)
i
return IsSpecifiedString(input, cStatementEndldentifier);
}
]
i \///////////////////////////////////////////////////////////////////
| }// COtherToken
; ,///////////////////////////////////////////////////////////////////
■ J// Лексема, которая не подходит под определение
[ '// ни одной из предыдущих.
r [COtherToken::COtherToken(istreamfi input)
u ' // Инициализировать элементы.
1 1 : CToken(eTokenTypeOther)
I SAVE_STATE(rdstateln, input.rdstate());
i '
// Итератор для удаляемых из потока символов.
char с;
bool bNotEOF;
. 1
j string sTokenText;
I i
// Получить первый символ слова.
// Удалить лексему из потока и записать
// удаленные символы в m_sTokenText.
while ( (bNotEOF = IsNotEOF(input)) && IsA(input))
(
с = input.get();
sTokenText += c;
}
1.1 if (bNotEOF)
276
Глава 8
1
// Возвратить лишний извлеченный символ.
input.putback(c);
SetTokenText(sTokenText);
VALIDATE_STATE(rdstateln, input.rdstate());
>
// Лексема, не являющаяся никакой иэ определенных
// выше. Так как многие функции, проверяющие
// принадлежность к конкретному типу лексем, требуют
// дополнительных параметров, эту
1 // принадлежность здесь проверить нельзя,
bool COtherToken::IsA(istream& input)
{
SAVEJSTATE(rdstateln, input.rdstate());
L
■ j bool bRet;
li
):
it
I
b-.
i *
i
i*i
if (CEOFToken::IsA(input)
|| CEOLToken::ISA(input)
|| CWhiteSpaceToken::IsA(input)
)
bRet = false;
else
bRet = true;
VALIDATE_STATE(rdstateln, input.rdstate());
return bRet;
I ПР1/
ПРИМЕЧАНИЯ
Чтобы лучше понять работу классов лексем, давайте рассмотрим CNume-
ricToken и CCommentToken.
Функция CNumericToken::IsA() проверяет следующие два символа из
входного потока. Если первый из них — цифра, функция расценивает лексему как
число и возвращает true. Если первый символ является точкой (.), то
проверяется следующий символ — не цифра ли это.
Как и большинство функций IsA(), CNumericToken::IsA() не проверяет,
является ли вся лексема корректным числом. Например, строка "0.9.9ее8.е",
появившаяся во входном потоке, будет считаться числом, хотя компилятор
сообщит об ошибке в этой лексеме. Вообще функции IsA() лишь дают наиболее
подходящую из возможных классификаций. Они не могут с уверенностью
констатировать, что синтаксический'элемент принадлежит к допустимому типу.
Строгая регистрация ошибок, как правило, оставляется компилятору. Хотя
идентификация лексемы может и не давать точного результата, функции
написаны так, что если IsA() возвращает true, можно успешно конструировать
лексему данного типа. Например, если CNumericToken::IsA() вернула true, то
Управление исходным кодом
277
оператор CToken token = CNumericToken::CNumericToken(input) успешно
создаст CNumericToken.
Заметьте, что все функции IsA() — статические. Это позволяет обращаться
к ним из других классов, не ссылаясь на какой-либо уже созданный
представитель.
Одним из «странных» свойств istream является следующее. После вызова
реек() или get() в конце файла istream::eof() возвращает true. К несчастью,
однако, если вы прочитали из потока символ eof, то не сможете уже
переместиться в какую-то другую позицию в файле и потому вызов istream::seekg() не
даст желаемого результата. Если символом, который должен быть прочитан из
потока, является eof, но никакой функции, действительно извлекающей
значение символа, вызвано не было, то istream::eof() возвращает false. Это
типичная «уловка-22»: чтобы определить, что вы в конце файла, нужно прочитать
символ eof; но сделав это, вы уже не в состоянии перемещаться по файлу.
Функции CToken::IsEOF() и ее двойник CToken::IsNotEOF() написаны для
того, чтобы обеспечить безопасную проверку конца файла. Они определяют,
находится ли программа в конце файла, и сбрасывают istream::eof обратно в false,
если это действительно так. Другими словами, функция IsEOF() вызывает
реек(). Если флаг eof при этом переключается в состояние true, то она
восстанавливает его значение и устанавливает указатель на последний символ потока.
Конструкторы лексем похожи на функции IsA() за исключением того, что
они предполагают, что следующая лексема принадлежит к запрошенному
классу, и удаляют ее из потока. Большинство конструкторов констатируют
ошибку, если по какой-то причине оказывается, что лексема не принадлежит к
их классу. Давайте разберем детали конструктора лексемы на примере ССот-
men tToken: :CComm en tToken().
В отличие от большинства других конструкторов лексем CCommentToken()
принимает два дополнительных параметра. Эти две строки идентифицируют
начало и конец комментария. Если не ставить задачу написания классов,
способных работать с различными языками, то можно было бы жестко
закодировать для них значения, требуемые C++ ("/*" и "*/"). После проверки того, что
sBeginCommentldentifier не пуст, функция удаляет из потока все символы,
пока не будет достигнут конец комментария.
Чтобы удалить из потока специфические символы, вызывается функция
CToken;:GetSpecifiedString(istream& input, const string sGet)
Она проверяет, что в потоке действительно находятся символы,
составляющие строку sGet, и перемещает указатель istream (т. е. istream::tellg(» на
символ, непосредственно следующий за sGet.
Теперь, ознакомившись с классами лексем, рассмотрим класс сканера, их
использующий.
Класс сканера CScanner
Класс CScanner определяет принадлежность следующей лексемы в istream,
создает ее представитель и возвращает ее синтаксическому анализатору. Класс
реализован как подкласс istream.
278
Глава 8
Код
Ниже приведен листинг определения класса CScanner.
t
г
// Scanner.h: интерфейс класса CScanner.
///////////////////////////////////////////////////////////////////
Hf !defined(SCANNER_H_INCLUDED)
^define SCANNER_H_INCLUDED
\ц
-*11 Идиосинкразии Microsoft.
//
#if _MSC_VER > 1000
// отключить предупреждение С4786: symbol greater than 255 character,
// okay to ignore (MSVC)
#pragma warning(disable: 4786)
#pragma once
#endif // MSC VER > 1000
//
// Локальные включаемые файлы.
//
#include "token.h"
//
// Библиотечные включаемые файлы.
//
ftinclude <iostream>
^include <fstream>
#include <string>
using namespace std;
class CScanner : public ifstream
{
//
// Конструктор/деструктор
//
public:
CScanner();
CScanner(const char* szName);
virtual -CScanner();
// _._
// Защищенные элементы данных.
.1 „
J protected:
// Отслеживает число лексем каждого типа.
// m_TokenCount[eTokenTypeCount] хранит
// общее число сканированных лексем,
int m_TokenCount[eTokenTypeCount+l];
// Символ, идентифицирующий строку.
Управление исходным кодом 279
£ 1 char m_cStringIdentifier;
// Символ, идентифицирующий представление символа,
// такое, как или 'с' or '\n'
char m_cCharacterIdentifier;
// Escape-символ внутри строки,
Г 1 // например, 'V перед символом двойной кавычки
// в C++
char m cEscapeCharacter;
\
^
г*
**
// Указывает начало комментария
string m_sBeginCommentIdentifier;
// Указывает конец комментария
* I string m_sEndCommentIdentifier;
ы
// Указывает начало комментария, оканчивающегося EOL
string m_sEOLCommentIdentifier;
// Указывает на допустимость подчеркиваний
// в начале слова.
bool mJbAllowUnderscore;
// Указьшает символ, означающий продолжение строки.
string m_sLineContinuationIdentifier;
// Используется для указания конца оператора.
char m_cStatementEndXdentifier;
// Хранит список зарезервированных слов языка.
TokenTextList m_ReservedWords;
//
// Функции доступа к элементам данных
//
{ public:
// Возвратить счетчик обработанных лексем
// определенного типа. GetTokenCount(eTokenTypeCount)
// возвращает общее число обработанных лексем.
long GetTokenCount(int nTokenType)
{
return m_TokenCount[nTokenType];
}
// Сбросить счетчики лексем.
void Reset()
{
for(int i = 0; i < eTokenTypeCount+1; i++)
m_TokenCount[i] = 0;
// Инициализировать номер строки.
m_TokenCount[eTokenTypeEOL] = 1;
}
i
Глава
// Установки для строк.
const char GetStringldentifier{)
return m_cStringIdentifier;
void SetStringXdentifier( const char cStringldentifier,
const char sEscapeCharacter )
m_cStringIdentifier = cStringldentifier;
m_cEscapeCharacter = sEscapeCharacter; ,
// Установки для строк-символов,
const char GetCharасterIdentifier()
return m_cCharacterIdentifier;
void SetCharacterldentifier( const char cCharacterldentifier,
const char sEscapeCharacter )
m_cCharacterIdentifier = cCharacterldentifier;
m_cEscapeCharacter = sEscapeCharacter;
const char GetEscapeCharacter()
return m_cEscapeCharacter;
void SetEscapeCharacter( const char cEscapeCharacter )
m_cEscapeCharacter ~ cEscapeCharacter;
// Установки для комментариев.
const char* GetBeginCommentldentifier()
return m_sBeginCommentIdentifier. c_str();
const char* GetEndCommentldentifier()
return m_sEndCommentIdentifier.c_str();
void SetCoromentldentifiers(
const char* sBeginldentifier, const char* sEndldentifier)
m_sBeginCommentIdentifier = sBeginldentifier;
m_sEndCommentIdentifier = sEndldentifier;
// Установки для EOL-комментариев,
const char* GetEOLCommentldentifier()
return m_sEOLCommentIdentifier.c_str() ;
void SetEOLCommentldentifier( const char* sEOLCononentldentifier )
Управление исходным кодом
281
I \
с !
ц ■
m_sEOLCommentIdentifier = sEOLCommentldentifier;
// Установки для слов,
bool GetAllowUnderscoreО
return m_bAllowUnderscore;
void SetAllowOnderscore( const bool bAllowUnderscore )
m_bAllowUnderscore = bAllowUnderscore;
// Продолжение строки.
const char* GetLineContinuationldentifier()
return m_sLineContinuation!dentifier.c_str();
void SetLineContinuationldentifier(const char* nNewIdentifier)
m_sLineContinuation!dentifier = nNewIdentifier;
const char GetStatementEndldentifier()
return m_cStatementEndIdentifier;
void SetStatententEndldentifier (
const char cStatementEndldentifier)
m_cStatexnentEndIdentifier = cStatementEndldentifier;
bool IsReservedWord(const string sTokenText)
return (m_ReservedWords.find(sTokenText)
!= m_ReservedWords.end());
TokenTextList* GetReservedWords()
return &m_ReservedWords;
bool IsOnlyWhiteSpaceLeft()
return CToken::IsOnlyWhiteSpaceLeft(*this);
bool IsOnlyWhiteSpaceOrCommentsLeft()
return CToken::IsOnlyWhiteSpaceOrCommentsLeft(
♦this, GetBeginCommentldentifier(),
GetEndCommentldentifier(),
GetEOLCommentldentifier()
282
Глава 8
);
)
//
// Открытые функции.
//
public:
// Определить тип следующей лексемы,
virtual int PeekTokenType();
// Возвратить следующую лексему из входного потока
// Если лексемы нет, возвратить false.
virtual CToken GetToken();
);
#endif // 'defined(SCANNER H INCLUDED)
( ПРк
ПРИМЕЧАНИЯ
Идея определения класса CScanner состоит в том, чтобы придать ему
свойства istream, но ориентировать его не на отдельные символы, а на лексемы.
Так как главной задачей сканера является извлечение из потока очередной
лексемы, важнейшей его функцией оказывается GetToken(). Анализатор
синтаксиса снова и снова вызывает CScanner::GetToken(), двигаясь таким
образом вдоль всего файла.
В сущности сканер дает возможность анализатору синтаксиса работать не с
символами, а с целыми лексемами. Тем самым анализатор уже оперирует
блоками данных большего размера. Человеческие языки устроены точно так же.
Хотя можно произносить слова по буквам, это было бы нерационально и
крайне неэффективно. Аналогично сканер составляет из символов «слова»,
которые анализатор затем интерпретирует.
Вы можете видеть реализацию функции GetNextToken() и всех остальных
элементов сканера в нижеприведенном листинге.
Код
Следующий листинг показывает код класса CScanner.
// Scanner.cpp: реализация класса CScanner.
■ ■:[/,
:!// Локальные включаемые файлы.
••■)//
- т!#include "Scanner.h"
Я
//
Управление исходным кодом 283
t
г*:
tJ
J1
4.
I
t
И
Г
S
t
ч*
ч
// Библиотечные включаемые файлы.
// — —
#include <cassert>
UUUUUUIUUUUUUIUUUUUUUUUUUUUUIUUUUUU
U Конструктор/деструктор.
uuuuuuuuuuuuuuuuuu uiiii urn uu uu mi u uu u
CScanner::CScanner()
// Инициализация всех элементов данных.
: n^cStringldentifierCV") ,
nt_cCharacterIdentifier (NULL) ,
m_cEscapeCharacter(NULL),
m_bAllowUnderscore(true),
m_cStatementEndIdentifier(NULL),
ifstream()
<
Reset();
)
CScanner::CScanner(const char* szName)
// Инициализация всех элементов данных ч
: m_cStringIdentifier('\"*),
m_cCharacterIdentifier(NULL),
m_cEscapeCharacter(NULL),
m_bAllowUnderscore(true),
m_cStatementEndIdentifier(NULL),
if stream (szName)
<
Reset();
}
CScanner::-CScanner()
{
)
// Идентифицировать следующую лексему в ra_input.
// Лексемы определяются именно в таком порядке,
// так как иначе некоторые лексемы могли бы
// идентифицироваться неверно. Например,
// если бы eTokenTypeOtber проверялась
// раньше, чем CWordToken, мы никогда бы не смогли
// распознать лексемы-слова. Кроме того, проверка
// знака пунктуации перед проверкой слова
// препятствовала бы распознаванию слов,
// начинающихся с подчеркивания (если таковые допустимы).
int CScanner::PeekTokenType()
{
int nType;
if ( CWhiteSpaceToken::IsA(*this) )
return eTokenTypeWhiteSpace;
else if ( CWordToken::IsA(*this, GetAllowUnderscore()) )
nType = eTokenTypeWord;
284
Глава 8
V
и
\ i
else if ( CEOLToken::IsA(*this) )
{
nType = eTokenTypeEOL;
}
else if ( CCommentToken::IsA(*this,
GetBeginCommentldentifier(), GetEndCommentldentifier(}) )
{
nType - eTokenTypeComment;
}
else if ( CEOLCommentToken: :IsA(*this/
GetEOLCommentldentifier()) )
{
nType - eTokenTypeEOLComment;
}
else if ( CStringToken::IsA(*this/
GetStringldentifierO, GetEscapeCharacter()) )
{
nType = eTokenTypeString;
}
else if ( CCharacterToken::IsA(*this,
GetCharacterldentifier(), GetEscapeCharacter()) )
{
nType = eTokenTypeCharacter;
}
else if ( CNumericToken::IsA(*this) )
nType = eTokenTypeNumeric;
else if ( CStatementEndToken::IsA(*this/
GetStatementEndldentifier()) )
{
nType = eTokenTypeStatementEnd;
}
else if ( CLineContinuationToken::IsA(*this,
GetLineContinuationldentifier()) )
{
nType = eTokenTypeLineContinuation;
>
else if ( CPunctuationToken::IsA(*this) )
nType - eTokenTypePunctuation;
else if ( CEOFToken::IsA(*this) )
nType = eTokenTypeEOF;
i I else
'•_-1 nType = eTokenTypeOther;
L
*■
■
Управление исходным кодом
285
return nType;
i>
// Возвратить следующую лексему, находящуюся
//в m_input.
, CToken CScanner::GetToken()
{
CToken token;
switch (PeekTokenType())
Г (
t' 4 case eTokenTypeWhiteSpace:
token = CWhiteSpaceToken(*this);
break;
J case eTokenTypeWord:
ч token = CWordToken(*this, GetAllowUnderscore() ,
i GetReservedWords());
s break;
case eTokenTypeStatementEnd:
token = CStatementEndToken(*this,
■GetStatementEndldentifier());
break;
* i
case eTokenTypePunctuation:
token = CPunctuationToken(*this);
break;
case eTokenTypeEOL:
token = CEOLToken(*this);
break;
case eTokenTypeComment:
token = CCommentToken(*this, GetBeginCommentldentifier(),
GetEndCommentldentifier());
break;
case eTokenTypeEOLComment:
token = CEOLCommentToken(*this, GetEOLCommentldentifier());
break;
case eTokenTypeString:
token = CStringToken(*this, GetStringldentifier(),
GetEscapeCharacter());
break;
case eTokenTypeCharacter:
I token = CCharacterToken(*this, GetCharacterIdentifier(),
j GetEscapeCharacter() );
' break;
1 case eTokenTypeNumeric:
token = CNumericToken(*this);
break;
case eTokenTypeLineContinuation:
token = CLineContinuationToken(*this,
1 GetLineContinuationldentifier());
break;
j case eTokenTypeEOF:
j token = CEOFToken(*this);
I break;
i case eTokenTypeOther:
286
Глава 8
ft,*
ч token = COtherToken(*this);
t ч break;
k default:
assert(false);
}
i
m_JTokenCount [eTokenTypeCount] ++ ;
m_TokenCount[token.GetType()]++;
"*■' return token;
I1)
I ПРИМЕЧАНИЯ
Главная задача сканера — разбить входной файл на отдельные лексемы.
Сканер определяет, какой будет следующая лексема, создает представитель
последней и возвращает его анализатору.
Благодаря множеству статических функций IsA() сканеру достаточно
опросить каждый из классов лексем, относится ли к нему следующая лексема.
Однако тут имеются сложности, относящиеся к порядку, в котором производится
этот опрос. В идеале опрос должен был бы проходить в порядке убывания
средней частоты вхождения лексем, чтобы избежать ненужных вызовов IsA(),
которые по большей части будут возвращать false. При такой стратегии опроса
возникают трудности из-за того, что некоторые из этих функций не
производят исчерпывающей проверки на принадлежность именно к данному типу, так
что здесь возможна ложная интерпретация. Если, например, функция CPunc-
tuationToken::IsA() вызывается до CStringToken::IsA(), в ряде случаев
лексема не сможет быть идентифицирована как строка, поскольку лексема
пунктуации не знает, что т\"' есть идентификатор строки, а не просто очередной
знак пунктуации. Конструктор CPunctuationToken() пометит следующую
лексему как eTokenTypePunctuation, будучи вызван вместо необходимого
здесь CStringToken(). Поэтому сканер должен идентифицировать различные
лексемы в определенном порядке.
В функции CScanner::PeekTokenType() из файла Scanner.cpp определение
типа лексем производится в следующем порядке:
1. IsWhiteSpaceToken,
2. IsWordToken,
3. IsEOLToken,
4. IsCommentToken,
5. IsEOLCommentToken,
6. IsStringToken,
7. IsNumericToken,
8. IsStatementEndToken,
9. IsLineContinuationToken,
10. IsPunctuationToken и
ll.IsEOFToken.
Управление исходным кодом
287
|ЗАЛ
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Хотя этот порядок может в известных пределах варьироваться,
изменять его нужно с большой осторожностью.
Другой важной функцией сканера является хранение параметров языка,
применяемых при распознавании лексем. Например, в сканере имеется
список зарезервированных слов и хранятся идентификаторы, по которым
определяется принадлежность лексемы к строкам, комментариям и прочим типам.
Хотя для некоторых из идентификаторов задаются значения по умолчанию,
необходимо явным образом присвоить значения для конкретного языка
исходного файла, который будет сканироваться. В scanner.h вы видите длинный
перечень функций, позволяющий задавать значения идентификаторов языка.
Имя каждой из них имеет префикс «Set».
Инициализация сканера для C++
Теперь, покончив с классами лексем и сканера, мы можем написать
простую функцию, показывающую их использование. Функция называется Get-
IncludeFileListO и вызывается для получения списка файлов, включаемых в
сканируемый исходный файл.
Так как синтаксический анализатор и сканер написаны так, чтобы
обеспечить возможность обработки любого языка, необходимо их предварительно
настроить на распознавание символов C++. Это делается функциями из файла
CodeMaintCPP.h. Именно здесь мы загружаем зарезервированные слова и все
остальное, специфическое для C++, и определяем правила форматирования
текстов C++. (Более подробное объяснение определений форматирования
будет дано при рассмотрении функции AutoIndent().)
Все функции загрузки C++, а также Get IncludeFileListO, включены в файл
CodeMaintCPP.h. Таким образом, в нем сосредоточен весь код, специфический
для обработки языка C++.
Код
Ниже приведен код файла CodeMaintCPP.h, настраивающий сканер на
язык C++.
// CodeMaintCPP.h : Определяет некоторые функции,
// специфические для C++.
//
// Этот файл предназначен для предустановки объектов
// СScanner и CCodeParser данными, специфическими для C++.
// Включены также некоторые дополнительные функции
// управления файлами C++.
///////////////////////////////////////////////////////////////////
#if !defined(CODEMAINTCPP_H_INCLUDED)
#define CODEMAINTCPP H INCLUDED
288
Глава 8
\„
.// Идиосинкразии Microsoft.
■ //
j#if _MSC_VER > 1000
!// Отключить предупреждение С4786: symbol greater than 255 character,
// okay to ignore (MSVC)
#pragma warning(disable: 4786)
#pragma once
#endif // MSC VER > 1000
//
i// Локальные включаемые файлы.
j/z
i#include "Scanner.h"
,#include "CodeParser.h"
#include "Token.h"
//
// Библиотечные включаемые файлы.
//
4#include <set>
#include <string>
}#include <cassert>
using namespace std;
\„
4// Определения фзыка C++.
4П
7#define CPP_LINE_CONTINUATION_CHARACTER "\\"
s#define CPP_ALLOW_UNDERSCORE true
itfdefine CPP_COMMENT_IDENTIFIERS "/*", "*/"
Jtfdefine CPP_EOL_COMMENT_IDENTIFIER "//"
#define CPP_STRING_IDENTIFIER_CHARACTER '\'"
.#define CPP_CHARACTER_IDENTIFIER 'V'
#define CPP_STRING_ESCAPE_CHARACTER '\\*
.^define CPP_STATEMENT_END_CHARACTER
■f
4#define CPP_RESERVE_WORD_COUNT 60
fldefine CPP_RESERVED_WORDS \
J "auto", "bool", "break", "case", "catch", "char", "class",
* "const", "cons^cast", "continue", "default", "delete", "do",
" "double", "dynamic_cast", "else", "enum", "explicit", "extern",
i "false", "float", "for", "friend", "goto", "if", "inline",
1 "int", "long", "mutable", "namespace", "new", "operator",
i "private", "protected", "public", "register",
; "reinterpret_cast", "return", "short", "signed", "sizeof",
"static", "static_cast", "struct", "switch", "template",
"this", "throw", "true", "try", "typedef", "typeid",
I "typename", "union", "unsigned", "using", "virtual", "void",
"volatile", "while"
\u
Управление исходным кодом
289
// Правила форматирования C++.
J// -
#define CPP_FORMAT_STRINGS_COUNT 10
ttdefine CPP_FORMAT_STRINGS \
"#", "{", ">", ":", "case", "default", "for", \
1 "private", "protected", "public"
I
'#define CPP_FORMAT_FLAGS \
| /* .-#» */ \
elndentlgnore, \
\
/* "{" */ \
eindentAll | eindentlgnoreStatementEnd I eIndentStatementEnded \
| elndentNewLineBefore ( elndentNewLineAfter, \
\
/* "}" */ \
eIndentStatementEnded | elndentDecrement \
| eindentlgnoreStatementEnd | elndentNewLineBefore, \
\
/* »:" */ \
eIndentStatementEnded, \
\
/* "case" */ \
elndentDecrement | eindentAll | eindentlgnoreStatementEnd, \
\
/* "default" */ \
elndentDecrement | eindentAll | eindentlgnoreStatementEnd, \
\
/* "for" */ \
eIndentIgnoreNewLineAfter, \
\
/* "private" */ \
elndentlgnore, \
\
/* "protected" */ \
elndentlgnore, \
\
/* "public" */ \
eIndentIgnore
typedef set<string> FileList;
// -
// Функции эагруэки C++
//
// Загрузить сканер значениями для C++,
void LoadCPPScanner(CScannerS scanner)
{
TokenTextList* pReservedWords;
const nReservedWordElements = CPP RESERVE WORD COUNT;
10 Зак. 1208
290 Глава 8
Я
VI
*
'*
и
I?
char* ReservedWordsArray [nReservedWordElements]
= {CPP_RESERVED_WORDS};
int nCount;
pReservedWords = scanner.GetReservedWords();
for(nCount = 0; nCount < nReservedWordElements; nCount ++)
{
pReservedWords->insert(pReservedWords->end(),
ReservedWordsArray[nCount] );
)
scanner.SetAllowUnderscore(CPP_ALLOW_UNDERSCORE);
scanner.SetCommentldentifiers(CPP_COMMENT_IDENTIFIERS);
scanner.SetEOLCommentIdentifier(CPP_EOL_COMMENT_IDENTIFIER);
scanner.SetLineContinuationldentifier(
CPP_LINE_CONTINUATION_CHARACTER);
scanner. SetStringldentifier (CPP_STRING_IDENTIFIER_CHARACTER,
CPP_STRING_ESCAPE_CHARACTER);
scanner.SetCharacterIdentifier(CPP_CHARACTER_IDENTIFIER,
CPP_STRING_ESCAPE_CHARACTER);
scanner. SetStatementEndldentifxer (CPP_STATEMENT_END_CRARACTER) ;
// Загрузить анализатор C++.
void LoadCPPFormat(CCodeParserfi parser)
(
int nCount;
FormaterStringCol* pFormatStrings;
const nFormatStringCount = CPP_FORMAT_STRINGS__C0UNT;
const char* FormatStrings[nFormatStringCount]
= {CPP_FORMAT_STRINGS};
const long StringFormatingFlags[] = <CPP_FORMAT_FLAGS);
pFormatStrings = parser.GetFormatStringsO;
for(nCount = 0; nCount < nFormatStringCount; nCount ++)
(
// Добавить каждую строку и ее флаги к набору FormatStrings.
pFormatStrings->insert(pFormatStrings->end(),
FormaterStringCol: :value__type(
FormatStrings[nCount],
StringFormatingFlags[nCount]) );
}
//
// Другие специфические функции C++.
//
Управление исходным кодом
291
II Возвратить список имен файлов, включенных
// во входной поток CScannerfi.
bool GetlncludeFileList(CScannerS input, FileListS fileList)
i
II Возвратить false при неудачно» завершении,
bool bSuccess = false;
CToken token;
string sToken;
token = input.GetToken();
while(token.GetType() !- eTokenTypeEOF)
<
II Искать #include.
if (token.GetTokenText() ™ "#")
{
// Получить следующую лексему.
token * input.GetToken();
if (token.GetTokenText() ~= "include")
{
// Игнорировать пробелы и комментарии.
do
<
token = input.GetToken();
)
while ( (token.GetType() ~~ eTokenTypeWhiteSpace)
|| (token.GetType() = eTokenTypeCorament) );
if(token.GetType{) == eTokenTypeString)
{
// Имя файла заключено в кавычки, как в
// #include "CPPParser.h"
sToken = token.GetTokenText();
fileList.insert(sToken.substr(
1, sToken.length()-2) );
)
else
{
// Имя файла заключено в угловые скобки,
// показывая, что это библиотечный файл, как в
// #include <map>
if(token.GetTokenText() — "<")
{
sToken = "<";
token = input.GetToken();
while( (token.GetType() != eTokenTypeEOF)
&& (token != ">"))
{
sToken += token;
token = input.GetToken(); \
}
if( (token.GetType() — eTokenTypePunctuation)
&£ (token.GetTokenText() = ">") )
(
10*
292
Глава 8
\**\ sToken += ">";
* i fileList. insert (sToken) ;
* 1 }
u. A else
ш J bSuccess » false;
■ | } // Конец if (token.GetTokenText() = "<")
i } // Конец if (token.GetType() = eTokenTypeString)
} // Конец if (token.GetTokenText() == "include")
■ J ) // Конец if (token.GetTokenText() == "#")
token = input.GetToken();
)
- M
, J // Если сканирование успешно достигло конца файла,
ь ■* // возвратить true.
if (token.GetType() = eTokenTypeEOF)
i
bSuccess = true;
}
return bSuccess;
// Следующая функция используется для проверки опции
// автоматических отступов.
// Она удаляет из файла все имеющиеся отступы,
bool Jumbalize(CScannerfi input, ostreamfi output)
'<
r J assert(input.good() fi& input.is_open());
i
» - •
1 ■
f '
CToken token;
bool bRet = false;
bool bPrintEOL = false;
string sCurrentLine;
token = input.GetToken();
while(token.GetType() != eTokenTypeEOF)
<
switch( token.GetTypeO )
{
case eTokenTypeEOLComment:
bPrintEOL= true;
J sCurrentLine += token;
break;
case eTokenTypeLineContinuation:
bPrintEOL^ true;
sCurrentLine +- token;
break;
' case eTokenTypeEOL:
J output « sCurrentLine;
if (bPrintEOL || sCurrentLine.empty()
|| (input.PeekTokenType() «= eTokenTypePunctuation)
- J
\ 1
Управление исходным кодом 293
|| CToken::IsOnlyWhiteSpaceLeft(input) )
п
{
output « token;
sCurrentLine.erase();
j i bPrintEOL = false;
break;
case eTokenTypeWhiteSpace:
if(< sCurrentLine.empty{))
sCurrentLine +— token;
break;
case eTokenTypePunctuation:
if( token.GetTokenText() == "#")
bPrintEOL= true;
sCurrentLine += token;
break;
case eTokenTypeWord:
if(input.IsReservedWord(token))
j' I bPrintEOL = true;
sCurrentLine += token;
break;
default:
sCurrentLine += token;
break;
}
token = input.GetToken();
)
// Записать последнюю строку на случай,
// если что-нибудь осталось,
output « sCurrentLine;
// Если сканер успешно дошел до конца файла,
// возвратить true.
| if (token.GetTypeO == eTokenTypeEOF)
'" - (
j bRet - true;
■ j
f * return bRet;
'f
.l#endif // !defined(CODEMAINTCPP H INCLUDED)
\wpv
ПРИМЕЧАНИЯ
В CodeMaintCPP.h имеется ряд директив #define, определяющих
идентификаторы строк для C++, а также ключевые слова и различные константы.
Эти директивы используются функцией LoadCPPScanner() для загрузки
сканера соответствующими значениями. После загрузки вызывается Getlnclude-
294
Глава В
FileList()> которая распечатывает список файлов, включенных в указанный
исходный файл, как в следующем примере:
С:\>SCodeMnt SCodeMnt.cpp /include
<assert.h>
<fstream>
<iostream>
<ostream>
<set>
CPPParser.h
CommandLine.h
Scanner.h
stdafx.h
C:\>
Функция CCPPParser::GetIncludeFileList() сравнительно проста. Тело
функции представляет собой цикл while, внутри которого запрашиваются и
обрабатываются последовательные лексемы. Цикл завершается, когда сканер
возвращает лексему типа eTokenTypeEOF. В процессе итерации ищутся
лексемы со значением «#». Как только такая лексема найдена, функция проверяет
лексему, идущую непосредственно за данной — равна ли она include. Бели это
так, то мы нашли директиву включения файла. После этого удаляются все
пробельные символы и комментарии, и, таким образом, следующая лексема
должна быть именем файла.
Это имя может быть представлено в двух формах: в обычных кавычках
("CodeParser.h") или в угловых скобках (<stdio.h>). К сожалению, если имя
указано в угловых скобках, оно состоит не более и не менее, как из пяти
отдельных лексем (<, stdio, ., h, >), так что в этом случае процесс получения
имени более сложен. После извлечения имени оно добавляется в параметр file-
List для возврата в вызывающую программу.
GetIncludeFileList() является единственной функцией, написанной
специально для обработки файлов кода C++. Именно поэтому она включена в файл
CodeMaintCPP.h, а не находится в CodeParser.cpp вместе с остальными
функциями анализатора синтаксиса. Последние являются обобщенными,
благодаря чему при соответствующей загрузке сканера можно порождать лексемы
других языков.
Хотя вышеописанная функция и проста, она может служить введением в
более сложные функции из класса CCodeParser.
Класс синтаксического разбора CCodeParser
В классе синтаксического анализатора реализуются функции фактического
манипулирования кодом. Именно здесь мы находим функции WriteHTML() и
AutoIndent().
Код
Класс CCodeParser производит синтаксический разбор. Определение класса
находится в файле CodeParser.h:
Управление исходным кодом
295
// CodeParser.h: интерфейс класса CCodeParser.
//
// Класс реализует синтаксический раэбор программного кода.
// Одной из главных его функций является форматирование
// кода в соответствии с заданными правилами.
///////////////////////////////////////////////////////////////////
#if !defined(CODEPARSER_H_INCLUDED)
#define CODEPARSER_H_INCLUDED
//
// Причуды Microsoft.
//
#if _MSC_VER > 1000
// отключить предупреждение С4786: symbol greater than 255
character,
// okay to ignore (MSVC)
#pragma warning(disable: 4786)
#pragma once
#endif // MSC VER > 1000
//
// Локальные включаемые файлы.
II
linclude "token.h"
If include " scanner, h"
к :
// Библиотечные включаемые файлы.
II
^include <string>
#include <map>
ft include <set>
ftinclude <ostream>
using namespace std;
//
// Разнообразные макроопределения.
//
#define SUMMARY__HEADER "\n\nSUMMARY INFORMATION^";
// Строка по умолчанию для отступов.
#define DEFAULT__INDENT_STRING " "
//
// Константы HTML.
//
#define HTMX_HEADER \
"<HTML>" \
"\n<HEAD>M \
"\n<META NAME=\"\" Content=\"Source Code Рагвег\">" \
"\n<META HTTP-EQUIV=\"Content-Type\"content=\"text/html\">" \
"\n<TITLE>"
296
Глава 8
1 Т
#define DEFAULT TITLE "Source Code Colorized"
J 3#define HTML_SUBHEADER \
"\n</TITLE>" \
,„4 "\n</HEAD>\n\n" \
Г" "\n<BODY>" \
"\n<PRE>\n"
#define HTML_GREATERTHAN "figt;"
*i#define HTML LESSTHAU "<"
'. -1
//
.4
En
V
Г
SJ. // EIndent определяет правила форматирования
// отдельной строки.
i
fe
v>
I'
// Перечисления.
It
'■1
enum EIndent
{
// Без отступа.
elndentNone — 0,
// Отступ для всех следующих строк.
// (Пока не встретится лехсема elndentDecrement)
elndentAll - 1,
//Не вводить отступ для ТЕКУЩЕЙ строки,
elndentlgnore - elndentAll « 1,
//Не увеличивать счетчик отступа, если предьодущая
// строка не расценивается как завершенная.
elndentlgnoreStatementEnd = elndentlgnore « 1,
// Уменьшить счетчик отступов для ТЕКУЩЕЙ
//и ПОСЛЕДУЮЩИХ строк.
elndentDecrement - elndentlgnoreStatementEnd « 1,
// Показывает, что текст завершает строку или
// что оператор завершен.
elndentStatementEnded = elndentDecrement « 1,
// Эта лексема расширяет оператор на следующую строку.
\
ft #define HTML_F00TER \
"\n\n</PRE>" \
Ч&1 "\n</BODY>"
1 M\n</HTML>"
Is: #define HTML_TAB "finbsp;finbsp;finbsp;finbsp;finbsp;"
Г #define HTML_SPACE " "
:V]#define HTMLEOF "<PX/P>"
Управление исходным кодом
297
t
U
t
*
t
У
С
// Следующая строка должна иметь отступ такой же, как
// если оператор предыдущей строки не завершен.
elndentLineContinuation = elndentStateraentEnded « 1,
// Поместить лексему на следующую строку.
elndentNewLineBefore = elndentLineContinuation « 1,
// Поместить после лексемы символ новой строки.
elndentNewLineAfter = elndentNewLineBefore « 1,
// Не ставить перевод строки, даже если elndentNewLineAfter
// истинно. Например, если elndentNewLineAfter истинно
// для ';'. Объявление elndentlgnoreNewLineAfter
// для строки "for" предотвратит перевод строки
// внутри операторов 'for'.
elndentlgnoreNewLineAfter = elndentNewLineAfter « 1
typedef map<stringr long> FormaterStringCol;
typedef map<string, string> FormaterPairCol;
//
// Объявление класса.
//
,class CCodeParser
{
//
// Конструктор/деструктор.
XJi
» >
l Л
public:
CCodeParser();
virtual -CCodeParser();
//
// Защищенные элементы данных.
//
^protected:
1 // Сканер, используемый анализатором для
// извлечения лексем.
CScanner* m_pScanner;
// Цвета для HTML-вывода.
map<int, string> m_Colors;
// Цвет зарезервированных слов для HTML,
string m__ColorReservedWord;
// Хранит характеристики формата для функции AutoXndent
// Каждый пункт является строковым флагом Autolndent.
FormaterStringCol m_FormaterStrings;
// Текущая обрабатываемая лексема.
CToken m CurrentToken;
298
Глава 8
'• I
'1
■ J
. I
* «
// Выходной поток форматирования,
ostream* m_pOutput;
// Показываает, что последняя строка
// завершается законченным оператором,
int m_nLastStatementEnded;
// Текущая форматируемая строка,
string mjsCurrentLine;
// Определяет строку для каждого уровня отступа.
// Это может быть, например, символ табуляции
// или 5 пробелов,
string m_sIndentText;
// Показывает уровень отступа текущей строки,
int m_nCurrentLineIndentLevel;
// Показывает уровень отступа следующей строки,
int m_nNextLineIndentLevel;
// Хранит флаги форматирования текуущей строки,
int m_nCurrentFormatFlags;
г 1
//
ья4// Функции доступа к элементам данных.
//
public:
CScanner* GetScanner{)
return mjpScanner;
void SetlndentString(const string slndentText)
m_sIndentText = slndentText;
const string GetIndentString()
return m_sIndentText;
FormaterStringCol* GetFormatStrings()
return &m_FormaterStrings;
TokenTextList* GetReservedWords()
return m_pScanner->GetReservedWords();
i i
f
v.
//
-Я// Защищенные функции.
Управление исходным кодом
299
//
[protected:
// Напечатать сводку (число прочитанных строк и т.п.)
//о процессе синтаксического раэбора.
void PrintStatistics(ostreamfi output = cout);
virtual void PrintStatisticsHeader(ostreamfi output = cout);
// Окружить лексему соответствующими флагами
// формата HTML.
string FormatHTML(const CToken& token);
// Некоторые символы не могут быть непосредственно
// записаны в файл HTML. GetHTMLToken заменяет их
// соответствующими HTML-эквивалентами,
string GetHTMLToken(const CTokenS token);
// Записать заголовок html.
WriteHTMLHeader(ostreamfi output, string sTitle);
WriteHTMLFooter(ostreamfi output);
// Функции автоматического отступа.
// Обработать FormatDefinition для текущей строки
//и сохранить флаги.
// Заметьте, что elndentNewLine перед лексемой
// установлен, то строка будет передана на выход.
void SetlndentHandler(const long nlndentFlags);
// Форматировать текущую строку, включая отступы.
string GetIndentedLine();
// Вставить в текущую строку следующую лексему.
// Если это EOL, передать строку на выход.
void PutlndentedToken();
//
// Открытые функции.
//
public:
// Сбросить все текущие счетчики, вернув объект
// к состоянию, существовавшему непосредственно
// после его создания,
virtual void Reset();
// Получает от сканера следующую лексему,
virtual CToken GetNextToken();
// Копирует входной поток в выходной.
virtual bool Copy(CScannerfi input, ostreamfi output);
virtual bool WriteHTML(CScannerfi input, ostreamfi output);
virtual bool Autolndent(CScannerfi input, ostreamfi output);
};
#endi£ // !defined(CODEPARSER H INCLUDED)
300
Глава 8
I ПРИМЕЧАНИЯ
Хотя класс CCodeParser — специализированный анализатор для
программных языков, каким-либо одним конкретным языком он не ограничен. Двумя
его важнейшими функциями являются WriteHTML() и AutoIndent().
WriteHTML() генерирует HTML-представление исходного кода, снабжая
каждую его лексему ярлычками HTML, выделяющими определенные типы
лексем своим цветом. Полученный файл можно просматривать в обозревателе
или распечатывать на цветном принтере.
Вторая функция, AutoIndent(), выводит файл с «правильными» отступами.
Мнений относительно того, что тут «правильно», больше, чем самих
программистов. AutoIndent() следует одному из возможных наборов правил, но она легко
может быть модифицирована и для других правил форматирования. Правила
хранятся в карте (таблице) m_FormaterStrings типа FormaterStringCol. Это typedef
шаблона map<string, long>. Значение long хранит набор флагов формата для
указанной строки. В таблице 8.2 приведены возможные флаги форматирования.
Таблица В.2, Допустимые флаги форматирования
Перечислимая
константа
elndentNone
elndentAII
elndentlgnore
elndentlgnore-
StatementEnd
e Indent Decrement
elndentStatementEnded
elndentLineContinuation
elndentNewLineBefore
elndentNewLlneAfter
elndentlgnoreNew-
Line After
Описание
Нет отступа.
Отступ всех ПОСЛЕДУЮЩИХ строк (пока не встретится
elndentDecrement).
Нет отступа ТЕКУЩЕЙ строки (для лексем препроцессора C++,
например).
Не увеличивать отступ, даже если оператор предыдущей
строки не закончен.
Уменьшить отступ ТЕКУЩЕЙ и ПОСЛЕДУЮЩИХ строк.
Показывает, что текст завершает оператор или является
законченным оператором.
Продолжить оператор на следующую строку, следующая
строка получит отступ, как если бы оператор не был завершен.
Ставит текущую лексему в новую строку.
Ставит перевод строки после текущей лексемы.
Если лексема встречается перед elndentNewLineAfter, то этот
флаг elndentNewLineAfter игнорируется (чтобы предотвратить,
например, перевод строки внутри оператора for). |
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Чтобы изменить правила для отступов, нужно получить таблицу
m_FormaterStrings, вызвав GetFormatStrings(), и модифицировать ее
пункты. Пользуясь вышеприведенными константами, объедините по
ИЛИ необходимые флаги для желаемых строк. Например, если вы хо-
Управление исходным кодом
301
mumet чтобы некоторая лексема занимала отдельную строчку,
установите значение long пункта таблицы равным eIndentNewLineBefore |
elndent New LineAfter.
Код
Следующий файл, CodeParser.cpp, содержит весь код реализации класса
CCodeParser:
к
// CodeParser.cpp: implementation of the CCodeParser class.
///////////////////////////////////////////////////////////////////
//
// Локальные включаемые файлы.
//
#include "CodeParser.h"
#include "scanner.h"
//
// Библиотечные включаемые файлы.
//
#include <istream>
#include <ostream>
#include <cassert>
using namespace std;
//
// Конструктор/деструктор
//
CCodeParser::CCodeParser()
// Инициализация защищенных элементов.
: m_sIndentText(DEFAULT_INDENT_STRING) ,
m_nCurrentLineIndentLevel(0),
m_nNextLineIndentLevel(0),
m_nLastStatementEnded(l),
m_nCurrentFormatFlags(elndentStatementEnded)
{
// Инициализация цвета лексем.
m_Colors[eTokenTypeEOF] =
m_Colors[eTokenTypeEOL] «
m_Colors[eTokenTypeWhiteSpace]
m_Colors[eTokenTypeString] = "DarkMagenta";
m_Colors[eTokenTypeCharacter] = "Magenta";
m_Colors[eTokenTypeNumeric] = "BlueViolet";
m_Colors[eTokenTypeComment] = "Green";
mjColors[eTokenTypeEOLComment] = "DarkGreen";
m_Colors[eTokenTypePunctuation] = "Maroon";
m_Colors[eTokenTypeWord] = "Black";
m__Colors [eTokenTypeLineContinuation] = "Crimson" ;
m_Colors[eTokenTypeStatementEnd] = "Red";
m__Colors [eTokenTypeOther] = "Fuchsia" ;
m_ColorReservedWord = "Blue";
}
1Г II .
I
IIII .
r
II II
302
Глава 8
CCodeParser::-CCodeParser<)
I
L h
I
I
/ Сброс всех текущих счетчиков; возврат в состояние,
//в котором об'ьект находился сразу после создания,
void CCodeParser::Reset()
m_pScanner->Reset();
CToken CCodeParser::GetNextToken()
return ( m_pScanner->GetToken() );
v jbool CCodeParser::Copy(CScannerfi input, оstreams output)
CToken token;
■ bool bRet = false;
f J
assert{input.good() && input.is_open());
m_pScanner = &input;
( token = GetNextToken();
J
'J while (token. GetTypeO != eTokenTypeEOF)
. {
^ ■ output « token;
token = GetNextToken();
}
I
I
// Если мы прошли до конца файла, возвратить
// true.
if (token.GetTypeO = eTokenTypeEOF)
{
PrintStatistics();
bRet = true;
)
m_pScanner = NULL;
return bRet;
>
L i
" 'void CCodeParser::PrintStatisticsHeader(ostreamfi output)
{
output « SUMMARY_HEADER;
:)
// Напечатать сводку.
—_ void CCodeParser::PrintStatistics(ostreams output)
Управление исходным кодом
303
PrintStatisticsHeader(output);
output « "Total number of whitespace tokens is " «
mjpScanner-XSetTokenCount(eTokenTypeWhiteSpace) « endl ;
output « "Total number of strings tokens is " «
mjpScanner-XSetTokenCount(eTokenTypeString) « endl;
output « "Total number of numeric tokens is " «
m_pScanner->GetTokenCount(eTokenType№imeric) « endl;
output « "Total number of comments tokens is " «
mjpScanner-XSetTokenCount(eTokenTypeComment) +
mj?Scanner->GetTokenCount(eTokenTypeEOLComment) « endl;
output « "Total number of punctuation tokens is " «
mj?Scanner->GetTokenCount(еТокепТуреPunctuation) « endl;
output « "Total number of word tokens is " «
m_pScanner->GetTokenCount(еТокепТуреWord) « endl;
output « "Total number of line continuation tokens is " «
m_pScanner->GetTokenCount(eTokenTypeLlneContinuation) « endl ;
output « "Total number of unidentified tokens is " «
m_j>Scanner->GetTokenCount(eTokenTypeOther) « endl;
output «
« endl;
output « "Total number of statements is " «
m_pScanner->GetTokenCount(eTokenTypeEOL) « endl;
//Мы полагаем, что даже если в istream находится
// 0 байт, он все равно имеет одну строку,
output « "Total number of lines is " «
mjpScanner->GetTokenCount(eTokenTypeEOL)+1 « endl;
output « "Total number of tokens is " «
mjpScanner->GetTokenCount(eTokenTypeCount) « endl;
//
// Функции HTML.
//
// Записать заголовок файла HTML.
CCodeParser::WriteHTMLHeader(ostreamfi output, string sTitle)
{
output « HTML_HEADER;
if(sTitle.empty())
output « DEFADLT_TITLE;
else
304 Глава 8
У.
// Записать постскриптум файла HTML.
CCodeParser::WriteHTMLFooter(ostreamS output)
{
output « "<HR>";
VC\ PrintStatistics (output) ;
output « HTML_FOOTER;
}
f-4
Ik"
Ы
T
} *
i
i
? 4
u*
output « sTitle;
output « HTML_SUBHEADER;
}
// Некторые символы не могут быть непосредственно
// записаны в HTML. GetHTMLToken заменяет их
// соответствующими HTML-эквивалентами.
string CCodeParser::GetHTMLToken(const CToken& token)
{
long nPos;
strlng sOutput;
switch(token.GetType())
{
case eTokenTypeEOF:
sOutput += HTML_EOF;
break;
case eTokenTypeWhiteSpace:
for( nPos = 0; (nPos < token.GetTokenTextO .length()); nPos++ )
<
if (token.GetTokenTextO [nPos] = '\f )
sOutput += HTML_TAB;
else
sOutput += HTML_SPACE;
}
break;
default:
sOutput = token;
// Заменить все '>' на HTML_GREATERTHAN
nPos = sOutput.find('>');
while(nPos != string::npos)
<
sOutput = sOutput.substr(0, nPos) + HTML_GREATERTHAN +
sOutput.substr(nPos+1, sOutput.length());
nPos = sOutput.find('>', nPos);
}
// Заменить все '<* на HTML_LESSTHAN
nPos = sOutput.find('<');
while{nPos != string::npos)
i
sOutput = sOutput.substr(0, nPos} + HTML_LESSTHAN +
sOutput.substr(nPos+1, sOutput.length());
nPos = sOutput.find('<', nPos);
Управление исходным кодом
305
..' J return sOutput;
*- 1
(// Получить текст каждого иэ m_CurrentToken, записанного
1 V/ в HTML
| 'string CCodeParser::FormatHTML(const CTokenfi token)
' i
string sOutput;
string sColor;
1
l1 i
I
if( (token.GetType() = eTokenTypeWord)
&£ m_pScanner->IsReservedWord(token.GetTokenText ()) )
sColor — mjColorReservedWord;
else
sColor = m_Colors[token.GetType()];
t\
swi tch (token.GetType())
<
case eTokenTypeEOF:
case eTokenTypeWhiteSpace:
sOutput += GetHTMLToken(token);
У 1 break;
j default:
1 if( ! m__Colors [ token. GetType ()]. empty () )
. i i
sOutput += "<FONT color=\"";
sOutput += sColor;
sOutput += "\" >";
sOutput += GetHTMLToken(token);
sOutput += "</FONT>";
>
else
sOutput += token.GetTokenText();
)
return sOutput;
i
7
W I
// Записать каждый m__Cur rentToken а файл в формате HTML,
bool CCodeParser::WriteHTML(CScanner£ input, ostreamfi output)
// Проверить, что входной сканер открыт,
assert( input.good() && input.is^open() );
bool bRet = false;
t ; m_pScanner = fiinput;
m_pOutput — &output;
WriteHTMLHeader(output, DEFAULT_TITLE);
fj m CurrentToken = GetNextToken () ;
306
Глава 8
while(m_CurrentToken.GetType{) \- eTokenTypeEOF)
{
output « FormatHTML(m__CurrentToken) ;
m_CurrentToken = GetNextToken();
}
WriteHTMLFooter(output);
// Если мы дошли до конца файла, возвратить
// true.
if (m_CurrentToken.GetType{) == eTokenTypeEOF)
{
bRet = true;
}
m_pScanner = NULL;
m_pOutput = NULL;
return bRet;
I
■I
//
// Функции Autolndent.
//
// Возвратить текущую строку с отступом,
string CCodeParser::GetIndentedLine()
i
// Возвращаемая строка.
string sOutput;
// Текст для отступа,
string slndentText;
// Если оператор предыдущей строки не закончен
// и не elndentlgnoreStatementEnd, сделать
// хотя бы один отступ.
if( !(mjiLastStatementEnded)
&& !(m_nCurrentFormatFlags & elndentlgnoreStatementEnd) )
slndentText = m_sIndentText;
// Захвачена вся строка.
if( m__nCurrentFormatFlags & elndentlgnore )
{
if( !{m_nCurrentFormatFlags & elndentLineContinuation) )
{
// Установлен флаг игнорирования отступа. Сбросить.
m_nCurrentFormatFlags &= -elndentlgnore;
// Предполагается, предыдущий оператор закончен.
// Если нет символа продолжения, пометить
// этот оператор как законченный.
m_nCurrentFormatFlags I= elndentStatementEnded;
}
Управление исходным кодом
307
}
else
{
// Отступы для каждого уровня.
for(int i=0; (i < mjaCurrentLinelndentLevel); i++)
{
slndentText += m_sIndentText;
)
)
// Записать отступ.
sOutput += slndentText;
// Вывести текущую строку.
sOutput += m_sCurrentLine;
// Output the current token
sOutput += CEOLToken();
// Переходим к подготовке следующей строки.
// Ее уровень отступа должен быть установлен
// равным m_nNextLineIndentLevel.
m_nCurrentLineIndentLevel = m_nNextLineIndentLevel;
// Очистить содержимое строки.
m_sCurrentLine.erase();
//Запомнить, завершилась ли строка законченным оператором.
m_nLastStatementEnded = (
(m_nCurrentFormatFlags & elndentStatementEnded)
&fi !(mjiCurrentFormatFlags & elndentLineContinuation) );
// Сбросить флаг elndentLineContinuation.
m_nCurrentFormatFlags £= -elndentLineContinuation;
// Сбросить флаг elndentlgnoreStatementEnd.
m_nCurrentFormatFlags &= -elndentlgnoreStatementEnd;
// Сбросить флаг eIndentIgnoreNewLineAfter.
m__nCurrentForma tFlags &•= -elndentl gnoreNewLineAf ter ;
// Сбросить флаг elndentNewLineAfter.
m_nCurrent Forma tFlags fi= "•elndentNewLineAfter ;
return sOutput;
t
// Установить обработчик отступа для текущей лексемы.
void CCodeParser::SetlndentHandler(const long nFormatFlags)
{
CToken CurrentToken;
] if ( (nFormatFlags 5 elndentNewLineBefore)
&£ (!m_sCurrentLine.empty()) )
{
// Лексема начинается с новой строки;
// форсировать новую строку.
m_nCurrentFormatFlags |= elndentNewLineBefore;
*m_pOutput « GetlndentedLine() ;
}
// Сбросить бит законченного оператора.
m_nCurrentFormatFlags &= ~eIndentStatementEnded;
if(nFormatFlags & elndentDecrement)
{
m__nCurrentLineIndentLevel — ;
m_nNextLineIndentLevel--;
}
// Увеличить отступ следующих строк.
if (nFormatFlags & elndentAll)
m_nNextLineIndentLevel++;
// Установить игнорирование, чтобы отступ
// не вводился.
if (nFormatFlags & elndentlgnore)
m_nCurrentFormatFlags |= elndentlgnore;
if (nFormatFlags & elndentLineContinuation)
m_nCurrentFormatFlags |= elndentLineContinuation;
if ( (nFormatFlags S elndentlgnoreStatementEnd)
&& (m_sCurrentLine.empty()) )
// Игнорировать то, что предыдущий оператор
// не закончен, если текущая строка пустая.
m_nCurrentFormatFlags |= elndentlgnoreStatementEnd;
// Пометить оператор как законченный после
// этого m_CurrentTolcen.
if (nFormatFlags fi elndentStatementEnded)
m_nCurrentFormatFlags |= elndentStatementEnded;
// Присвоить m__CurrentToken текущую строку, проверив
// формат, так как при некоторых форматах важно,
// если m_sCurrentLine пуста.
m_sCurrentLine += m_CurrentToken;
if ( nFormatFlags S eIndentIgnoreNewLineAfter )
m__nCurrentFormatFlags |= elndentlgnoreNewLineAfter;
if ( (nFormatFlags & elndentNewLineAfter)
&& >(m_nCurrentFormatFlags fi elndentlgnoreNewLineAfter)
&& !m_pScanner->IsOnlyWhiteSpaceLeft() )
{
// Форсировать новую строку
m nCurrentFormatFlags |= elndentNewLineAfter;
Управление исходным кодом 309
*m_pOutput « GetIndentedLine();
>
}
// Поместить текущую лексему в
// текущую строку.
void CCodeParser::PutIndentedToken()
{
CToken PreviousToken;
string sNextLine;
FormaterStringCol::const_iterator FormatStringlterator;
FormaterPairCol::const_iterator FormatPairlterator;
// Построить строку.
switch (m__CurrentToken. GetType ())
<
case eTokenTypeEOL:
// Этого не должно быть. EOL должен проверяться
//до того, как мы попадем сюда.
assert(false);
break;
//
case eTokenTypeWhiteSpace:
if (! m__sCurrentLine. empty ())
<
// Пока текущая строка не пуста, так что
// нужно записать указанный пробельный символ.
m__sCurrentLine += m_CurrentToken ;
)
break;
//
case eTokenTypeComment:
case eTokenTypeEOLComment:
m_sCurrentLine += m_CurrentToken;
break;
//
case eTokenTypeStatementEnd:
m_nCurrentFormatFlags |= elndentStatementEnded;
m_sCurrentLine += m_CurrentToken;
// Всегда записывать новую строку в конце
// оператора, если в правилах это не оговорено
// специально.
if( !(m_nCurrentFormatFlags & elndentlgnoreNewLineAfter)
&& !m_pScanner->lsOnlyWhiteSpaceOrConmientsLeft() )
<
' m nCurrentFormatFlags | = elndentNewLineAfter;
310
Глава 8
* *mjpOutput « GetIndentedLine();
}
i 'A
I
break;
//
ft *
case eTokenTypeLineContinuation:
m__nCurrentFormatFlags |= elndentLineContinuation;
m_sCurrentLine +~ m_CurrentToken;
break;
//
default:
// Проверить, имеет ли m_CurrentToken описатель формата.
FormatStringlterator =
/ | m_FormaterStrings.find(m__CurrentToken.GetTokenText()) ;
i
*
if(FormatStringlterator != m_FormaterStrings.end{))
{
// Обработать описатель формата.
SetlndentHandler(FormatStringlterator->second);
}
else
I i {
U "! // Сбросить бит окончания.
|-. \i m_nCurrentFormatFlags &= -elndentStatementEnded;
m_sCurrentLine += m_CurrentToken;
}
и
k"
i
r=
t
4
r
Г
.1 m_pScanner = &input;
L j mjpOutput = &output;
bool CCodeParser::AutoIndent(CScanner& input, ostreamfi output)
{
assert(input.good() && input.is_open());
M
■•'■■J'
bool bSuccess = false;
m_nLastStatementEnded = 1;
m CurrentToken = GetNextToken();
while (m_CurrentToken.GetType() != eTokenTypeEOF)
<
£ switch(m_CurrentToken.GetType())
{
L_ case eTokenTypeEOL:
Управление исходным кодом
311
' i // Записать строку.
v' J *m_pOutput « GetIndentedLine();
if ] break;
V J default:
// Поместить следу
// current line.
PutIndentedToken();
} // Конец switch(m CurrentToken.GetType())
L '
4
v
M
a ■
uA
m_CurrentToken = GetNextToken();
} // Конец while(m_CurrentToken.GetType() != eTokenTypeEOF)
*m_pOutput « GetlndentedLine();
// Если мы дошли до конца файла, возвратить
// true.
if (m_CurrentToken.GetType() — eTokenTypeEOF)
{
bSuccess — true;
}
PrintStatistics();
m_pScanner = NULL;
m_pOutput = NULL;
return bSuccess;
J ПР1/
ПРИМЕЧАНИЯ
Давайте более подробно рассмотрим две главных функции класса
синтаксического анализа: WriteHTML() и AtitoIndent(). Подобно GetlneludeFileList(),
в каждой из этих функций организован цикл while, в котором у сканера
запрашиваются последовательные лексемы; затем они обрабатываются. Цикл
завершается, когда сканер возвращает лексему типа eTokenTypeEOF.
Цикл while в WriteHTML() охвачен функциями WriteHTMLHeader() и
WriteHTMLFooter(); они формируют формат файла HTML. Внутри цикла
происходят вызовы GetNextToken(). Для каждой возвращенной лексемы
вызывается FormatHTMLO, чтобы получить ее html-представление. Эта функция
имеет несколько более сложное строение. FormatHTMLO содержит оператор
switch, который преобразует лексему в соответствии с ее типом.
Заметьте, что пробелы, табуляции и символы конца файла не могут быть
записаны в html-файл непосредственно, поэтому они преобразуются в
соответствующие html-представления. Последние определены в файле CodeParser.h:
#define HTMLJFAB "Snbsp; finbsp; inbsp; "
#define HTML_SPACE "finbsp;"
#define HTML EOF "<PX\P>"
312
Глава 8
Если лексема не является пробельным символом или концом файла, то по
умолчанию она окружается ярлычками шрифта, которые задают цвет ее
отображения. Цвета хранятся в таблице m_Colors, которая определяется как
map<int, string>. Тем самым возможно вводить дополнительные цвета для
лексем пользовательских типов, которые еще не определены. После создания
файла HTML (ключ /html) вы можете просмотреть его содержимое в
обозревателе Internet (рис. 8.3).
Рис. 8.3.
Окно обозревателя,
отображающее содержимое
файла, обработанного с
ключом /html. Каждая
лексема выделена своим
собственным цветом
и/: J- '*> ,-* j-v- у+- j^.-
. ; . -: - ■ !.т - * ..'.• - ь.
1
г* .
AutoIndent() — наиболее сложная из всех функций, рассматривавшиеся до
сих пор, поскольку она написана так, чтобы ее можно было настраивать без
изменения ее собственного кода. То, как будет форматироваться файл, можно
изменить путем динамической модификации строк в карте m__FormaterStrmgs.
Например, если вы добавите к ней строку if и установите для этого пункта
флаг eIndentNewLineBefore, то при получении лексемы со строкой if перед
ней всякий раз будет вставляться символ новой строки.
Подобно другим, уже рассмотренным функциям анализа кода, AutoIndent()
содержит цикл while, выполняющий итерации для каждой лексемы,
полученной от сканера, с последующей ее обработкой. После получения лексемы
устанавливается переменная m_CurrentToken и вызывается функция Put In den-
tedToken() (если только сканер не возвращает лексему типа eTokenTypeEOL).
Эта функция помещает лексему в текущую строку. Если же лексема принадле-
Управление исходным кодом 313
жит к типу eTokenTypeEOL, то текущая строка дополняется символами
отступа и записывается в выходной поток.
PutIndentedToken() содержит большой оператор switch, обрабатывающий
каждую лексему в соответствии с ее типом. Если лексема не обрабатывается
явным образом с одной из групп case, для нее выполняется раздел по
умолчанию оператора switch. В этом разделе ищется соответствие лексемы
какой-либо строке формата. Если лексема найдена, она обрабатывается функцией Set-
IndentHandler(). Последняя устанавливает переменную m_CurrentFofmat-
Flags для следующей строки и увеличивает либо уменьшает уровень отступа.
SetIndentHandler(), кроме того, форсирует вставку новой строки после
каждого оператора и удаляет любые пробельные символы в начале строк (поскольку
они будут вставлены обратно при вызове GetIndentedLine(».
Наконец, когда все лексемы текущей строки обработаны, внутри Autoln-
dent() будет распознан case eTokenTypeEOL. Будет вызвана функция Getln-
dentedLine(), чтобы форматировать строку в целом и снабдить ее
соответствующим отступом перед тем, как она будет передана на выход.
Показанные ниже два листинга иллюстрируют обработку файла с
автоматическими отступами. Заметьте, что в первом из них вообще нет отступов, а
некоторые строки содержат по два оператора.
Исходный файл
/* Демонстрационный файл. */
#include <iostream>
#include <set>
int main( int argc, char **argv )
{int i = OeO;
std::set<char>::const_iteratorpC;
std::set<char> CharSet;
while( i <= 255 )
(if (ispunct(i)!=0)
CharSet.insert((char)i);
i++;}
// Распечатать по порядку
// все знаки пунктуации.
for(pC = CharSet.begin(); pC != CharSet.end(); pC++)
{ /* Напечатать символ. */
std::cout « *pC « std::ends;}std::cout « std::endl,return 0;
}
Выходной файл с автоматическими отступами
/* Демонстрационный файл. */
#include <iostream>
#include <set>
int main( int argc, char **argv )
int i = OeO;
314
Глава 8
std::set<char>::const_iteratorpC;
std::set<char> CharSet;
while( i <- 255 )
{
if (ispunct(i)!=0)
CharSet.insert((char)i);
i++;
)
// Распечатать по порядку
// все знаки пунктуации.
for(pC « CharSet.begin(); pC != CharSet.end(); pC++)
{
/* Напечатать символ. */
std::cout « *pC « std::ends;
}
std::cout « std::endl;
return 0;
}
Теперь, когда мы разобрались с основной структурой AutoIndent(), можно
поближе познакомиться с форматирующими флагами, загружаемыми для C++.
В файле CodeMaintCPP.h мы видели функцию LoadCPPFormat(). Давайте
рассмотрим детали форматирования кода C++. В таблице 8.3 перечислены
строки, для которых определяется форматирование.
Таблица 8.3. Строки формата для C++.
Строка формата
"#"
Т
"}"
.....
"case", "default"
"for"
"private"
"protected"
"public"
Присваиваемые флаги enum
elndentlgnore
elndentAII | elndentlgnoreStatementEnd | elndentStatementEnded |
elndentNewLineBefore | elndentNewLineAfter
elndentStatementEnded | elndentDecrement |
elndentlgnoreStatementEnd | elndentNewLineBefore
elndentStatementEnded
elndentDecrement | elndentAII
elndentlgnoreNewLineAfter
elndentlgnore
В таблице 8.4 представлены пояснения для каждой из строк и
приписанных ей флагов.
Управление исходным кодом
315
Таблица 8.4. Строки и флаги формата
Строка
формата
"#"
.
"}"
Присваиваемые флаги
Этот символ используется в препроцессорных директивах, таких, как
#define и #endif. Их строки всегда начинаются с первой позиции, так что
никаких отступов не делается. Флаг отступа, таким образом,
устанавливается равным elndentlgnore.
// Библиотечные включаемые файлы.
#include <iostream>
Начинает блок кода.
Комбинация elndentNewLineBefore | eIndentNewLineAfter приводит к
тому, что скобка печатается на отдельной строчке.
elndentlgnoreStatementEnd означает, что строка, начинающаяся со
скобки, не будет иметь дополнительного отступа, даже если предыдущая
строка не закончена. Например, можно поместить скобку после заголовка
оператора while, строка которого не оканчивается точкой с запятой.
elndentlgnoreStatementEnd разместит скобку непосредственно в
позиции, с которой начинается оператор while.
elndentStatementEnded говорит, что строка, заканчивающаяся скобкой,
должна считаться полным оператором.
while (token.GetType() != eTokenTypeEOF)
f
cout « token;
token = scanner.GetNextToken();
}
Уменьшает отступ текущей и всех последующих строк благодаря флагу
elndentDecrement. Тем самым уровень отступа возвращается к значению,
которое он имел до появления открывающей скобки.
elndentNewLineBefore помещает скобку на новую строку. Заметьте, что
флаг eIndentNewLineAfter не устанавливается. Это привело бы к переносу
комментария или точки с запятой, следующих за скобкой, на новую строку.
elndentlgnoreStatementEnd применяется для того, чтобы в случае, когда
предыдущая строка не закончена, отступ для скобки не увеличивался. Это
нужно, например, для закрывающих скобок операторов enum, потому что
внутри определения enum нет точек с запятой.
Последний флаг, elndentStatementEnded, показывает, что не нужно
вводить отступ после закрывающей скобки.
Рассмотрите такой фрагмент:
enum EType
<
elnteger,
eFloat,
eDouble
}
while(bNotFinished)
{
bNotFinished = GetNext();
> I
316
Глава 8
Строка
формата
"}"
11 П
"case",
"default"
"for"
Присваиваемые флаги
Обратите внимание, что eFloat смещено дальше, чем elnteger. Это
потому, что строка с elnteger не оканчивается точкой с запятой, и в то же
время нет символа, которому можно было бы приписать флаг
elndentlgnoreStatementEnd. Конечно, вы можете изменить такое
поведение. Вторая неприятность касается объявления и инициализации
массивов. Поскольку в инициализации также используются фигурные
скобки, объявление разбивается на отдельные строки.
int array[] =
i
1,2,3,4
)
elndentStatementEnded применяется, чтобы следующий за меткой case
или default оператор не получал лишнего отступа.
elndentDecrement и eIndentAII размещают метки вариантов и умолчания
на том же уровне, что и заголовок switch.
void myfunc()
{
switch (token.GetType())
{
case eTokenTypeEOF:
sOutput += HTML_EOF;
break;
default:
sOutput += token;
}
1
Устанавливается elndentlgnoreNewLineAfter, чтобы предотвратить вставку
новой строки даже в случае, когда в той же строке встречается конец
оператора. Это необходимо, поскольку в заголовке оператора for имеются
две точки с запятой, расцениваемые как признак конца оператора,
void myfunc()
{
int i;
for(i = 0; i < repeat; i ++)
{
cout « "-";
}
}
С оператором for происходит такая неувязка: если в исходном тексте в
заголовке оператора имеется перевод строки, то все его разделы
располагаются в разных строках.
void myfunc()
{
int а;
for(i ■ 0;
i < repeat;
i ++)
{
cout « "-";
>
Управление исходным кодом
317
Строка
формата
Присваиваемые флаги
Строки с этими лексемами сдвигаются в первую позицию, подобно
директивам #define и #include. Поэтому для них устанавливается флаг
elndentlgnore.
class MyClass
<
public:
MyClass();
-MyClass() ;
protected:
bool blnitialized;
}
Разобравшись в том, как работает форматирование для кода C++, вы
сможете без труда писать форматеры и для других языков.
Главная программа
Приведенная ниже функция main() показывает, как могут применяться
различные подпрограммы управления исходным кодом. Она создает
представитель синтаксического анализатора и объект сканера, а затем вызывает
каждую из тех функций, которые мы обсуждали в этой главе, в зависимости от
того, какие ключи командной строки были указаны при запуске программы.
Код
Главная программа находится в файле SCodeMnt.cpp, показанном ниже.
// SCodeMnt.cpp
// Определяет входную точку консольного приложения.
///////////////////////////////////////////////////////////////////
//
// Причуды Microsoft.
//
#if _MSC_VER > 1000
// Отключить предупреждение С4786: symbol greater than 255 character,
// okay to ignore (MSVC)
#pragma warning(disable: 4786)
#endif // _MSC_VER > 1000
//
// Локальные включаемые файлы.
//
#include "CodeParser.h"
#include "CodeMaintCPP.h"
#include "Scanner.h"
318
Глава В
■ л„
' J// Библиотечные включаемые файлы.
//
'#include <iostream>
#include <ostream>
#include <set>
>#include <string>
J#include <vector>
г 'using namespace std;
' J'
* }//
. .// Различные константы.
//
* 7 #define HELPTEXT \
* | "Identifies which files are included in the specified file.\n" \
"\n" \
^. "ScodeMnt.exe SourceCode [Output] {[/?] | [/j] | [/ai] | [/с] " \
*. 4 "" "I [/html]}\n\n" \
L J "" \
^ "SourceCode " \
,,'ч "Identifies the file in which to look for the include files.\n" \
': "" \
* 1 "Output " \
1 "Specifies the name of the output file. Defaults to stdout.\n" \
*-,! "" \
. : V? - \
"■. ,1 "Shows this help screen.\n" \
.... x
4 "/j " \
"Removes the indents from a file. Used for testing the\n" \
[/format] switch.\n" \
\
'■/ai » \
"Automatically indents each line in SourceCode.\n" \
\
"/c " \
f \ "Copies SourceCode to OutputFile. Used for testing the\n" \
* Я " tokenizing code.\n" \
. J| "" \
'' v "/html " \
' ^ "Writes SourceCode out to OutputFile using HTML so\n" \
*-j " that each type of token appears in a" \
1 "" "different color.\n" \
i. '
4? \
V
■J
\
k ,j "/include Lists all the included files found " \
\ "" "in SourceCode.\n" \
■ ( »\n" \
, 1 "'• \
14 1 "NOTES: The command line is case sensitive. \n" \
If OutputFile is left blank all output " \
"will be sent to stdout.\n" \
I и к
\ "
,_, Jttdefine NO__INCLUDE_FILES "No include files found."
\ 4 " The InputFile and OutputFile can be the same.
1*defi
Управление исходным кодом
319
// Ключи командной строки,
«define SWITCH_HELP "?"
#define SWITCH_AUTOINDENT "ai"
#define SWITCH_INCLUDE "include"
#define SWITCH_HTML "html"
#define SWITCH_COPY "c"
«define SWITCH JUMBALIZE "j"
«define SWITCH CHARACTERS "/-"
«define BACKUP FILE EXTENSION ".bak1
//
// Сообщения об ошибках.
//
«define ERR_FILE_NOT_EXIST "ERROR: " \
"The file *%s' does not exist or is in use."
«define ERR_UNEXPECTED_ERROR "ERROR: " \
"An unexpected error occurred."
«define ERR_FILE_NOT_OPENED "ERROR: " \
"The file '%s' could not be opened for output
«define ERR_PARAMETERS_INCORRECT "ERROR: " \
"The program parameters are invalid.1'
//
// Объявления typedef.
//
// Тип для хранения набора аргументов,
typedef vector<string> ArgumentCol;
// Тип для хранения набора командных ключей,
typedef set<string> SwitchCol;
//
// Объявления функций.
//
■// Сохранить командную строку, чтобы можно было
// извлечь параметры позже,
void SaveCommandLine(
// Число параметров командной строки.
const int argc,
// Параметры командной строки.
char* argv[],
// Набор аргументов.
ArgumentCol& arguments,
// Набор командных ключей.
SwitchCol& switches);
// Печатает HELPTEXT на stdout.
void PrintHelpO;
320
Глава 8
int main(int argc, char* argv[])
{
//
// Объявления.
//
// Входной файловый поток.
CScanner SourceCode;
// Выходной файл,
ofstream OutputFile;
// Выходной поток, используемый всеми
// функциями обработки кода,
оstream* pOutputStream;
// Объект синтаксического анализатора C++.
CCodeParser parser;
// Хранит список включаемых файлов.
FileList files;
// Индикатор благополучного состояния,
bool bRet = false;
// Для вывода сообщений об ошибках,
char sErrorMsg[255];
// Набор аргументов.
ArgumentCol arguments;
// Набор ключей.
SwitchCol swi tches;
//
// Начало обработки.
//
// Сохранить информацию командной строки.
SaveCommandLine(argc, argv, arguments, switches);
' l
i
// Проверить, что указано имя файла.
// Первый аргумент - это имя, под которым
// запущена программа (SCodeMnt.exe);
// оно игнорируется.
if (arguments.size() > 1)
{
// Проверить, не совпадает ли имя выходного файла
// со входным. Если так, создать копию и сканировать ее.
if( (arguments.size() >= 2) && (arguments[l] == arguments[2]) )
{
// Изменить имя входного файла и
// назначить входной поток на новое имя.
arguments[l]+= BACKUP_FILE_EXTENSION;
Управление исходным кодом
321
// Удалить последнюю резервную копию;
// предупреждения не выдается,
remove(arguments[1].c_s tr()) ;
// Переименовать входной файл.
rename(arguments[2].c_str(), arguments[1].c_str{));
}
// Установить сканер на имя файла в командной строке.
// Попытаться открыть файл. Если это не удается, файл
// заблокирован или не существует.
SourсеСode.open(arguments[1].c_str()) ;
if ( !(SourceCode.good() && SourceCode.is_open()) )
<
// Файл не может быть открыт.
sprintf (sErrorMsg, ERR__FILE_NOT_EXIST, arguments [1] . c_str ()) ;
cerr « sErrorMsg « endl « endl;
bRet - false;
}
else
bRet - true;
)
else if ( switches.find(SWITCH JiELP)!=switches.end() )
<
PrintHelpO ;
bRet = false;
)
else if (arguments.size() <= 1)
i
cerr « ERR_PARAMETERS_INCORRECT « endl « endl;
// Входной файл не указан. Напечатать текст справки.
PrintHelpO;
bRet = false;
}
// Если пока все нормально...
if(bRet)
{
try
{
// Загрузить сканер определениями C++.
LoadCPPScanner(SourceCode);
// Проверить, указан ли выходной файл,
if(arguments.size() > 2)
{
// Удалить, если он уже существует.
// Заметьте, что предупреждения не выдается
remove(arguments[2].c_str());
// Открыть выходной поток.
0utputFile.open(arguments[2].c_str());
// Проверить, что все в порядке.
if ( !(OutputFile.good() S£ OutputFile.is_open()) )
{
// Открыть не удалось! Направить вывод в stdout.
pOutputStream — Scout;
// Сформатировать сообщение об ошибке,
sprintf(sErrorMsg, ERR_FILE_NOT_OPENED,
arguments[2].c_str());
// Вывести сообщение об ошибке,
cerr « sErrorMsg « endl « endl;
}
else
pOutputStream = SOutputFile;
)
else // Имя выходного файла не указано. Направить в stdout.
pOutputStream = Scout;
//
// Команда Include.
//
// Обработать команду include.
if( switches.find(SWITCH_INCLUDE)'«switches.end() )
{
GetlncludeFileList(SourceCode, files);
FileList::const_iterator i;
if (files.empty())
•pOutputStream « N0_INCLUDE_FILES « endl;
else
<
for(i = files.begin(); i ! = files.end(); i++)
{
♦pOutputStream « *i « endl;
)
) // end if (files.empty())
) // end if ( switches.find(SWITCH_INCLUDE)!=switches.end()
//
// Команда Copy.
//
// Копировать файл.
else if( switches.find(SWITCH_COPY)!=switches.end() )
bRet = parser.Copy(SourceCode, *pOutputStream);
//
// Команда Jumbalize.
//
// Удалить избыточные пробельные символы.
else if( switches.find(SWITCH_JUMBALIZE)!=switches.end() )
Управление исходным кодом 323
bRet = Jumbalize(SourceCode, *pOutputStream);
//
// Команда Autolndent.
//
// Форматировать код, добавив соотретстующие
// "правильные" пробелы.
else if( switches.find(SWITCH_AUTOINDENT)'^switches.end() )
{
LoadCFFFormat(parser);
bRet = parser.Autolndent(SourceCode, *pOutputStream);
L }
h
//
j, 1 // Команда HTML.
H //
\ // Записать html-вариант кода.
else if( switches.find(SWITCH_HTML)'«switches.end() )
bRet = parser.WriteHTML(SourceCode, *pOutputStream) ;
// Ключ "/?", недействителен или не указан,
else
i
PrintHelpO ;
bRet = false;
}
}
catch (...)
{
// Непредусмотренная ошибка.
cerr « ERR_UNEXPECTED_ERROR « endl « endl;
// Что бы там ни было, файл нужно закрыть.
bRet = false;
}
}
// end if (bRet)
if (SourceCode.is_open())
SourceCode.close();
if (OutputFile.is_open())
OutputFile.close();
return bRet;
void SaveCommandLine(
// Число параметров командной строки.
const int argc,
// Парамеры командной строки.
char* argv[],
// Набор аргументов.
ArgumentColfi arguments,
324
Глава 8
■i
// Набор ключей командной строки.
Switched& switches)
i
for(int i — 0; i < argc; i++)
{
if (strchr(SWITCH_CHARACTERS, argv[i][0])!=NULL)
// Первый символ является ключевым.
// Внести в набор ключей.
switches.insert(argv[i]+l);
else
// Это должен быть аргумент/ так как
// он начинается с символа ключа.
arguments.push_back(argv[i]);
■ Л return;
И*
j// Напечатать тенет справки по командной строке.
■void PrintHelpO
l{
' std::cout « HELPTEXT « std::endl;
)
J
1"™
ПРИМЕЧАНИЯ
Сначала main() вызывает SaveCommandLine(), чтобы сохранить параметры
командной строки и использовать их в дальнейшем. После этого она пытается
открыть входной файл. Установив, что в командной строке не ошибок,
программа загружает сканер определениями C++, вызывая LoadCPPScanner().
После возврата из этой функции делается проверка и определяется, куда будет
направлен выходной поток. В этой точке, собственно, и исполняется
введенная пользователем командная строка.
Заметьте, что весь код синтаксического разбора помещен в блок try...catch.
Таким образом перехватываются все ошибки ввода/вывода.
Перед завершением main() все открытые файлы закрываются.
Компиляция SCodeMnt.exe
Теперь, когда вы увидели все модули программы, настало время собрать их
все вместе. Короче говоря, компилировать и скомпоновать SCodeMnt.cpp,
Token.epp, Scanner.cpp и CodeParser.cpp. Например, если вы работаете с
Visual C++, следующая командная строка компилирует и соберет программу
обработки кода:
CL -GX SCodeMnt.cpp Token.epp Scanner.cpp CodeParser.cpp
Чтобы построить программу в интегрированной среде разработки Visual C++,|
просто загрузите проект SCodeMnt.dsw и постройте проект целиком. Имеется]
и make-файл с именем SCodeMnt.mak.
Управление исходным кодом 325
Запуск SCodeMnt.exe
В командной строке можно ввести
fecodeMnt /?
чтобы распечатать список параметров, воспринимаемых программой.
Синтаксис запуска программы следующий:
SCodeMnt SourceCode [OutputFile] {[/?] | [/j] | [/ai] | [/с] |
[/html] | [/include]}
Параметр
SourceCode
OutputFile
n
/\
/ai
/c
/html
/include
Описание
Указывает входной файл.
Указывает имя выходного файла. По умолчанию stdout.
Выводит справку.
Удаляет из файла все отступы. Используется для тестирования ключа /format.
Генерирует автоматические отступы для всех строк SourceCode.
Копирует SourceCode в OutputFile. Используется для тестирования
разбиения на лексемы.
Пишет SourceCode в OutputFile, преобразуя его в форму HTML, так что все
типы лексем будут показаны своим цветом.
Выводит список файлов, включенных в SourceCode.
Заметьте, что все ключи чувствительны к регистру. Кроме того, если
OutputFile опущен, весь вывод посылается в stdout. Наконец, вполне можно
указать одинаковые имена входного и выходного файлов. В этом случае файл
будет переписан.
Чем можно заняться
Программу управления исходным кодом, обсуждавшуюся в этой главе,
можно легко расширять и усовершенствовать. Вот некоторые соображения.
Надеемся, они высекут некую искру творческой мысли относительно
потенциала синтаксического анализатора исходного кода.
Можно раскрашивать и генерировать автоматически отступы для кода
других языков, определив дополнительные функции загрузки, сходные с Load-
ScannerCPP() и LoadFormatCPP(). Даже лучшим решением будет такое
изменение программы, чтобы она могла читать определения языка из файла. С
помощью такого метода определения языка могут загружаться во время
выполнения и будут управляться данными, вместо того чтобы приходилось писать
новый код для анализа каждого нового языка.
Другое возможное направление модификации — написание функции,
которая сохраняла бы в файле все уникальные лексемы, найденные в программном
файле. Такой файл лексем мог бы, например, использоваться в качестве
словаря текстового процессора.
И последнее предложение — создать базу данных для перекрестных
ссылок, показывающую, в каких строках встречается та или иная функция или
переменная.
#d ЬУ*
<s,
лО>
у
328
Глава 9
Сегодня, похоже, все устремились в Internet — кто ради бизнеса, кто
просто так, — даже те, кто вряд ли вообще знает что-то еще о компьютерах,
пытаются «залезать в паутину». Знание того, как лучше пользоваться
протоколами Web, является важным требованием, предъявляемым к вам как
разработчику — особенно если вы разрабатываете коммерческие приложения.
В основе коммуникации Internet лежат протоколы. Некоторые из
протоколов используются различными службами Internet, другие являются
специфическими. Однако на нижнем уровне все коммуникации в Internet
осуществляются через сокеты (sockets — гнезда), т. е. некоторый двусторонний механизм.
Эта глава посвящена работе этого механизма со стороны клиента, как в
принципиальной ее части, так и частностях доступа к удаленным объектам на сервере.
Для осуществления такой коммуникации мы напишем свой собственный
простой обозреватель (browser); при этом мы воспользуемся коммерческой
библиотекой, что упростит интегрирование сетевого сервиса в приложение.
Службы Internet
В сетях IP широко используются различные типы служб. Примерами могут
служить FTP, telnet, gopher, World Wide Web, archie, служба DNS и многие
другие. Протоколы, ими применяемые, определены в документах,
называемых Internet Request For Comment, или RFC.
Сами службы обычно доступны по известным номерам портов. Например,
для соединения с сервером telnet на любой главной машине Internet нужно
попытаться подключиться к порту 23 протокола Transport Control Protocol (TCP)
этой главной машины. Известные порты, распознаваемые машиной, обычно
идентифицируются в файле служб системы. Этот файл используют как UNIX,
так и в Windows.
Сокеты являются основанием коммуникации по Internet. Вы можете
представлять себе сокет как некий двунаправленный трубопровод между двумя
отдаленными друг от друга системами. В Windows интерфейс сокетов Беркли
реализуется посредством API WinSock, который позволяет создавать сокеты и
управлять ими. Имеются некоторые различия, зависящие от специфики
платформ реализаций, но модели этой главы образуют твердую основу для работы с
сокетами в любой среде. Имена функций могут немного отличаться от системы
к системе, но назначение их всегда одинаково.
WinSock API
«Интерфейс сокетов Беркли» может применяться в ориентированных на
соединение протоколах, таких, как TCP, равно как и в протоколах без соединения типа
UDP. Программной моделью интерфейса может служить модель клиент-сервер;
серверы ждут поступления запроса, клиенты инициируют сеансы обмена.
Может быть, наиболее значимым отличием реализаций сокетов Беркли в
WinSock и в UNIX является то, что дескрипторы сокетов и файлов не могут в
WinSock быть взаимозаменяемы, как это имеет место в UNIX, Это
оказывается весьма важным при переносе приложений, которые предполагают такую
эквивалентность.
C/C++ в Internet
329
Другим отличием является необходимость инициализации библиотеки Win-
Sock. Приложения, намеревающиеся вызывать функции WinSock, должны
предварительно вызвать WSAStartup(). Когда работа с библиотекой WinSock
окончена, для корректного завершения необходимо вызвать WSACleanup().
В API WinSock имеется, кроме того, ряд специфических функций для
асинхронного обмена через сокеты. Они полезны при разработке GUI-приложений
с хорошей реакцией.
Инициализация WinSock
В своем приложении вы инициализируете WinSock вызовом WSAStartup().
Вызывающее приложение предоставляет адрес структуры WSADATA, в
которой будет храниться инициализирующая информация.
В процессе вызова WSAStartupO выясняется соответствие версий
приложения и библиотеки WinSock. Если номера версий, поддерживаемые
библиотекой и вашим приложением, несовместимы, запрос на инициализацию
отклоняется. При ошибке WSAStartupO возвращает ненулевое значение.
Расширенную информацию о причинах ошибки приложение может получить от
функции WSAGetLastError().
Сокет создается вызовом функции socket(). Параметры ее указывают тип
сокета, тип адреса сети и используемый протокол. Например, так создается
сокет TCP:
socket(AF_INET, SOCK_STREAM, IPPROTOJTCP)
Для привязки сокета к действительному адресу машины и номеру порта
приложение обычно вызывает функцию bind(). В дополнение к
идентификатору, или дескриптору, гнезда (возвращаемому socket()) bind() принимает
параметр, являющийся указателем на структуру с описанием адреса сокета.
Структура эта определяется так:
struct sockaddr {
u_short sa_family;
char sa__data[14] ;
};
Элемент sa_family структуры указывает тип адреса. Для адресов Internet
это значение устанавливается равным AF_INET. Элемент sa_data содержит
действительный адрес.
Приложение может легко получить доступ к различным компонентам
адреса через структуру sockaddr_in (вместо sockaddr) Вот ее определение:
struct sockaddr_in {
short sin_family;
u__short sinjport;
struct in_addr sin_addr;
char sin_zero[8];
>;
В этой структуре sin_port является 16-битным номером порта, a sinaddr —
32-битным адресом машины.
330
Глава 9
Служба имен
Чтобы присвоить осмысленное значение полю адреса машины в структуре
sockaddr_in, приложение сначала должно получить это 32-битное значение.
Если известно символическое имя машины, можно вызвать функцию gethost-
byname().
При таком вызове приложение передает символическое имя и получает
указатель на структуру HOSTENT. Структура определяется следующим образом:
struct HOSTENT {
char FAR *h_name;
char FAR * FAR * h_aliases;
short h_addrtype;
short h__length;
char FAR * FAR * h_addr_list;
};
Эта структура необходима, так как имя машины может ассоциироваться с
различными адресами (верно и обратное). В большинстве случаев приложения
просто берут первый (и часто единственный) адрес в h_addr_list. Для простоты
h_addr_list[0] определяется как символ h_addr.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Хотя Visual C++ и другие компиляторы давно уже не делают различия
между длинными и короткими указателями, библиотека сокетов, тем
не менее, определяет структуру именно таким образом (это
первоначальное определение), поэтому мы оставили ее, как есть.
Если приложение хочет вместо этого воспользоваться численным адресом,
оно может вызвать функцию inet__addr(), чтобы преобразовать строку с
численным адресом в 32-битное его значение. Как только это значение получено,
можно вызвать gethostbyaddr(), возвращающую структуру HOSTENT
указанной машины.
Вопросы, касающиеся порядка байтов
Когда дело доходит до разработки приложений, которые предположительно
будут работать на гибридных сетях, встает важный вопрос относительно
порядка байтов. Некоторые архитектуры, например, семейство Motorola 68000,
являются, как говорят, big-endian (первым в слове идет старший байт). Другие
же, такие, как семейство процессоров Intel или DEC, называют little-endian
(первым идет младший байт).
Числа Internet (адреса машин, например) всегда big-endian. Чтобы
обеспечить корректное преобразование между машинно-независимыми числами
Internet и их машинно-зависимым представлением, можно воспользоваться
функциями из следующего набора: htonl(), htons(), ntohl() и ntohs(). Эти
функции преобразуют короткие или длинные целые из сетевого в машинный
формат и наоборот. В зависимости от реализации компилятора они могут быть
макросами.
C/C++ в Internet 331
Можно также обратиться к функциям WinSock API WSAHtonl(), WSAH-
tons(), WSAHtohl() и WSAHtonl(). При их использовании вы можете вызвать
WSANLastError(), чтобы определить возможную причину ошибки, если
таковая произошла.
Коммуникация через сокеты
В случае ориентированного на соединение протокола TCP
приложение-сервер связывается со специфическим портом TCP и затем посредством функции
listen() показывает свою готовность принять приходящие запросы на
соединение. Сразу после вызова listen() сервер вызывает accept(), переходя к
ожиданию приходящих запросов соединения. При возврате эта функция передает
адрес запрашивающего процесса.
Клиент, создав сокет вызовом socket(), может немедленно инициировать
соединение с помощью connect(). Нет необходимости предварительно
привязывать сокет к конкретному порту вызовом bind().
Если соединение успешно инициировано, и клиент, и сервер могут
передавать данные посредством send() или принимать их, вызывая recv(). Семантика
этих функций схожа с семантикой вызовов read() и write() при
низкоуровневом вводе-выводе. На самом деле на системах UNIX можно пользоваться для
обмена данными через сокеты последней парой функций.
К сожалению, как уже упоминалось, в WinSock это невозможно из-за
различий между дескриптором файла и дескриптором сокета. По той же причине
нельзя пользоваться системным вызовом close() для закрытия сокета;
приложения WinSock должны вызывать closesocket() для аннулирования
соединения. Эту функцию могут вызывать как клиент, так и сервер, закрывая сокет-
ное соединение. При ошибке она возвращает значение SOCKETJERROR.
Чтобы получить численное представление типа ошибки, можно обратиться к
функции WSANLastError().
В случае протокола без соединений UDP последовательность
происходящего несколько иная; действия клиента и сервера более симметричны. И клиент,
и сервер создают свои соответствующие сокеты и привязывают их к
конкретным номерам портов. Сервер затем вызывает recvfromQ, ожидающую
поступления данных. Клиент, в свою очередь, вызывает sendto(), чтобы послать
денные по конкретному адресу. Когда сервер получит эти данные, происходит
возврат из recvfrom(), причем серверу будет передан адрес, откуда они
пришли. Сервер использует этот адрес в последующих вызовах sendto() при
передаче данных клиенту.
Проблема блокировки и вызов select()
В упрощенных моделях из предыдущего раздела и клиент, и сервер при
ожидании данных используют блокирующие вызовы. Управление при
блокирующем вызове не возвращается в вызывающую программу, пока
запрошенные данные не станут доступны. Другими словами, сделавшее такой вызов
приложение приостанавливается до момента завершения вызова. Во многих
простых ситуациях это вполне допустимо, но, очевидно, совершенно
неприемлемо для интерактивных приложений. Такое приложение (например, клиент
telnet) не может просто «замереть», ожидая прихода данных от сервера.
332
Глава 9
Решение, к которому прибегает большинство приложений UNIX, работая с
протоколом TCP/IP, основано на системном вызове select(). Такой вызов
делает возможным ожидание на нескольких дескрипторах (файлов или сокетов).
Таким образом, приложение может просто ожидать данные и от сокета, и от
стандартного ввода, немедленно переходя к активным действиям при
поступлении данных от любого из этих источников.
Опять же с WinSock дело обстоит не так просто, поскольку дескрипторы
сокета и файла не взаимозаменяемы. К сожалению, select() здесь не
исключение; этот вызов может ожидать поступлений от нескольких дескрипторов
сокета, но не от «смеси* их с файловыми дескрипторами. Хотя возможно
следить за сокетом посредством его опроса, это не слишком удачное решение; но
положение спасают параллельные (мультилинейные) свойства Win32.
Процесс легко может инициировать дополнительные нити управления для
каждого из источников данных. Этот механизм хорошо работает как для утилит
командной строки, так и для графических приложений TCP/IP.
Тем не менее, библиотека WinSock предлагает вам другое семейство
функций, помогающих писать хорошие приложения TCP/IP без необходимости
прибегать к множественным нитям: это асинхронные вызовы сокетов,
которые обсуждаются в следующем разделе.
Асинхронные вызовы сокетов
Асинхронные вызовы сокетов опираются на механизмы передачи
сообщений системы Windows, который и осуществляет передачу приложению
событий сокета. В центре этого механизма находится вызов функции WSAAsync-
Select(). Благодаря этой функции приложение может ожидать поступления
событий от комбинации сокетов. Приложения могут получать уведомления о
готовности к чтению данных, записи, завершении соединения и закрытии
сокета. (Тот же самый вызов может использоваться для уведомлений,
касающихся несвязанных (out-of-band) данных, но этого вопроса мы здесь касаться
не будем.) Уведомление о событии происходит в форме определяемого
пользователем сообщения, которое отправляется окну, также указываемому в вызове
WSAAsyncSelect().
WSAAsyncSelect() отправляет одиночное сообщение для каждого события,
в котором заинтересовано приложение. Как только сообщение отправлено, для
того же самого события никаких сообщений посылаться не будет, пока
приложение неявным образом не сбросит событие, вызвав одну из функций
библиотеки сокетов. Например, если отправлено сообщение о приходе данных, для
того же сокета подобные сообщения отправляться больше не будут, пока
приложение не извлечет данные вызовом recv() или recvfrom().
В число других асинхронных функций входят, например, аналоги
стандартных вызовов Беркли gethostbyname() и gethostbyaddr(): WSAAsyncGet-
HostByName() и WSAAsyncGetHostByAddr(). Приложения WinSock могут
также повлиять на работу механизма блокировки стандартных вызовов в
стиле Беркли, воспользовавшись функцией WSASetBIockingHookQ.
C/C++ в Internet
333
Синхронные операции и сериализация
Назначением класса CAsyncSocket является обеспечение низкоуровнего
интерфейса с библиотекой WinSock. В противоположность этому класс CSoc-
ket предоставляет функциональные возможности более высокого уровня.
CSocket, в отличие от CAsyncSocket, предусматривает блокировку. Его
функции-элементы не возвращают управления, пока запрошенная операция
не будет завершена.
|ЗАГ,
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Для объектов класса CSocket никогда не используются
возвратно-вызываемые функции OnConnect() и OnSend().
Одним из примеров применения объектов CSocket (в комбинации с
объектами CFileSocket) является обеспечение работы сериализирующих функций MFC
с сокетами. Объект CFileSocket может быть соединен с объектом CArchive и с
объектом CSocket; после этого вы можете посылать и принимать данные
посредством простой сериализации MFC.
Проект простого обозревателя
Помня все вышесказанное и познакомившись к этому моменту с
некоторыми основами работы с сокетами в Windows, давайте перейдем к рассмотрению
простой программы-обозревателя. Хотя в некоторых отношениях такую
программу написать труднее, чем простой сервер, обозреватель, видимо, все-таки
полезнее. В следующем разделе приведен код классов документа и
представления (вида) этого простого обозревателя. После кода, в примечаниях, мы
обратимся к моментам реализации данных классов.
Код
Так как основной объем обработки данных в простом обозревателе
приходится на долю объектов документа и вида, в этой главе мы разберем только
реализацию классов CSimpBrowseDoc и CSimpBrowseView. Ниже вы можете
видеть код класса CSimpBrowseDoc.
i.
#include "stdafx.h"
♦include "SimpleBrowser.h"
#include "SimpBrowseDoc. h"
#include "openhttp. h"
jfifdef _DEBUG
ft define new DEBUG_NEW
#undef THIS_FILE
static char THIS FILE[] = FILE ;
jfendif
334
Глава 9
:i to
»* JIMPLEMENT_DYNCREATE(CSimpBrowseDoc, CDocument)
1 JBEGIN_MESSAGE_MAP(CSimpBrowseDoc, CDocument)
j //{{AFX_MSG_MAP(CSimpBrowseDoc)
ч i ON_COMMAND(ID__FILE_HTTP, OnFileHttp)
}AFX-MSG_MAP
ON_COMMAND(ID_FILE_SEND_MAIL, OnFileSendMail)
• 'I ON_UPDATE_COMMAND_UI(ID_FILE_SEND_MAIL, OnUpdateFileSendMail)
f ''END MESSAGE MAP()
■ jeSimpBrowseDoc::CSimpBrowseDoc() (}
CSimpBrowseDoc::-CSimpBrowseDoc() {}
BOOL CSimpBrowseDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
strcpy (real_file_name, "CANARCH.HTM");
strcpy (file_name, "CANARCH.HTM");
I OpenHttpFile (real_file_name);
return TRUE;
)
*" void CSimpBrowseDoc: :Serialize(CArchives ar)
{
if (ar.IsStoring()) {
// TODO: add storing code here
}
else (
// TODO: add loading code here
}
У
#ifdef _DEBUG
void CSimpBrowseDoc::AssertValid() const
{
CDocument: :AssertValid() ;
)
■
И
h
!
f
\ 1
Г '
Г 1
r
£
:U
I
V
void CSimpBrowseDoc::Dump(CDumpContext6 dc) const
CDocument::Dump(dc);
У
#endif //_DEBUG
BOOL CSimpBrowseDoc::OpenHttpFile (LPCTSTR IpszPathName,
LPCTSTR local_file_name)
{
strcpy (real_file_name, IpszPathName) ;
jj
if (strnicmp (real_file_name, "http://", 7) = 0) {
internet = InternetOpen ("Browser", INTERNET__OPEN_TYPE_DIRECT,
NULL, NULL, NULL);
C/C++ в Internet 335
*
к
if (internet) {
HINTERNET file_handle = InternetOpenUrl
J .'^ (internet, real_f ile^name,
NULL, 0, INTERNET_FLAG_RAW_DATA, 0);
if (file_handle) {
{ DWORD bytes_read = 0;
InternetReadFile (file_handle, buffer, 10000, &bytes_read);
i
l1'
r
\
г
к .
i
it
I
f
FILE* fp;
if ((fp = fopen (local_file__name, "wb")) != NULL) {
fwrite (buffer, bytes_read, 1, fp);
fclose (fp);
strcpy (file_name, local file_name) ;
}
else
return FALSE;
)
else
return FALSE;
)
else
return FALSE;
InternetCloseHandle (internet);
}
else {
if (!CDocument::OnOpenDocument(file_name))
return FALSE;
strcpy (file_name, real_file_name) ;
>
return TRUE;
void CSimpBrowseDoc::OnFileHttp()
{
OpenHTTP dialog_box;
if (dialog_box.DoModal{) = IDOK) {
OpenHttpFile (dialog_box.m_file_name);
UpdateAllViews (NULL);
}
Ш'л J _ _,„.,.
|ПРк
ПРИМЕЧАНИЯ
В архитектуре документ/вид библиотеки MFC ответственность за
поддержание устойчивых или полуустойчивых данных лежит на классе документа.
В простой программе обозревателя класс документа отслеживает информацию
об открытом файле HTML, в то время как класс вида управляет
действительным отображением класса в окне.
Большинство функций файла SimpBrowseDoc.cpp являются достаточно
очевидными, принимаемыми в MFC по умолчанию. Три функции, однако, вы-
336
Глава 9
полняют специализированную обработку файлов Internet; это OnNewDocu-
ment(), OpenHttpFile() и OnFileHttp(). Давайте разберем сначала реализацию
OnNewDocument().
В структуре MFC OnNewDocument() вызывается всякий раз, когда
программа создает новый объект документа. Код функции просто дает команду
открыть файл canarch.htm (находящийся на прилагаемом диске), когда
пользователь открывает новый документ. Этот файл напоминает документ по
умолчанию, когда вы открываете обозреватель, не указывая адрес URL. В функции
сначала производится проверка того, что программа в состоянии успешно
открыть документ, путем вызова OnNewDocument() базового класса CDocument:
BOOL CSimpBrowseDoc::OnNewDocument()
i
if (!CDocument::OnNewDocument())
return FALSE;
Затем программа копирует имя локального файла в поля real_file_name и
file_name. После этого вызывается функция OpenHttpFile(), о которой
рассказывается ниже, чтобы открыть и отобразить файл canarch.htm.
strcpy (real_file_name, "CANARCH.HTM");
strcpy <file_name, "CANARCH.HTM");
OpenHttpFile (real_file_name);
return TRUE;
}
Наконец, функция возвращает TRUE, давая программе знать, что
документ корректно конструирован и заполнен.
[ ФУНКЦИЯ OPENHTTPFILE0
Нет нужды говорить, что ключевые действия выполняются в функции Ореп-
HttpFile(). Она принимает в качестве параметра маршрут, который затем
анализируется на предмет того, относится ли он к локальному файлу или файлу
Internet. Это происходит в операторе if, где вызывается функция strnicmp() для
определения того, содержит ли маршрут идентификатор протокола http://.
BOOL CSimpBrowseDoc::OpenHttpFile (LPCTSTR IpszPathName,
LPCTSTR local_file_name)
i
strcpy (real_file_name, IpszPathName);
if (strnicmp (real_file_name, "http://", 7) = 0) {
Если маршрут является адресом Internet, программа с помощью функции
InternetOpen(), определяемой API, открывает соединение с Internet. Функция
принимает пять параметров. Ключевыми здесь являются имя агента — Simp-
Browse, соответствующее нашему приложению — и тип соединения Internet.
Второй параметр, специфицирующий тип соединения, может быть одной из
трех констант:
C/C++ в Internet
337
INTERNET_OPEN_TYPE_DIRECT
INTERNET_OPEN_TYPE_PROXY
INTERNET_OPEN_TYPE_PRECONFIG
Предлагает Internet API разрешить все имена
машины локально (метод, который мы здесь и
применяем).
Предлагает Internet API передать все запросы на
разрешение серверу-посреднику.
Предлагает компьютеру извлечь конфигурацию,
которую он должен использовать, из
Internet-пунктов реестра. В Windows эти пункты
можно вводить или модифицировать через
панель управления.
Ради простоты мы открываем в этой программе прямое соединение по
умолчанию. Следующие два параметра специфицируют информацию для
использования в среде сервера-посредника — имя сервера и список локальных адресов,
которые программа может передать серверу. Последний параметр позволяет
установить специфические флаги соединения; они в деталях объясняются в
справочнике по Win32 Internet API.
internet = InternetOpen ("Browser", INTERNET_OPEN_TYPE_DIRECT,
NULL, NULL, HULL);
Итак, теперь, если имя файла относится к файлу Internet, мы пытаемся
открыть соединение Internet. Мы делаем это в операторе if на случай, если по
какой-то причине программа не сможет открыть соединение. Если соединение
открыто успешно, программа определяет дескриптор типа HINTERNET и
инициализирует его вызовом функции InternetOpen Url().
if (internet) {
HINTERNET file_handle = InternetOpenUrl
(internet, real_file_name,
NULL, 0, INTERNET_FLAG_RAW_DATA, 0);
Если файл открыт успешно, функция возвращает действительный
дескриптор; в противном случае возвращается NULL. Параметры функции детально
описываются в справочнике по Win32 Internet API; мы только отметим
ключевые пункты. Первый параметр должен быть дескриптором открытого сеанса
Internet; второй параметр — строка, содержащая полностью
квалифицированное имя адреса URL, который требуется открыть; третий же и четвертый
параметры относятся к заголовкам, которые вы хотите послать серверу. Пятый
параметр говорит API, как возвращать данные — в данном случае мы
запрашиваем сырые, а не форматированные данные, — а шестой передает информацию
контекста, полезную для обработки определяемых программой
возвратно-вызываемых функций.
После инициализации дескриптора файла мы снова должны проверить, что
она прошла успешно. Если это так, код программы начинает считывать из
файла данные в буфер размером 10000 символов с помощью функции API In-
ternetReadFile(), как показано ниже:
if (filejiandle) {
DWORD bytes_read = 0;
InternetReadFile (file_handle, buffer, 10000, &bytes_read);
338
Глава 9
ЗАМЕЧАНИЕ ПРОГРАММИСТА
В реальной среде вы, вероятно, захотите читать значительно большие
файлы — хотя маловероятно, что страница Web будет иметь размер
более 10000 символов, если только она не содержит код сценария.
Затем мы должны создать локальный временный файл, в котором будит
храниться данные, полученные из Internet. Мы получаем и сохраняем
информацию файла, а затем анализируем и отображаем ее позднее в классе вида.
Снова, в целях безопасности, мы проверяем, успешно ли создан файл (в
двоичном формате только для записи), внутри оператора if:
FILE* fp;
if ((fp « fopen (local_file_name, "wb")) != NULL) {
Вели файл создан успешно, программа записывает в него все символы из
буфера и закрывает его. Наконец, она копирует имя файла в переменную
filename, которая будет использоваться там, где программа производит
синтаксический разбор содержимого файла (в классе вида):
fwrite (buffer, bytes_read, 1, fp) ;
fclose (fp);
strcpy (file_name, local_file_name);
Как вы уже видели, управление «провалится» во внутренние выражения
else, если произойдет какая-то ошибка в обращении к удаленному файлу.
В этом случае функция возвращает FALSE, показывая, что файл открыть не
удалось:
else
return FALSE;
)
else
return FALSE;
)
else
return FALSE;
В любом случае программа должна закрыть дескриптор HINTERNET
прежде, чем произойдет выход из функции, что и делается вызовом InternetClose-
Handle():
InternetCloseHandle (internet);
}
ЗАМЕЧАНИЕ ПРОГРАММИСТА
[ ЗА*
Код, как он написан здесь, для проверки на ошибку применяет
операторы if, но вы можете, конечно, использовать вместо этого блоки
try...catch, что потребует очень незначительных изменений, и,
вероятно, такой код в реальных условиях будет работать лучше благодаря
более полному контролю над состояниями ошибки.
C/C++ в Internet 339
Если вы вернетесь немного назад, то увидите, что следующий блок else
исполняется в случае, когда программа опознает файл как локальный, а не
удаленный, Тогда для его открытия вызывается функция OnOpenDocument()
родительского класса и снова возвращается FALSE, если что-то идет не так.
Когда файл открыт, программный код копирует имя локального файла в
переменную file_name:
else {
if (tCDocument::OnOpenDocument(file_name))
return FALSE;
strcpy (file_name, геа1_^11е_пате) ;
}
return TRUE;
}
|фУ1
ФУНКЦИЯ ONFILEHTTPO
Функция OnFileHttpO вызывается всякий раз, когда пользователь
выбирает команду Open в меню File главного окна программы. Функция выводит
панель диалога с идентификатором ресурса IDD_HTTP_FILE (оболочкой
которой является класс ОрепНТТР). Панель диалога содержит просто текстовое
окно, в котором пользователь может ввести открываемый URL, а также
кнопки ОК и Cancel, что иллюстрирует рис. 9.1.
.'.!-'л -дй-tr. £**/.: JE*---.* -*- \\-4-v Л:Л
Рис. 9.1. Панель диалога, в которой пользователь вводит открываемый URL
340
Глава 9
Для простоты мы открываем диалог как модальный:
void CSimpBrowseDoc::OnFileHttp()
{
OpenHTTP dialog_box;
if (dialog_box.DoModal() -= IDOK) {
Если пользователь нажимает OK, программа извлекает URL, введенный
пользователем в текстовом окне. Программа передает значение строки m_fi-
le__name функции OpenHttpFile(), которая открывает и читает файл, что вы
видели выше. Затем программа вызывает функцию UpdateAllViews(),
которая заставляет все виды документов перерисовать себя. (Далее вы увидите, что
происходит при перерисовке.)
OpenHttpFile (dialog_box.m_file_name);
UpdateAllViews (NULL)";
}
}
Итак, как вы видели, документ поддерживает информацию о текущем
открытом файле; и он содержит код для извлечения запрошенного
пользователем файла, находящегося либо в Internet, либо на локальном компьютере.
Отображением файла, однако, ведает код в модуле SimpBrowseView.cpp. Этот
файл содержит весь код для рисования вида, а также код синтаксического
разбора файла HTML. В следующих разделах представлен этот код и примечания
к каждой из специализированных функций модуля.
Код
!// SimpBrowseView.cpp : implementation of the CSimpBrowseView class
//
I#include "stdafx.h"
I#include "SimpleBrowser.h"
j#include "SimpBrowseDoc.h"
I #include "SimpBrowseView.h"
#ifdef __DEBUG
|#define new DEBUG_NEW
Uundef THIS_FILE
static char~THTS_FILE H = FILE ;
#endi£
IMPLEMENT_DYNCREATE (CSimpBrowseView, CScrollView)
jBEGIN_MESSAGE_MAP (CSimpBrowseView, CScrollView)
// ((APX_MSG_MAP (CSimpBrowseView)
ON_WM_LBUTTONDOWN()
ON_WM_MOUSEMOVE()
//}}AFX_MSG_MAP
// Standard printing commands
ON_CQMMAND(ID__FILE_PRINT, CScrollView: :OnFilePrint)
ON_C0MMAND(ID_FILE_PRINT_DIRECT/ CScrollView: :OnFilePrint)
C/C++ в Internet 34±
III ON_COMMAND (ID_FILE_PRINT_PREVIEW,
CScrollView::OnFilePrintPreview)
END_MESSAGE_MAP ()
CSimpBrowseView: :CSimpBrowseView()
<
frame_window = NULL;
printing_document = FALSE;
for (int i = 0; i < MAX_HREFS; i++)
href[i].clicked = FALSE;
JV
/"J
I*
}
CSimpBrowseView: : "-CSimpBrowseView () { }
BOOL CSimpBrowseView: :PreCreateWindow(CREATESTRUCT& cs)
return CScrollView::PreCreateWindow(cs);
" ]}
void CSimpBrowseView::0nDraw(CDC* pDC)
t
CSimpBrowseDoc* pDoc - GetDocument();
ASSERT_VALID(pDoc);
parser(pDoc, pDC);
}
void CSimpBrowseView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
// TODO: calculate the total size of this view
sizeTotal.ex - sizeTotal.cy = 100;
SetScrollSizes (MMJTEXT, sizeTotal) ;
}
BOOL CSimpBrowseView::OnPreparePrinting(CPrintlnfo* plnfo)
// default preparation
return DoPreparePrinting(plnfo);
void CSimpBrowseView::OnBeginPrinting(CDC* /*pDC*/, CPrintlnfo*
/*plnfo*/)
(
printing_document = TRUE;
}
void CSimpBrowseView::OnEndPrinting(CDC* /*pDC*/, CPrintlnfo*
/*pInfo*/)
{
printing document = FALSE;
342
Глава 9
г —
#ifdef JDEBUG
"t void CSimpBrowseView: :AssertValid() const
{
CScr oil View: :AssertValid() ;
i>
j Jvoid CSimpBrowseView: :Dump (CDumpContexts dc) const
'<
CScrollView: :Dump(dc) ;
}
CSimpBrowseDoc* CSimpBrowseView: :GetDocument ()
// non-debug version is inline
{
I ASSERT (mjpDocument->IsKindOf (RUNTIME_CLASS (CSimpBrowseDoc))) ;
i return (CSimpBrowseDoc*)m_jpDocument;
!>
#endif //JDEBUG
void CSimpBrowseView: :parser (CSimpBrowseDoc* pDoc, CDC* pDC)
{
dc = pDC;
if (!printing_document)
{
CWnd* window = dc->GetWindow();
RECT rect;
window->GetClientRect (&rect);
frame_window = window->6etParentFrame();
width - (int) rect.right;
height = (int) rect.bottom;
>
\
COLORREF old_color = dc->SetTextColor(RGB(0,0,0));
current_font = new CFont;
current_font->CreatePointFont (120, "Times New Roman");
xjbegin - X_BEGIN;
y_begin = Y_BEGIN;
J y_increment = Y_INCREMENT ;
' x = x_begin;
i i У - y_.be.gin;
! i y_top » y_begin;
y_bottom - y_top + y_increment;
href_index = 0;
parse(pDoc->file_name) ;
dc->SetTextColor (old_color);
CSize x (width, y+30);
SetScrollSizes (MM_TEXT, x);
1}
jvoid CSimpBrowseView::parse(char* filename)
{
FILE* fp;
l-
C/C++ в Internet
343
if ((fp = fopen(filename, "r")) == NULL) {
string.Format ("Unable to open <%s>", filename);
dc->TextOut (0, 0, string);
return;
}
title = FALSE;
center = FALSE;
int done = 0;
while ('done) (
done = get_tag (fp, tag);
if (done)
break;
if (stricmp (tag, "TITLE") — 0) {
title - TRUE;
string.Empty();
}
else if (stricmp (tag, "/TITLE") == 0) {
title = FALSE;
if (frame_window)
frame_window->SetWindowText (string);
string.Empty();
)
else if (stricmp (tag, "HR") == 0) {
у += y_increment + y_increment/2;
dc->MoveTo (x_begin, y);
dc->LineTo (width - X_BEGIN*2, y);
x = x_begin;
У += y_increment - y_increment/2;
Y_top = y;
y_bottom = y_top + y__increment;
}
else if (stricmp (tag, "BR") = 0 || stricmp (tag, "P") == 0) (
print_string();
string.Empty();
}
else if (stricmp (tag, "CENTER") =- 0) {
center = TRUE;
string.Empty();
)
else if (stricmp (tag, "/CENTER") =- 0) {
center = FALSE;
x = (width - (dc->GetTextExtent (string)).ex) / 2;
print_characters (string);
x = xjbegin;
у += y_increment;
y_top = y;
y_bottom = y_top + y_increment;
}
else if (stricmp (tag, "B") ~= 0) (
bold_on ();
}
else if (stricmp (tag, "/B") = 0) {
bold_off ();
}
else if (stricmp (tag, "I") == 0) {
italic_on ();
)
else if (stricmp (tag, "/I") = 0) {
italie_off ();
}
else if (stricmp (tag, "U") — 0) {
underline_on ();
>
else if (stricmp (tag, "/U") == 0) {
underline_off ();
)
else if (strnicmp (tag, "A ", 2) = 0) (
href_on() ;
}
else if (stricmp (tag, "/A") — 0) {
href_off();
)
else if (stricmp (tag, "PRE") = 0) (
preformatted_on ();
}
else if (stricmp (tag, "/PRE") — 0) {
preformatted_off ();
}
else if (stricmp (tag, "UL") == 0) {
unordered_list_on ();
}
else if (stricmp (tag, "/OL") — 0) {
unordered_list_off ();
>
else if (stricmp (tag, "OL") ===== 0) (
ordered_list_on ();
)
else if (stricmp (tag, "/OL") = 0) {
ordered_list_off ();
}
else if (stricmp (tag, "LI") = 0) {
insert^list^temO ;
}
else if (strnicmp (tag, "IMG ", 4) == 0)
display_image();
)
else if (stricmp (tag, "HI") — 0) {
set_headl();
}
else if (stricmp (tag, "H2") == 0) {
set_head2() ;
}
else if (stricmp (tag, "/HI") = 0 ||
stricmp (tag, "/H2") = 0) {
set_normal();
)
else {
print—characters ("<");
C/C++ в Internet 345
print_characters (tag);
print_characters (">");
)
tag [0] = '\0';
>
fclose(fp);
* -
I
i\
int CSimpBrowseView: :get_tag(FILE* fp, char* tag)
{
int c;
while ((c = fgetc(fp)) !« EOF) {
if (c — '<') {
int i = 0;
i \ while ((c = fgetc(fp)) != '>') {
if (i < TAG_LENGTH)
tag[i++] = c;
}
'- j tag[i] = '\0';
return(O);
}
' else
process_info(c);
}
\*
* ■
(■•
J
\
t,
if (c — EOF)
return(1);
else
return(0);
void CSimpBrowseView::process_info (int c)
if (c == '\n') (
i
\r ' if (center) {
print_string();
i, . return;
}
else if (x != x_begin)
с - '
else
return;
}
f
if (title || center) {
string += c;
return;
}
print__character (c);
}
void CSimpBrowseView::print_characters(CString characters)
346
Глава 9
{
int length = characters.GetLength{);
for (int i = 0; i < length; i++)
print_character (characters.GetAt (i));
}
void CSimpBrowseView: : print__character (char c)
{
char s[2];
stOJ • c;
s[l] = *\0';
CString character(s);
CFont* old_font = dc->SelectObject (current_font);
dc->TextOut (x, y, character);
x +- (dc->GetTextExtent(character)).ex;
dc->SelectObject (old_font);
if (x > (width-X_BEGIN*2)) {
x = x_begin;
у +~ y_increment;
y_top = y;
у bottom = y_top + y_increment;
}
}
void CSimpBrowseView::bold_on{)
{
LOGFONT If;
current_font->GetLogFont (filf);
lf.lfWeight ~ 700;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
)
void CSimpBrowseView::bold_off()
{
LOGFONT If;
current__font->GetLogFont (filf) ;
lf.lfWeight = 400;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
}
void CSimpBrowseView::italic on()
(
LOGFONT If;
current_font->GetLogFont (filf);
If.IfItalic = TRUE;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
* i
C/C++ в Internet 347
void CSimpBrowseView: :italic_off ()
i
LOGFONT If;
current_font->GetLogFont (elf);
If.IfItalic = FALSE;
delete current_font;
current_font — new CFont;
( current_font->CreateFontIndirect (filf);
void CSimpBrowseView::underline_on()
{
LOGFONT If;
current__font->GetLogFont (filf) ;
If.IfUnderline = TRUE;
delete current_font;
1 I current_font = new CFont;
£ | current font->CreateFontIndirect (filf);
: :>
I void CSimpBrowseView: :underline_off ()
i l(
,. j LOGFONT If;
current_font->GetLogFont (filf);
If.IfUnderline ■ FALSE;
delete current_font;
current_font — new CFont;
current_font-x:reateFontIndirect (filf);
}
void CSimpBrowseView::href_on()
{
underline_on ();
if (href [href_index].clicked)
dc->SetTextColor (RGB (255, 0, 0));
else
dc->SetTextColor (RGB (0, 0, 255));
char* pi = strchr (tag, '"•) + 1;
char* p2 = strchr (pi, "" );
int length = (int) (p2 - pi);
href [href_index].rect.left = x;
href [href_index].rect.top = y;
href [href_index].ref.Format (M%*.*s", length, length, pi);
}
void CSimpBrowseView: :href_off()
{
underline_off ();
dc->SetTextColor (RGB (0, 0, 0));
href [href_index].rect.right = x;
href [href_index].rect.bottom = у + 20;
if (href_index < MAX_HREFS-1)
href index++;
348 Глава 9
void CSimpBrowseView: :preformatted_on()
h r
I*
V
' i
I
\ ■
I
I
i
■ . 1
\л
c;
{
delete current^font;
current_font = new CFont;
current_font->CreatePointFont (100, "Courier New");
)
void CSimpBrowseView::preformatted_off()
<
delete current_font;
currant_font = new CFont;
current_font->CreatePointFont (120, "Times New Roman");
}
void CSimpBrowseView: :unordered_list_on()
{
ordered_list = FALSE;
x_begin = X_BEGIN + 40;
x = x_begin;
if (string.GetLength() > 0) {
print_string();
string.Empty();
)
print_string();
}
void CSimpBrowseView: :unordered__list_off ()
i
x_begin = X_BEGIN;
print_string();
string.Empty();
print_string();
}
void CSimpBrowseView: :ordered_list_on()
{
ordered_list = TRUE;
list_item = 0;
x_begin = X_BEGIN + 40;
x = x_begin;
if (string.GetLength() > 0) {
print_string();
string.Empty();
}
print_string();
}
void CSimpBrowseView: :orderedjlist_off ()
(
ordered_list = FALSE;
xjaegin = X_BEGIN;
print_string();
string.Empty();
print_string();
C/C++ в Internet
349
.void CSimpBrowseView: :insert__list_item()
5 <
■ ' print_string() ;
1 string.Empty();
• if (ordered_list) {
" J list_item++;
CString characters;
•* ' characters.Format ("%d.", list_item);
w x - 20;
,, i print characters (characters) ;
M }
у j else {
' I CBrush brush (RGB (0, 0, 0));
, i CBrush* old_brush = dc->SelectObject (£brush);
i \ dc->Ellipse (x-10, y+6, x-4, y+12);
\ \ dc->SelectObject (old_brush);
: i }
x = x_begin;
Л
(void CSimpBrowseView::display_image()
'<
char* pi = strchr (tag, "") + 1;
J J char* p2 = strchr (pi, "");
I j int length ■ (int) (p2 - pi);
char filename[100];
i strncpy (filename, pi, length);
filename [length] = '\0';
WORD image_width, image_height;
FILE* fp;
if ((fp - fopen (filename, "r")) != NULL) {
fseek (fp, 6L, SEEK_SET);
fread (&image_width,2,1, fp);
fread (fiimage_height, 2, 1, fp);
fclose (fp);
}
1 ; else {
, | char old__file_name [100];
■ char old_real_file_name [100];
I char image_file [100];
J i CSimpBrowseDoc* pDoc = GetDocument();
| strcpy (old__real__file_name, pDoc->real_file_name) ;
* I strcpy(old_file_name, pDoc->file_name);
if (strnicmp (filename, "http://", 7) — 0)
strcpy(image_file, filename);
else (
strcpy (image_f ile, old_real_f ile_name) ;
strcat(image_file, filename);
}
if (pDoc-X)penHttpFile (image_file, "IMAGE.FIL") £&
j (fp = fopen ("IMAGE.FIL", "r")) != NULL) [
fseek (fp, 6L, SEEK_SET);
350
Глава 9
\i.* fread (&image_width, 2, 1, fp) ;
fread (£image_height,2,1, fp) ;
fclose (fp);
s.«J if (image_width > 1000 | | image_height > 1000) {
image width = 100;
«V :* image height = 100;
:$ }
-J }
else {
image_width = 100;
*. <| image_height = 100;
и '.
strcpy (pDoc->real_f ile_name, old_real_file_name) ;
."] strcpy (pDoc->f ile_name, old_file_name) ;
f' •>* У = y_toP'
:| dc->MoveTo (x, y);
\ i dc->LineTo (x + image_width - 1, y);
-I dc->LineTo (x + image_width - 1, у + image_height - 1);
', j dc->LineTo (x, у + image_height - 1);
. "j dc->LineTo (x, y) ;
; j y_top = y;
j 1 yjbottom = y_top + image_height;
! x += image_width;
( ,| у = у + image__height - y_increment;
/J}
П
. ivoid CSimpBrowseView: :set__headl ()
{
delete current_font;
current_font — new CFont;
current_font->CreatePointFont (240, "Times New Roman");
bold_on ();
if (x != x_begin) {
у += y_increment;
x = x_begin;
y_top = y;
y_bottom = y_top + y_increment;
}
y_increment ss Y_INCREMENT_H1;
)
f;
7
1 I
|r void CSimpBrowseView: :set_head2()
\Л | delete current_font;
current_font = new CFont;
current_font->CreatePointFont (180, "Times New Roman");
bold__on () ;
if (x != x__begin) {
j^.jJ у +— y_increment;
Y ■ x = xjbegin;
j» y_top = y;
"' y_bottom = y_top + y_increment;
*-j }
11
C/C++ в Internet
351
Л
y_increment = Y_INCREMENT_H2;
\
i
void CSimpBrowseView: :set__normal ()
.{
'[ print_string() ;
, delete current_font;
| current_font = new CFont;
j current_font->CreatePointFont (120, "Times New Roman");
bold_off ();
if (x != xjbegin) {
У +ss y_increment;
x ■ xjbegin;
y_top = y;
y_bottom = y__top + у increment;
}
y_increment ■ Y_INCREMENT;
}
void CSimpBrowseView: :print_jstring()
if (center)
f i x = (width - (dc->GetTextExtent (string)).ex) / 2;
■ i
v print_characters (string);
t string.Empty();
i
11 x = x_begin;
, I y += y_increment;
» I y_toP " У'*
, yjbottom = y_top + y_increment;
' 0
void CSimpBrowseView::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC dc(this);
OnPrepareDC(Sdc);
~ dc.DPtoLP(Spoint);
for (int i = 0; i < href_index; i++) {
if (point.x >= href [i].rect.left &&
point.x <= href [ij.rect.right &&
point.у >= href [i].rect.top &&
point.у <= href [i].rect.bottom) {
href [i].clicked = TRUE;
CSimpBrowseDoc* pDoc = GetDocument();
if (strnicmp (href [i].ref, "http://", 7) == 0)
pDoc->OpenHttpFile (href [i].ref);
else if (strnicmp (pDoc->real_file_name, "http://", 7) == 0)
strcat (pDoc->real_file_name, href [ij.ref);
pDoc->OpenHttpFile (pDoc->real_file_name);
}
else {
strcpy (pDoc->real_file_name, href [i] .ref) ;
352
Глава 9
J
pDoc-XDpenHttpFile (pDoc->real_£ile__name) ;
)
RedrawWindow() ;
break;
)
* }
I)
-void CSimpBrowseView: :OnMouseMove(UINT nFlags, CPoint point)
if (frame__window) {
.] CClientDC dc(this);
i OnPrepareDC(£dc);
dc.DPtoLP(fipoint);
BOOL message_not_set = TRUE;
•* for (int i = 0; i < href_index; i++) {
J J if (point.x >= href [i].rect.left &&
point.x <= href [i].rect.right &£
** point.у >- href [i].rect.top £&
\ point.у <= href [i].rect.bottom) {
f'- frame_window->SetMessageText (href [i].ref);
'." message_not_set = FALSE;
* ■■ break;
;i >
-'.'. >
. ,i if (message_not_set)
j, ■, frame_window->SetMessageText ("") ;
; >
1 СScrollView::OnMouseMove(nFlags, point);
| T\PV
ПРИМЕЧАНИЯ
He стоит и говорить, что в классе вида происходит масса всего —
программирование анализа синтаксиса и отображение HTML-файла не столь сложно,
сколько утомительно. Программа обозревателя, которую мы здесь
приводим — а это более 1500 строк кода, — чрезвычайно проста по своей
реализации; действительно, она даже не отображает графику, а только заменяющие ее
прямоугольники.
Программа не выводит графику, потому что большая часть графики в Web
существует в Формате обмена графикой (GIF) или формате Объединенной
группы экспертов по фотографии (JPEG). Ни один из них Windows
непосредственно не поддерживает, поэтому вам на самом деле пришлось бы
преобразовывать графику в битовые матрицы. Такое преобразование страшно сложно, и в
Сети имеется масса исходного кода, имеющего дело со стандартами сжатия и
преобразованиями форматов, но эти программы велики. Самый короткий
общедоступный конвертер, который можно найти — это еще 3200 строк кода.
Однако, в нашей программе имеется заглушка — если хотите, можете с
легкостью вставить туда этот код.
C/C++ в Internet 353
Итак, давайте рассмотрим код в файле вида. Мы начнем с конструктора
класса CSimpBrowseView, где мы устанавливаем пару элементов данных,
равных соответственно NULL и FALSE.
CSimpBrowseView: : CSimpBrowseView ()
<
frame_window = NULL;
printing_document = FALSE;
Затем мы проходим по массиву структур href_struct. Эта структура
содержит информацию о гиперссылках документа и определяется в файле CSimp-
BrowseView.h следующим образом:
struct href_struct {
CRect rect;
BOOL clicked;
CString ref;
} href [MAX__HREFS] ;
Как можно видеть, структура содержит прямоугольник rect, в котором
хранится информация о положении гиперссылки на экране; переменную clicked,
которую программа будет использовать для установки цвета гиперссылки; и
CString, которая будет содержать указываемый гиперссылкой URL. Проходя
по массиву, мы устанавливаем clicked равной FALSE для всех его элементов
(значение по умалчанию):
for (int i = 0; i < MAX_HREFS; i++)
href[i].clicked = FALSE;
}
Следующей важной функцией является OnDraw(), управляющая
рисованием содержимого на экране. Первые два оператора являются стандартными и
извлекают указатель на объект документа:
void CSimpBrowseView::OnDraw(CDC* pDC)
{
CSintpBrowseDoc* pDoc = GetDocument () ;
ASSERT_VALID(pDoc);
Последний оператор вызывает функцию parser(), передавая ей ссылку на
документ и контекст устройства. Как вы увидите далее, программный код этой
функции выполняет синтаксически разбор файла HTML, изолируя и
обрабатывая теги документа и соответствующим образом меняя информацию,
отображаемую в окне.
parser(pDoc, pDC);
)
Как вы помните, OnDraw() вызывается всякий раз, когда пользователь
изменяет вид, изменяет размер окна и т. д. Важно каждый раз при этом заново
производить разбор файла, в первую очередь потому, что необходимо
поддерживать точное состояние информации, содержащейся в структуре rect,
ассоциированной с каждой гиперссылкой.
Следующая функция — это OnInitiaIUpdate(); она позволяет определить
общий размер вида, настраивает его и сообщает линейкам прокрутки, каким
должен быть их размер:
12 Зак. 1308
354
Глава 9
void CSimpBrowseView: :OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
sizeTotal.cx = sizeTotal.cy = 100;
SetScrollSizes(MM_TEXT, sizeTotal);
}
В показанных далее функциях OnBeginPrintingO и OnEndPrinting()
программный код просто управляет содержимым элемента данных printingdocu-
ment, устанавливая ее равной TRUE, когда пользователь запускает печать, и
сбрасывая ее, когда программа завершает вывод.
void CSimpBrowseView::OnBeginPrinting(CDC* /*pDC*/, CPrintlnfo*
/*pInfo*/)
{
printing_docnment я TRUE;
)
void CSimpBrowseView::OnEndPrinting(CDC* /*pDC*/, CPrintlnfo*
/*pInfo*/)
{
printing_document = FALSE;
}
1 ФУНКЦИИ PARSER() И PARSE()
Следующей важной функцией является parser(), которая получает размер
окна, устанавливает шрифт и вызывает parse(). Прежде всего код проверяет,
что пользователь не печатает в данный момент документ, и если это так,
определяет размер окна. Сначала вызывается функция GetWindow() и ссылка на
окно помещается в локальную переменную window:
void CSimpBrowseView: :parser (CSimpBrowseDoc* pDoc, CDC* pDC)
{
dc = pDC;
if (!printing_document) {
CWnd* window = dc->GetWindow();
Затем программа создает структуру RECT и получает прямоугольник
клиента в окне, записывая полученные значения в эту структуру. Наконец,
функция получает дескриптор окна обрамления, помещая его в поле frame_window
класса:
RECT rect;
window->GetClientRect (Srect);
frame_window = window->GetParentFrame();
Когда вся информация получена, функция устанавливает значения width и
height равными ширине и высоте прямоугольника клиента, сохраняя их для
упрощения последующего доступа:
width = (int) rect.right;
height = (int) rect.bottom;
C/C++ в Internet 355
После этого программа сохраняет текущий цвет пера в структуре old_color
и создает объект current_font для хранения информации о текущем экранном
шрифте. Эта информация будет использоваться в дальнейшем для
восстановления состояния дисплея при вызове функции parse().
COLORREF old_color = dc->SetTextColor(RGB(0,0,0));
current_font - new CFont;
current_font->CreatePointFont (120, "Times New Roman");
Теперь мы установим значения некоторых элементов данных равными
константам, определенным в заголовочном файле. Мы используем здесь
переменные, поскольку значения, такие, как приращения, позже могут меняться в
зависимости от размера текста, который мы отображаем:
xjbegin = XJ3EGIN;
y_begin = Y_BEGIN;
y_increment = Y_INCREMENT;
Затем устанавливаются начальные значения нескольких переменных
класса, связанных с отображением информации. И наконец, мы устанавливаем
значение href_index, ссылающуюся на текущую гиперссылку массива; ей
присваивается 0, и она, таким образом, указывает на его начало:
х = x__begin;
У ~ y_begin;
y_top = y_begin;
y__bottom = y_top + y_increment ;
href_index = 0;
Когда все это сделано и инициализация завершена, программа вызывает
функцию parse(), передавая последней имя файла из объекта документа:
parse(pDoc->file_name);
По возврате из parse() файл уже отображается — так что нам остается
сделать только некоторую приборку. Программа восстанавливает цвет пера для
текста, затем создает объект CSize, соответствующий текущему размеру
документа. Этот объект используется для правильной установки линеек
прокрутки:
dc->SetTextColor (old_color);
CSize x (width, y+30);
SetScrollSizes (MM_TEXT, x);
}
После возврата из parserQ экран нарисован (или файл распечатан). Однако
код, который в действительности анализирует и отображает файл, содержится
в функции parse(). Она открывает файл и проходит по всем его тегами,
отображая текст и заглушки, выполняя форматирование. Когда обработка
завершена, экранный образ страницы полностью прорисован. Функция parse()
начинает с открытия локального временного файла, имя которого она получает в
параметре filename:
void CSimpBrowseView::parse(char* filename)
t
FILE* fp;
if ((fp = fopen(filename, "r")) == NULL) {
string.Format ("Unable to open <%s>", filename);
12*
356
Глава 9
dc->TextOut (0, О, string);
return;
)
Как обычно, мы делаем быструю проверку, чтобы убедиться в успешном
открытии файла, выдавая пользователю предупреждение в случае, если
операция не удалась, и завершая функцию. Однако если файл открыт успешно, мы
можем перейти к его синтаксическому разбору и отображению.
Мы начинаем с установки переменных title и center, которые будут
использоваться для отслеживания специфической информации для форматирования,
равными FALSE. Наконец, мы инициализируем переменную done,
сигнализирующую об окончании анализа, значением 0; после этого мы входим в цикл
while, повторяющийся до тех пор, пока done не станет равна TRUE.
title = FALSE;
center = FALSE;
int done = 0;
while (!done) {
Первым шагом в цикле является вызов get_tag(). Функция читает файл в
поисках тега (которым считается что-либо заключенное между < и >). Если
функция находит тег, она возвращает 0, если нет — 1.
done = get_tag (fp, tag) ;
if (done)
break;
Когда get_tag() возвращает управление, в tag содержится строковое
значение, представляющее символы между < и >. Внутри функции get_tag(),
кстати, происходит вывод каждого встреченного ею символа, не являющегося <.
Когда функция возвращает значение tag, программа просто определяет в
последовательности операторов if...else if, что означает данный тег. Первый
оператор if проверяет, не соответствует ли его значение TITLE, и в этом случае
код устанавливает переменную title равной TRUE и очищает переменную
string. Далее обработка вплоть до конца функции проходит довольно
однообразно:
if (stricmp (tag, "TITLE") — 0) {
title = TRUE;
string.Empty();
}
Если тег — не TITLE, программа проверяет, не соответствует ли он /TITLE.
Если это так, код присваивает title значение False, а текст окна в объекте
parent_window (родительском окне дисплея страницы) устанавливает
значением строки, заключенной между тегами TITLE и /TITLE. После этого код
снова очищает переменную string:
else if (stricmp (tag, "/TITLE") == 0) {
title = FALSE;
if (frame_window)
frame_window->SetWindowText (string);
string.Empty();
}
Следующим проверяемым тегом является HR, который указывает, что
программа должна нарисовать на экране горизонтальную линейку (линию).
C/C++ в Internet
357
Когда встречается этот тег, для рисования линейки вызываются функции
MoveTo() и LineTo() класса контекста устройства CDC:
else if (stricmp (tag, "HR") == 0) {
у += y_increment + y_increment/2;
dc->MoveTo (x_begin, y);
dc->LineTo (width - X_BEGIN*2, y) ;
x = x_begin;
у += y_increment - y_increment/2;
y_top = y;
y_bottom = y_top + y_increment;
}
Далее, тег BR означает простой разрыв строки (так же как и Р). Если
программа встречает один из этих тегов, она выводит текущий текст и переводит
строку. Переменная string очищается:
else if (stricmp (tag, "BR") =- 0 || stricmp (tag, "P") =» 0) {
print_string();
string.Empty(};
}
Затем проверяется тег CENTER. Если он встретился, программа
устанавливает переменную center равной TRUE и готовится принять текст, который
будет центрирован:
else if (stricmp (tag, "CENTER") ■= 0) {
center = TRUE;
string.Empty();
}
Как вы, вероятно, уже догадались, далее мы ищем тег /CENTER,
означающий конец центрированного текста. Обнаружив его, мы производим
некоторые вычисления, чтобы определить правильное положение текста
относительно горизонтального центра экрана. Сначала вызывается функция
GetTextExtent(), чтобы определить общую длину строки. Она вычитается из
ширины страницы, и результат делится пополам, что дает положение строки
относительно левого края страницы:
else if (stricmp (tag, "/CENTER") == 0) {
center = FALSE;
x = (width - (dc->GetTextExtent (string)).ex) / 2;
После установки нужного значения х код выводит содержимое строки.
Также производится установка переменных х и у для следующей строки файла
HTML:
print_characters (string);
х = x_begin;
У += y_increment;
y_top = у;
y_bottom = y__top + y_increment;
}
Затем проверяется, не означает ли тег, что нужно установить или сбросить
признак полужирного шрифта; вызывается соответствующая
функция-элемент.
358
Глава 9
else if (stricmp (tag, "B") — 0) {
bold_on ();
}
else if (stricmp (tag, "/B") = 0) {
bold_off ();
)
Аналогичная проверка с вызовом соответствующей функции производится
для тегов установки или сброса курсива:
else if (stricmp (tag, "I") == 0) {
italic_on ();
)
else if (stricmp (tag, "/I") = 0) {
italic_off ();
)
Наконец, проверяются аналогичные теги атрибута подчеркивания с
вызовом, опять же, соответствующей вспомогательной функции:
else if (stricmp (tag, "П") = 0) {
underline_on ();
}
else if (stricmp (tag, "/U") «= 0) {
underline_off ();
)
Следующим обрабатывается тег А, означающий, что идущий далее текст
относится к гиперссылке. Если тег — А, вызывается функция href_on():
else if (strnicmp (tag, "A ", 2) == 0) {
href_on() ;
}
Следующая проверка относится к закрывающему тегу; если он обнаружен,
вызывается href_off():
else if (stricmp (tag, "/A") — 0) {
href_off();
}
Далее проверяется пара тегов для преформатированного текста, и снова
вызывается соответствующая вспомогательная функция:
else if (stricmp (tag, "PRE") = 0) {
preformatted_on ();
}
else if (stricmp (tag, "/PRE") — 0) {
preformatted_off () ;
}
Тег UL указывает на начало ненумерованного списка (каждый пункт
списка предваряется тегом LI); /UL означает его окончание. Аналогичным
образом теги OL и /OL означают начало и конец нумерованного списка (каждый
пункт которого также отмечается тегом Ы):
else if (stricmp (tag, "UL") » 0) {
unordered_list_on ();
}
else if (stricmp (tag, "/UL") — 0) {
C/C++ в Internet
359
unordered_list_off ();
}
else if (stricmp (tag, "OL") = 0) t{
ordered_list_on () ;
}
else if (stricmp (tag, "/OL") == 0) {
ordered_list_off ();
}
Вся обработка пунктов списка производится в функции insert_list_item(), о
которой еще будет говориться подробнее:
else if (stricmp (tag, "LI") = 0) {
insert_list_item();
}
Следующий else if проверяет, не относится ли тег к изображению. Если это
так, программа вызывает вспомогательную функцию display__image().
else if (strnicmp (tag, "IMG ", 4) == 0) {
display_image();
}
Далее идет проверка на указание того, что последующий текст должен
форматироваться как заголовок HI (первого уровня). Если этот тег обнаружен,
вызывается вспомогательная функция set_headl().
else if (stricmp (tag, "Hi") == 0) {
set_headl();
}
To же самое для заголовка второго уровня:
else if (stricmp (tag, "H2") = 0) {
set_head2();
}
Наконец, последний из else if выключает форматирование заголовка как
первого, так и второго уровней, устанавливая вызовом set_normal()
нормальный режим форматирования. Все эти вспомогательные функции будут
описаны позднее.
else if (stricmp (tag, "/Hi") ===== 0 | J
stricmp (tag, "/H2") — 0) {
set_normal();
Очевидно, разобранный программный код не исчерпывает всех возможных
тегов; но мы можем отобразить неопознанный тег просто как он есть, чтобы
пользователь знал, что он не видит. Так что в заключительном блоке else этой
длиннейшей последовательности вложенных условных операторов,
обрабатывающем все, что не было опознано выше, мы выводим тег в его натуральном
виде:
else {
print_characters ("<")
print_characters (tag)
print_characters (">")
360
Глава 9
}
tag [0] = 'NO';
}
Когда все содержимое файла прочитано, мы его закрываем и выходим из
функции; весь файл теперь отображен на экране.
fclose(fp);
}
|ФУг
ФУНКЦИИ GET_TAG() И PROCESS_INFO()
Следующая функция, с вызовами которой вы уже встречались при
обсуждении parse() — это get_tag(), которая выделяет в файле теги. Функция ищет
символ <; когда она его находит, следующие символы, вплоть до
закрывающего тег символа >, помещаются в параметр tag.
|ЗМ
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Функция get_tag() не распознает кавычек, поэтому любой символ <,
встретившийся в заключенной в кавычки строке, также будет
расцениваться в качестве начала тег.
В качестве параметров get_tag() принимает указатель на файл и массив
char, в котором она возвращает текст тега. Функция объявляет переменную
типа char, в которую считывается очередной байт файла.
int CSimpBrowseView::get_tag(FILE* fp, char* tag)
{
int c;
Далее идет цикл while, повторяющийся до тех пор, пока не встретится
конец файла (хотя в случае обнаружения полного тега также произойдет выход
из цикла). В цикле мы проверяем значение переменной с и, если это символ <
(левая скобка), начинаем читать тег:
while ((с = fgetc(fp)) •= EOF) {
if (с «= '<■) {
int i = 0;
while ((с = fgetc(fp)) •= '>') {
if (i < TAG_LENGTH)
tag[i++] - c;
)
Тег не может иметь длину, большую константы TAG_LENGTH; каждый
прочитанный символ сохраняется в массиве tag. После выхода из цикла в
конец прочитанной строки дописывается нуль-символ и происходит выход из
функции:
tag[i] = '\0';
return(0);
}
C/C++ в Internet
361
Если символ не является индикатором тега, вызывается функция рго-
cess_info() с символом в качестве параметра:
else
process_info(c);
}
По завершении цикла while происходит выход из функции с возвратом 1,
если цикл закончился чтением конца файла, или 0, если цикл завершился по
какой-то другой причине:
if (с — EOF)
return(1);
else
return(0);
}
Как было сказано выше, функция process_info() обрабатывает символы, не
имеющие отношения к тегам. Она получает от get_tag() по одному символу и
обрабатывает полученный символ различными способами в зависимости от
текущего формата вывода. Первым делом проверяется, не является ли символ
возвратом каретки:
void CSimpBrowseView::process_info (int с)
{
if (с == Лп') {
Если это возврат каретки, функция проверяет, не установлено ли
центрирование текста. Если центрирование включено, немедленно печатается элемент
данных string:
if (center) {
print_string();
return;
}
Далее мы проверяем, не равно ли х значению xjbegin, и если не равно,
присваиваем символу пробел. В противном случае мы просто возвращаемся в
вызывающую функцию:
else if (x != x_begin)
с = '
else
return;
}
Если символ — не возврат каретки, проверяются значения элементов title и
center. Если хотя бы один из них истинен, символ присоединяется к
переменной string, и ничего не печатается.
if (title || center) {
string += c;
return;
)
Если, наконец, символ благополучно прошел все предыдущие проверки,
программа просто вызывает функцию print_character(), которая выводит его
на экран в соответствии с текущими установками формата:
print_character (с) ;
}
362
Глава 9
(am
ФУНКЦИИ PRINT_CHARACTERS() И PRINT_CHARACTER()
Для печати строки функция print_characters() вызывает print_character(),
передавая последней символы строки по одному:
void CSimpBrowseView::print_characters(CString characters)
{
int length = characters.GetLength();
for (int i = 0; i < length; i++)
print_character (characters.GetAt (i));
}
Итак, print_character() вызывается программой, чтобы напечатать
одиночный символ; при этом текущая х-координата (и, если необходимо, у-координа-
та) получает приращение. Функция получает единственный параметр
(символ), который помещается в первый элемент массива s:
void CSimpBrowseView::print character(char c)
i
char s[2];
s[0] - c;
e[l] = '\0';
Затем из этих символа и ограничителя функция конструирует объект CString
и устанавливает шрифт, чтобы он наверняка соответствовал переменной cur-
rent_font:
CString character(s);
CFont* old_font — dc->SelectObject (current_font);
После установки шрифта вызывается TextOut() для вывода текста на
экран. Затем ширина символа прибавляется к текущему значению
горизонтальной координаты, и происходит возврат к старому шрифту.
dc->TextOut (x, у, character);
х += (dc->GetTextExtent(character)).ex;
dc->SelectObject (old_font);
Бели символ находится в конце строки, программа возвращает значение
х-координаты к началу строки и увеличивает у-координату на величину,
специфицированную значением y_increment.
if (x > (width-X_BEGIN*2)) (
х = x_begin;
}
}
у += y_increment;
y_top = у;
y_bottom =* y_top + y_increment;
{фу*
ФУНКЦИИ УСТАНОВКИ АТРИБУТОВ ШРИФТА
Следующие несколько функций являются элементами класса,
вызываемыми из функции parse() для изменения вида выводимого текста. Первая из
них — bold_on(), которая включает полужирный шрифт. Она делает это, уве-
C/C++ в Internet
363
личивая вес шрифта, создавая после этого новый шрифт функцией Create-
FontIndirect().
void CSimpBrowseView: :bold_on()
{
LOGFONT If;
current_font->GetLogFont (filf);
lf.lfWeight = 700;
delete current_font;
current_font = new CFont,-
current_font->CreateFontIndirect (filf);
}
Функция bold_off() выполняет сходные действия, возвращая вес шрифта к
значению 400, как показано ниже:
void CSimpBrowseView: :bold_off ()
{
LOGFONT If;
current_font->GetLogFont (filf);
lf.lfWeight » 400;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
}
Как и следовало ожидать, функция italic_on() устанавливает атрибут
курсива для текущего шрифта — вне зависимости от его размера или цвета. Это
делается путем присваивания Ifltalic значения TRUE и создания нового
шрифта, как показывает следующий код:
void CSimpBrowseView: :italic_on()
{
LOGFONT If;
current_font->GetLogFont (filf);
If.Ifltalic = TRUE;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
}
Аналогичным образом italic_off() выключает курсив, сбрасывая элемент
Ifltalic в FALSE и создавая затем новый шрифт.
Обратите внимание, что в каждой из этих функций мы удаляем current_font
и создаем его заново операцией new:
void CSimpBrowseView: :italic_off {)
i
LOGFONT If;
current_font->GetLogFont (filf);
If.Ifltalic - FALSE;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
}
364
Глава 9
Как и показанные bold_on() и italic__on(), функция underline_on
устанавливает элемент структуры LOGFONT. В данном случае мы устанавливаем ^Underline
равным TRUE и заново создаем шрифт с новыми характеристиками:
void CSimpBrowseView::underline_on()
{
LOGFONT If;
current_font->GetLogFont (Slf);
If.IfUnderline = TRUE;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (&lf);
}
Функция underline_off() выключает подчеркивание, устанавливая
соответствующий элемент равным FALSE:
void CSimpBrowseView: :underline_of f ()
<
LOGFONT If;
current_font->GetLogFont (6lf);
If.lfUnderline = FALSE;
delete current_font;
current_font = new CFont;
current_font->CreateFontIndirect (filf);
}
|ФУ1
ФУНКЦИИ HREF_ON() И HREF_OFF()
Как вы в свое время видели в этой главе, если программный код встречает
тег А, он отображает последующий текст как гиперссылку. Для начала
обработки информации о гиперссылке вызывается функция href_on(), а для
окончания ее обработки — href_off(). Большинство обозревателей выделяют текст
гиперссылки подчеркиванием, поэтому первое, что делает href_on() — это
включает подчеркивание:
void CSimpBrowseView: :href_on()
(
underline_on () ;
Затем код функции проверяет, был ли сделан щелчок на ссылке — если
так, она отображается красным; в противном случае устанавливается синий
цвет.
if (href [href_index].clicked)
dc->SetTextColor (RGB (255, 0, 0)) ;
else
dc->SetTextColor (RGB (0, 0, 255));
Теперь возникает маленькая сложность. В данный момент программа знает
координаты левого верхнего угла прямоугольника, окружающего гиперссыль-
ку, но не знает положения правого нижнего угла. Поэтому далее мы
устанавливаем его левый верхний угол, откладывая установку правого нижнего до
того момента, когда мы достигнем конца гиперссылки:
C/C++ в Internet
365
char* pi = strchr {tag, "") + 1;
char* p2 = strchr (pi, "") ;
int length = (int) (p2 - pi);
href [href_index].rect.left = x;
href [href_index].rect.top = y;
href [href_index].ref.Format ('*%*.*s", length, length, pi);
}
Мы также не знаем пока всего содержимого ссылки, поэтому мы
форматируем ее как пустую строку, пока не узнаем все до конца — что и происходит в
функции href_off(). В этой функции (она вызывается, когда мы обнаруживаем
закрывающий тег) программа выключает подчеркивание и снова делает текст
черным. Мы также записываем в структуру href координаты правого нижнего
угла и увеличиваем индекс гиперссылки на единицу (если, конечно, он все
еще меньше максимально допустимого числа ссылок).
void CSimpBrowseView; :href_off ()
{
underline_off ();
dc->SetTextColor (RGB (0, 0, 0));
href [href_index].rect.right — x;
href [href_index].rect.bottom = у + 20;
if (href_index < MAX_HREFS-1)
href_index++;
}
1 ФУ1
ФУНКЦИИ PREFORMATTED ON() И PREFORMATTED OFF()
Функция preformatted_on() выполняет действия, несколько
отличающиеся от того, что вы видели в обсуждавшихся до сих пор «on/off-функциях».
Вместо того, чтобы просто менять атрибуты текущего шрифта, она создает
совершенно новый шрифт типа Courier New с размером 100 пунктов (это чуть
меньше, чем для нормального текста страницы):
void CSimpBrowseView: :preformatted_on()
<
delete current_font;
current_font = new CFont;
current_font->CreatePointFont (100, "Courier New");
}
Функция preformatted_off() возвращает шрифт к обычному размеру.
Таким образом, если бы кто-то поместил тег PRE внутрь заголовка HI, после
закрывающего /PRE шрифт вернулся бы к обычному тексту, а не к шрифту
заголовка.
void CSimpBrowseView: :preformatted_off ()
<
delete current_font;
current_font = new CFont;
current_font->CreatePointFont (120, "Times New Roman");
}
366
Глава 9
[ ФУУ
ФУНКЦИИ ОБРАБОТКИ СПИСКОВ
В программе имеется пять функций, назначением которых является
управление отображением списков — две функции для ненумерованных списков,
две для нумерованных и одна функция вставки пункта. Сначала мы
рассмотрим ту, что управляет созданием ненумерованного списка (тег UL). Она
начинает с того, что устанавливает элемент ordered_list равным FALSE и затем
сдвигает точку начала текста на 40 пунктов вправо (оставляя место для
рисования маркера):
void CSimpBrowseView::unordered__list_on()
<
ordered__list = FALSE;
xjaegin = X_BEGIN + 40;
x = x__begin;
Затем код функции проверяет, содержится ли текст в элементе string; если
это так, он распечатывается и строка очищается:
if (string.GetLength() > 0) {
print_string();
string.Empty{);
}
В противном случае код просто распечатывает строку (тем самым перед
списком выводится пустая строка).
print_string();
}
Когда весь маркированный список распечатан, программе нужно вернуть
левое поле текста в его исходное положение, что выполняется в функции unor-
dered_list_off():
void CSimpBrowseView: :unordered_list_off()
{
x_begin = X_BEGIN;
Код функции также выводит текст в переменной string и очищает ее
содержимое:
print__string() ;
string.Empty{);
print_string();
)
Функция ordered_list_on() производит в основном те же действия, что и
unordered_list_on(). Главным отличием ее является то, что функция
устанавливает значение элемента ordered_list равным TRUE:
void CSimpBrowseView::ordered_list_on()
{
ordered__list = TRUE;
list_item = 0;
x_begin « X_BEGIN +40;
x = x_begin;
C/C++ в Internet
367
И снова функция очищает содержимое переменной string» выводя перед
списком пустую строку:
if (string.GetLength() > 0) {
print__string() ;
string.Empty();
}
print_string();
}
Точно так же, как unordered_list_off(), функция ordered_list_off(),
сбросив флаг ordered_Jist, восстанавливает крайнюю левую колонку страницы и
распечатывает то, что осталось в переменной string:
void CSimpBrowseView: :ordered_list_off ()
{
ordered_list = FALSE;
xjbegin = X_BEGIN;
print_string();
string. Empty();
print_string(J;
)
Как вы видели, тег Ы может указывать на пункт как маркированного, так и
нумерованного списка. Это нужно учесть при обработке тега, для чего
используется элемент ordered_list. Если его значение равно TRUE, мы знаем, что это
нумерованный список — т. е. мы должны поместить в начале пункта число.
Вставка пунктов в список производится функцией msert_list_item():
void CSimpBrowseView: :insert_list_item()
{
print_string();
string.Empty();
if (ordered_list) {
В блоке if мы увеличиваем на единицу номер пункта в списке, а затем
форматируем его, сдвигаем х-координату влево и распечатываем:
list_item++;
СString characters;
characters.Format ("%d.", list_item);
x -= 20;
print_characters (characters);
, >
С другой стороны, если список ненумерованный, мы должны нарисовать
перед пунктом маркер — что мы делаем с помощью функции Ellipse(), рисуя
маленький кружок:
else {
CBrush brush (RGB (0, 0, 0>);
CBrush* old_brush = dc->SelectObject (fibrush);
dc->Ellipse (x-10, y+6, x-4, y+12);
dc->SelectObject (old_brush);
}
368
Глава 9
В любом случае, нарисовав кружок или число, программа возвращает
значение х в точку, откуда должен начинаться вывод текста:
х = x_begin;
)
[ ФУНКЦИЯ DISPLAYJMAGEQ
Как отмечалось ранее, в своей реализации мы на самом деле не выводим
изображений; вместо этого мы рисуем окно, показывающее, что в данном
месте находится рисунок. Чтобы нарисовать окно правильного размера, нужно
обратиться к содержимому действительного файла, который мы находим,
поместив его имя в переменную filename:
void CSimpBrowseView: :display_image ()
<
char* pi = strchr (tag, "") + 1;
char* p2 = strchr (pi, '",);
int length = (int) (p2 - pi);
char filename[100];
strncpy (filename, pi, length);
filename [length] = *\0';
WORD image_width, image_height;
Далее программа пытается получить доступ к файлу, пробуя сначала
извлечь его с локального диска:
FILE* fp;
if ((fp = fopen (filename, "r")) \- NULL) {
Если это удалось сделать, программа получает размеры изображения. Она
предполагает, что все изображения записаны в формате GIF (от такого
предположения придется отказаться, если вы действительно будете отображать в
обозревателе графику). Этот формат хранит ширину и высоту изображения (в
пикселах), отводя для них по два байта начиная с шестого байта файла. Программа
сохраняет ширину и высоту в переменных, названных imagewidth и ima-
ge_height:
fseek (fp, 6L, SEEK_SET);
fread (Simage_width, 2, 1, fp);
fread (&image_height, 2, 1, fp) ;
fclose (fp);
}
Если же программа не находит файла на локальном диске, она
отправляется дальше и пытается извлечь его из Internet, используя ссылку на
изображение, записанную в теге.
else {
char old_file_name [100];
char old_real_file_name [100];
char image_file [100];
Сначала программа получает ссылку на документ и копирует имя
родительского HTML-документа в пару временных массивов. Это необходимый шаг, по-
C/C++ в Internet
369
скольку мы будем открывать и копировать файл с удаленной на локальную
машину с помощью функции OpenHttpFile().
CSimpBrowseDoc* pDoc = GetDocument{);
strcpy(old_real_file_name, pDoc->real_file_name);
strcpy(old_file_name, pDoc->file_name);
Затем программа рассматривает имя и убеждается, что оно начинается со
ссылки на протокол HTTP — что делает ссылку абсолютной. Если имя файла
начинается соответствующим образом, программа просто копирует полностью
квалифицированное маршрутное имя в переменную image_file. Если имя
файла не имеет префикса протокола HTTP, программный код предполагает, что
ссылка является относительной и копирует в image_file адрес родительского
документа, присоединяя к нему относительную ссылку:
if (strnicmp (filename, "http://", 7) = 0)
strcpy(image_file, filename);
else {
strcpy (image_file, old__real_file_name) ;
strcat(iraage_file, filename);
)
Затем программа извлекает файл и убеждается, что она может успешно
открыть его локально. Если так, из файла опять же извлекается ширина и
высота изображения:
if (pDoc->OpenHttpFile (image_file, "IMAGE.FIL") &&
(fp = fopen ("IMAGE.FIL", "r")) != NOLL) {
fseek (fp, 6L, SEEK_SET);
fread (&image_width, 2, 1, fp);
fread (&image__height, 2, 1, fp) ;
fclose (fp);
Чтобы упростить отображение страницы, мы убеждаемся, что изображение
не больше некоторого максимального размера (1000 пиксел в каждом
направлении); если это так, мы используем его действительный размер. В противном
случае мы полагаем для рисунка размер 100 на 100 пиксел:
if (image_width > 1000 || image_height > 1000) {
image_width = 100;
image_height - 100;
}
)
Если же программа вообще не может по какой-то причине открыть файл,
мы также устанавливаем для размера заглушки значение 100 на 100 пиксел:
else {
image_width = 100;
image_height = 100;
}
Получив необходимую информацию, программа снова записывает в
элементы данных класса документа имя HTML-файла:
strcpy(pDoc->real_file_name, old_real_file_name);
strcpy(pDoc->file_name, old_file_name);
}
370
Глава 9
Теперь, когда у нас есть размеры окна, которое нужно нарисовать, мы
можем двигаться дальше и нарисовать прямоугольник, вызывая функции Move-
То() и LineToO, как показано ниже:
у = y_top;
dc->MoveTo (x, у);
dc->LineTo (х + image_width - 1, у);
dc->LineTo (х + image__width - 1, у + image_height - 1);
dc->LineTo (х, у + image_height - 1);
dc->LineTo (х, у);
Наконец, мы выполняем некоторую окончательную подчистку координат
отображаемого документа.
y_top - у;
y_bottom = y_top + image_height;
х += image__width;
у = у + image_height - y_increment;
)
[включение и выключение заголовков
Следующие три вспомогательные функции обрабатывают команды
форматирования, управляющие включением и выключением «заголовочного» стиля
текста. Первая из них, set_headl(), вызывается, когда программа встречает
тег форматирования HI, который специфицирует большой полужирный
шрифт. Если найден этот тег, мы прежде всего очищаем переменную сиг-
rentfont, а затем создаем новый шрифт большого размера и устанавливаем
полужирный стиль:
void CSimpBrowseView: :set_headl{)
{
delete current_font;
current_font = new CFont;
current_font->CreatePointFont (240, "Times New Roman");
bold_on ();
Мы также проверяем, что текст начинается с начала строки. Если нет, мы
переводим строку:
if (x != x_begin) {
у += y_increment;
х = x_begin;
y_top = у;
y_bottom = y_top + y_increment;
}
Нужно также установить соответствующее приращение по вертикали,
чтобы расстояние между строками соответствовало более крупному шрифту. Это
делается путем присваивания приращению константы Y_INCREMENT_H1:
y_increment = Y_INCREMENT_Hl ;
}
C/C++ в Internet
371
В точности то же самое программа делает в случае тега Н2, только здесь
используются меньшие размер и константа Y_INCREMENT_H2. Вызываемая
при этом функция — set_headl().
void CSimpBrowseView: :set_head2()
i
delete current_font;
current_font = new CFont;
current_font->CreatePointFont (180, "Times New Roman");
bold_on ();
if (x != xjbegin) {
У +~ y_increment;
x = x__begin;
y_top = y;
y_bottom = y_top + y_increment;
}
y_increment = Y_INCREMENT_H2;
}
Когда программа встречает закрывающий тег для заголовков обоих типов,
она возвращается к обычному размеру шрифта^^^я«этого вызывается
функция set_normal(). В ней сначала распечатывается весь полученный к этому
моменту текст (крупным шрифтом), как показано ниже:
void CSimpBrowseView: :set_normal ()
{
print_string();
Затем выполняются действия, аналогичные тем, что вы уже видели, и
шрифт возвращается к обычному виду:
delete current_font;
current_font = new CFont;
current_font->CreatePointFont (120, "Times New Roman");
bold_off () ;
Опять-таки мы не можем менять размер шрифта в середине строки,
поэтому на экран выводится эквивалент возврата каретки; позиция вывода
устанавливается на начало следующей строки и приращению присваивается
константа, соответствующая размеру шрифта по умолчанию:
if (х != x__begin) {
у += y_increment;
х = x_begin;
y_top = у;
y_bottom = y_top + y_increment;
>
y_increment = Y_INCREMENT;
}
Is
ФУНКЦИЯ PRINT_STRING()
Функция print_string(), как вы неоднократно видели, отображает
содержимое текущей строки. Прежде всего функция проверяет состояние переменной
center. Если она равна TRUE, программа устанавливает соответствующим
образом х-координату, сдвигая ее вправо на половину размера пустой части строки:
372
Глава 9
void CSimpBrowseView: :print__string()
i
if (center)
x - (width - (dc->GetTextExtent (string)).ex) / 2;
В любом случае затем вызывается функция print_characters(),
распечатывающая все символы строки по одному, и строка очищается:
print_characters (string);
string.Empty();
После этого функция генерирует возврат каретки, устанавливая х-коорди-
нату в позицию крайней левой колонки страницы. Вертикальная координата
получает приращение, сдвигая вывод вниз, на следующую строчку:
х » x_begin;
у += y_increment;
y_top = у;
y_bottom — y_top + у increment;
ПЕРЕГРУЖЕННЫЕ ФУНКЦИИ КЛАССА ВИДА
Имеются две перегруженные функции класса вида, которые следует
рассмотреть — это OnMouseMove() и OnLMouseButtonDown(). Когда
пользователь проводит курсором мыши над гиперссылкой, мы должны показать, на
что она ссылается- Для этого нужно перегрузить функцию OnMouseMove().
Windows вызывает ее при каждом перемещении мыши, передавая в качестве
параметров х- и у-координаты курсора:
void CSimpBrowseView::OnMouseMove(UINT nFlags, CPoint point)
<
Прежде всего программа проверяет, имеется ли окно обрамления, и если
оно есть, создает контекст для окна клиента:
if (f ramejwindow) {
CClientDC dc(this);
OnPrepareDC(fidc);
dc.DPtoLP(Spoint);
Затем происходит итерация по массиву всех гиперссылок, имеющихся в
документе. Если мышь пользователя находится над какой-то из гиперссылок,
программа вызывает функцию SetMessageText(), отображая гиперссылку в
окне обрамления:
BOOL message_not_set = TRUE;
for (int i - 0; i < href^JLndex; i++) {
if (point.x >= href [i].rect.left s&
point.x <- href [i].rect.right &£
point.у >« href [i].rect.top &&
point.у <» href [i].rect.bottom) {
frame_window->SetMessageText (href [i].ref);
message_not_set = FALSE;
Если такая гиперссылка найдена, программа выходит из цикла, так как
курсор не может находиться сразу над двумя ссылками:
C/C++ в Internet
373
break;
}
)
Если же мышь находится не над гиперссылкой, программа устанавливает в
качестве отображаемого текста пустую строку, иначе пользователь продолжал
бы видеть информацию о ссылке, когда курсор мыши уже сошел с нее.
if (message__not_set)
frame_window->SetMessageText ("");
}
Наконец, функция вызывает свою предшественницу из базового класса,
чтобы та выполнила положенные ей действия:
CScrollView::OnMouseMove(nFlags, point);
}
Если функция OnMouseMove() должна проверить, не находится ли мышь
пользователя над гиперссылкой, то OnLMouSeButtonDown() проверяет, не
щелкнул ли пользователь на гиперссылке левой кнопкой. Windows вызывает
OnLMouscButtonDown() всякий раз, когда пользователь щелкает левой
кнопкой (опять же, как и в случае OnMouseMove(), дор&ддоая в качестве
параметров координаты курсора):
void CSimpBrowseView::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC dc(this);
OnPrepareDC(fidc);
dc.DPtoLP(fipoint);
После создания локального контекста устройства программа снова
проверяет, не щелкнул ли пользователь на гиперссылке. Но в этом случае
выполняются несколько более сложные действия. Прежде всего, программа
устанавливает элемент clicked равным TRUE:
for (int i = 0; i < href_index; i++) {
if (point.x >= href [i].rect.left &&
point.x <= href [i].rect.right &&
point.у >= href [i].rect.top &&
point.у <= href [i].rect.bottom) {
href [i].clicked = TRUE;
Затем она открывает ссылку на класс документа. Далее производится
проверка того, является ли URL гиперссылки абсолютным или относительным.
Если он абсолютный, программа вызывает функцию OpenHttpFile() со
строкой ссылки в качестве параметра:
CSimpBrowseDoc* pDoc = GetDocument{);
if (strnicmp (href [i].ref, "http://", 7) — 0)
pDoc->OpenHttpFile (href [i].ref);
Если URL относительный и текущий документ имеет корректный Ьир://-ад-
рес, программа выполняет конкатенацию этого адреса с относительным URL и
вызывает OpenHttpFileQ для сконструированного таким образом адреса:
else if (strnicmp (pDoc->real_f:^L«_namo, "http://", 7) == 0) (
strcat (pDoc->real_file_name, href [i].ref) ;
pDoc->OpenHttpFile (pDoc->real_file_name);
> к,
374
Глава 9
Если же текущая страница является локальным файлом, код
рассматривает новую страницу также в качестве локального файла и просто открывает его:
else {
strcpy (pDoc->real_file_name, href [i].ref);
pDoc->OpenHttpFile (pDoc->real_file_name);
}
После этого вызывается RedrawWindow(), которая аннулирует окно и
заставляет parser() обрабатывать новый файл.
RedrawWindow();
break;
}
}
}
Эта простая программа обозревателя работает довольно неплохо, вполне
приемлемо обрабатывая чисто текстовые документы HTML. На рис. 9.2 показан
обозреватель после открытия локального файла по умолчанию. На рис. 9.3
показан он же, но с открытой Web-страницей издательства Osborne/McGraw-Hill.
Рис. 9.2. Простой обозреватель с открытым файлом по умолчанию
Нет нужды говорить, что написание обозревателя дело не простое, —
например, один исходный код для Netscape Communicator занимает более
12 Мбайт. Реализация, показанная в этой главе, полезна, однако если вы
работаете с Visual C++, и знаете, что у всех ваших пользователей будет Internet
Explorer (как в среде локальной сети), вы можете гораздо проще реализовать в
своих приложениях функциональные возможности обозревателя, применив
класс CHtmlView. Эту методику вкратце описывает следующий раздел.
C/C++ в Internet
375
v\.- - -*'> :iw „>■* f'-^i A'"^,^
Рис. 9.3. Простой обозреватель, показывающий страницу Web
Новый Internet-класс CHtmlView
Оснастив свои приложения элементом управления Microsoft Web Browser,
вы сможете легко предоставить своим пользователям возможность просмотра
узлов WWW либо папок и файлов на локальной или сетевой файловой
системе. Создать Web-приложение с помощью готового компонента обозревателя
просто, потому что здесь придется выполнить относительно небольшое число
операций, в особенности по сравнению с усилиями, необходимыми для
написания своего собственного полнофункционального обозревателя, — усилиями,
о которых вы теперь уже получили достаточное представление.
Для большинства приложений сегодня поддержка модели и реализации
WWW является непременным требованием конечных пользователей.
Поддержка Internet в ваших приложениях может принимать разнообразные
формы, но поддержка функций обозревателя является, по видимому, наиболее
распространенной из них. В следующих разделах вы больше узнаете об
управляющем элементе обозревателя Internet, познакомитесь с поддержкой
просмотра Web в Visual C++ 6.0 и увидите, как можно наиболее эффективно
применять этот элемент управления.
Управляющий элемент обозревателя
Управляющий элемент обозревателя Web поддерживает просмотр сети
посредством выбираемых мышью гиперссылок, а также прямого перемещения
по адресам URL. Элемент даже поддерживает исторический список,
позволяющий вам перемещаться вперед и назад по уже просмотренным узлам, папкам и
документам.
376
Глава 9
Приложения* могут также использовать этот управляющий элемент в
качестве контейнера активного документа, который может принимать другие
активные документы. Другими словами, в управляющем элементе обозревателя
можно открывать и редактировать по месту документы с насыщенным
форматированием (такие, как документы Word или таблицы Excel).
Считая, по-видимому, что одного элемента обозревателя недостаточно,
Microsoft предлагает еще и класс MFC, инкапсулирующий этот элемент, так что
применять его становится еще проще. Класс CHtmlView обеспечивает вашим
приложениям удобный доступ к функциональным возможностям элемента
обозревателя Web в контексте архитектуры документ/вид библиотеки MFC.
ActiveX-элемент обозревателя Web (и соответственно класс CHtmlView)
доступен только для программ, работающих под Windows 95, Windows 98
либо Windows NT версии 3.51 или позднейшей, если пользователь установил
Internet Explorer 4.0 (или более позднюю версию).
Некоторые функции-элементы применимы только к приложению Internet
Explorer. Они не вызовут ошибки при вызове их для элемента Web Browser,
однако не произведут никакого видимого эффекта. Это функции GetAddress-
Ваг(), GetFullNameO, GetStatusBarO. SetAddressBar(), SetFullScreen(), Set-
MenuBar(), SetStatusBar() и SetToolBar().
Создание проекта, использующего CHtmlView
Задача создания приложения на основе класса CHtmlView настолько же
проста, насколько это, вероятно, можно ожидать для программ с обычной
архитектурой документ/вид. Вам нужно создать оболочку приложения,
поддерживающий интерфейс одного либо нескольких документов.
Большинство опций, включая те, что определяют наличие
инструментальной линейки или возможность печати и предварительного просмотра, никак
не влияют на способность программы использовать класс CHtmlView. Вы
можете задать эти опции так, как это требуется для остальной части
приложения, и оно будет безо всяких проблем работать с элементом обозревателя Web
(и классом CHtmlView), какими бы эти установки ни были.
Единственным важным моментом, о котором не следует забывать, это
установка опции базового класса CView на последней панели диалога AppWizard;
ее нужно заменить на CHtmlView. Либо, конечно, вы всегда можете ввести
производный от CHtmlView класс в качестве дополнительного вида своего
приложения. (Обычно такой дополнительный вид будет также иметь
отдельный от остальной части приложения документ, особенно если поддержка Web
четко отграничена от остальных функций приложения.)
После того, как вы создадите проект на основе CHtmlView, его без всяких
дополнительных модификаций можно компилировать и запустить. URL по
умолчанию будет указывать на базовую страницу узла Microsoft по адресу
http://www.microsoft.com.visualc, что иллюстрирует рис. 9.4.
Можно легко изменить начальный URL, отредактировав функцию Onlni-
tialUpdate(). Например, если вы хотите начать от http://eee.osborne.com,
нужно изменить эту функцию следующим образом:
C/C++ в Internet
377
. t " •/ -".' . л -,." ' " . # '
« . *■»*,' I', я* ■*.
Рис. 9.4. Когда проект на базе С Html View запускается впервые, появляется страница Microsoft
void CSampleHTMLView: :OnInitialUpdate()
{
CHtmlView: :OnInitialUpdate () ;
Navigate2(_T("http://www.osborne.com"), NULL, NOLL);
)
Перемещение в среде CHtmlView
Когда программа на основе CHtmlView запущена, пользователи могут
перемещаться самостоятельно, щелкая мышью на гиперссылках, отображаемых
программой. Для обеспечения такой возможности вам ничего не нужно
делать — стоящий за классом элемент Web Browser выполняет все необходимые
действия. Однако часто вам требуется перемещаться по различным URL,
программно управляя целевыми адресами.
Наиболее ярким примером является реализация исторического списка, по
которому можно перемещаться вперед и назад, — например, находясь на
данной странице Web, пользователь пожелает вернуться к странице, которую он
просматривал непосредственно перед этим. Большинство обозревателей имеют
кнопки «Вперед» и «Назад», реализующие подобный тип перемещений, и
Internet Explorer здесь не исключение. Но элемент управления Web Browser
не отображает, однако, автоматически значков с такими функциями
(подобных тем, что мы видим в Internet Explorer).
Интерфейс на основе дополнительных значков в инструментальной
линейке — это, вероятно, самое удобное для пользователей средство перемещения
вперед и нааад. В дополнение к этому вы, возможно, захотите предусмотреть в
своей программе соответствующие пункты меню (с короткими клавишами).
378
Глава 9
В обработчиках для этих пунктов меню и инструментальных кнопок вы,
вероятно, захотите вызывать функцию класса вида Васк(), чтобы переместиться
назад:
void CSampleView: :Back ()
{
GoBack();
}
Аналогичным образом вы могли бы вызывать функцию Forward() для
перемещения вперед:
void CSampleView: :Forward()
{
GoForward();
}
В число других важных функций, экспонируемых элементом управления
Web Browser, входят Stop(), Refresh(), GoHome() и GoSearch().
♦ Stop() заставляет управляющий элемент обозревателя остановить
загрузку содержимого текущей страницы Web.
♦ Refresh() говорит обозревателю, что нужно снова получить содержимое
текущей страницы с целевого сервера Web и по завершении этого
процесса обновить экран обозревателя.
♦ Если пользователям требуется вернуться на свою домашнюю страницу, вы
можете вызвать функцию GoHome(). Домашняя страница определяется
из предпочтений реестра, которые устанавливаются Internet Explorer.
♦ GoSearch() отправляет пользователя на его страницу поиска по
умолчанию (за поиск по умолчанию также ответствен Internet Explorer).
Элемент управления Web Browser экспонирует также две функции, которые
можно использовать для перемещения пользователей прямо к новому URL. Это
Navigate() и Navigate2(). Отличие их только в типе принимаемых URL.
Navigate() ожидает относительный URL (хотя он может обрабатывать
абсолютные URL), в то время как Navigate2() может обрабатывать исключительно
абсолютные URL. Например, пользователь может просматривать страницу
Web с адресом
http://www.osborne.com/index.html
и захочет перейти к
http://www.osborne.com/archives/index.html
Новый URL просто на один уровень глубже в иерархии каталогов сервера.
Для такого перемещения вполне достаточно следующего кода:
Navigate("archives/index.html");
Заметьте, однако, что следующий код работать не будет (и выбросит
исключение), поскольку Navigate2() работает только с абсолютными URL:
Navigate2("archives/index.html") ;
Если использовать Navigate2(), нужно было бы присоединить
навигационному вызову весь маршрут сервера с каталогом:
Navigate2 ("http: //www. оsborne. com/archives/index. html") ;
C/C++ в Internet
379
Стоит упомянуть еще об одном важном моменте относительно обеих
навигационный функций. В вызове и той, и другой функции можно опустить у
передаваемого функции URL префикс протокола http://. Управляющий элемент
обозревателя полагает, что все соединения будут использовать протокол HTTP
и потому производит синтаксический разбор URL, автоматически
присоединяя в его начало имя протокола (если оно уже не указано).
Простая программа-обозреватель на основе
класса CHtmlView
Написать простую программу с интерфейсом одиночного документа,
пользуясь тем, что вы узнали из предыдущих разделов и прибавив к этому класс
CHtmlView — такая задача решается достаточно прямолинейно. Вы найдете
реализацию программы MFCBrowse на прилагаемой к книге дискете. Почти весь
код — стандартный для MFC, так что совершенно ни к чему приводить его здесь.
Чтобы написать простой обозреватель, вам нужно сделать следующее:
♦ Создайте приложение на базе класса CHtmlView
♦ Напишите реализацию функций, описанных в этом разделе главы
♦ Предусмотрите меню и инструментальные линейки для доступа
пользователя
Единственным инструментом, который вам придется создать самому,
является какое-то средство ввода URL для непосредственного перехода
безотносительно к содержимому файла истории.
В программе MFCBrowse функция OnNavigateGoTo() создает модальный
диалог (из шаблона), отображающий единственное текстовое поле, в котором
пользователь может ввести URL. Реализация этой функции показана ниже:
void CMFCBrowseView: :OnNavigateGoto{)
{
CNewUrl NewURL;
if (NewURL.DoModal{) = IDOK)
Navigate2(NewURL.m_strNewUrl, 0, 0, 0);
}
Класс CNewUrl является производным от CDialog и соответствует диалогу,
вид которого должен быть понятен из самого этого кода.
Обозреватель для просмотра результатов поиска
Реализация варианта обозревателя с интерфейсом многих документов столь
же просто — нужно только воспользоваться объектом CMultiDocTemplate и
выполнить ряд рутинных операций для создания видов MDI,
Однако будет полезно создать приложение MDI, чтобы продемонстрировать
некоторые возможные применения класса CHtmlView в ваших приложениях.
На дискете вы найдете программу MultiSearch, которая показывает
преимущества CHtmlView в плане отображения интересной для пользователя
информации. Программа, как таковая, предназначена для более удобного просмотра
результатов поиска, выполненного поисковой машиной.
380
Глава 9
При первоначальном запуске программа отображает в своем окне четыре
дочерних окна. Три из них пусты, а четвертое указывает на http://www.yahoo.com,
стандартную машину поиска. (Вы можете, конечно, переадресовать его куда
захотите из программного кода.)
Щелчок на любой ссылке в окне машины поиска вызовет отображение
запрошенного URL в одном из других окон. Программа посылает в них URL по
порядку, т. е. четвертый запрос будет опять послан в первое из них.
Чтобы осуществить такое поведение, программа переопределяет функцию
CHtmlView::OnBeforeNavigate2(). Этой функции поступают запросы,
переданные Navigate2(), перед тем как последняя станет исполняться. В
программе MultiSearch запрос перехватывается и посылается одному из других окон,
где и отображается запрошенный URL.
Машины поиска запрашивают URL для отображения своих результатов.
Эти URL всегда имеют html-аргументы, предваряемые вопросительным
знаком. Функция OnBefoM^av|gate2() пользуется этим для проверки того, что
данный запрос есть заЖос^&тины поиска. Такой запрос функция передает
дальше, ничего с ним атсеяРЦгНо если URL обычный, функция
перехватывает его, посылает комашнУ£||р?а1е2() в целевом окне и устанавливает элемент
*pbCancel равным ^ие™йгшрвдзируя оболочке, что не следует исполнять
команду Navigate2() в окне поиска; Рис. 9.5 показывает приложение MultiSearch
после поиска "C++" и выборку страниц Web.
eiX£;*'."?<:. ;r*:-'\rb
■»;_ #■
Рис. 9.5. Приложение MultiSearch показывает окно поиска и три окна результата
«•*
тШЕЕ
. *i & * *
sld.cpp
syd.cpp
ddb.cpp
fv.cpp
fvseries.cpp
, !• 1 i. §• #
ч: #>*.*&
[ape
*s -^i
fvs2scpp
pv.cpp
pvsenes.cpp
loan.cpp
retire.cpp
.# ,;.s
I*. "
t, *^
П . . L
382
Глава 10
Всякий знает, что сила компьютеров заключается прежде всего в
проведении расчетов. На самом деле на некотором уровне все, что делает
компьютер, сфокусировано на выполнении вычислений. Тем не менее многие книги
совершенно игнорируют аспект важных вычислений, которые бывает
необходимо ввести в программу. Хотя стандартные библиотеки C/C++ содержат ряд
функций, помогающих в вычислениях, вроде тангенсов и косинусов, от этих
библиотек практически нет никакой пользы, если речь идет не о научных
расчетах. В этой и следующей главах вы познакомитесь с некоторыми
специальными функциями, которые можно применять в программах, производящих
финансовые и статистические расчеты.
В интересах ясности материал этой главы делится на две категории:
♦ Вычисление амортизации активов. Это важная методика бухгалтерских
программ.
♦ Вычисление годовой ренты. Хотя глава имеет в виду ренту вообще,
основных видов ренты два — когда деньги выплачиваются вам и когда вы
выплачиваете их кому-то другому. Обычно в случае, когда вы платите
деньги кому-то еще, речь идет о ссуде.
Логика всех программ,эт,с$х главы достаточно прямолинейна; основной объ- ,
ем обсуждений будет посвящен вычислениям как таковым, производящимся в
функциях, и отношению этих вычислений к реальной жизни.
Вычисление амортизации
Амортизация является средством определения стоимости имущества на
некоторый момент времени после того, как имущество было приобретено.
В мире бухгалтерских расчетов вычисление амортизации является одной из
важнейших задач. Метод амортизации для конкретного имущества будет
влиять на общий баланс компании и выплату налогов. Он может даже повлиять
на продажную стоимость компании, если речь идет о публичных торгах, и на
ценность активов компании как функции ее баланса. Амортизация
применима обычно только к «материальным» активам, например, компьютерам,
гидравлическим прессам и т. п.
В этой главе вы увидите, как работают три типа амортизации: линейная
амортизация, амортизация по сумме цифр лет и амортизация по балансу с
двойным наклоном.
Линейная амортизация
Вероятно, лучшим способом понять, что такое амортизация, будет
рассмотрение простого примера линейной амортизации.
Предположим, ваша компания купила сервер, который обошелся вам в
$10000. Бухгалтеры обычно амортизируют компьютеры за период от трех до
пяти лет. В случае покупки данного типа и при использовании линейной
амортизации бухгалтерия обычно оценит амортизацию имущества за период в пять
лет. Поскольку компьютеры по истечении некоторого времени уже ничего не
стоят, конечная стоимость данного имущества скорее всего будет равна $0 (в
отличие от автомобиля, который спустя пять лет еще сохранит треть своей
первоначальной цены).
Финансовые расчеты
383
При данных условиях стоимость имущества по истечении каждого года его
реального существования отражена в таблице ЮЛ.
Таблица 10.1. Простой пример линейной амортизации
Цена при покупке
$10000
$10000
$10000
$10000
$10000
Остаточная стоимость
$0
$0
$0
$0
$0
Текущий год
1
2
3
4
5
Текущая стоимость
$8000
$6000
$4000
$2000
$0
Как можно видеть, линейная модель амортизации просто делит разницу
между стоимостью при покупке и остаточной стоимостью на число лет, за
которые вы амортизируете имущество. Если бы вы амортизировали то же
имущество в течение десяти лет, то амортизация составила бы $1000 в год.
Таким образом, функцию для линейной амортизации можно записать так:
Сумма амортизации = (Цена покупки - Остаточная стоимость) / Число лет
Определив сумму амортизации, можно теперь вычислить текущую
стоимость имущества по следующей формуле:
Текущая стоимость = Цена покупки - (Сумма амортизации * Текущий год)
Итак, вы можете вычислить линейную амортизацию имущества, если вам
известна цена при покупке, стоимость в конце периода амортизации (остаточная
стоимость) и число лет, за которое вы собираетесь имущество амортизировать.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Бухгалтеры не решают произвольно, как они будут амортизировать
активы. Как правило, они принимают для амортизации данного вида
активов строго установленное число лет. Это число лет может быть
указано в документах Внутренней службой доходов, Ассоциацией
уполномоченных общественных бухгалтеров или каким-то другим
авторитетным органом. Если разработанная вами программа вычисления
амортизации предназначена для общего пользования, вам нужно
обратиться к соответствующим таблицам и убедиться, что вы
пользуетесь узаконенными цифрами для каждого вида оборудования.
Код программы для расчета линейной амортизации
Следующая программа, sldxpp, использует для расчета линейной
амортизации приведенную выше формулу.
384
Глава 10
• #include <iostream>
,. 'using namespace std;
i
•double sld(double cost, double salvage, int lifetime)
M
return (cost - salvage) / lifetime;
■, :>
1 int main()
■. :M
double cost, salvage, deprecval;
tJ r int lifetime;
int i;
" '. cout « "Enter the original cost of the asset: ";
cin » cost;
£
:i
cout « "Enter the salvage value of the asset: ";
*, ' cin » salvage;
■4
cout « "Enter the lifetime in years of the asset (integer): ";
1 cin » lifetime;
~i
J
) deprecval = sld(cost, salvage, lifetime);
cout « "The straight-line depreciation of the asset is: " «
deprecval « " dollars per year." « endl;
: for (i=l; i < lifetime + 1; i++) {
cout « "The value of the asset after year " « i;
cout « " is: " « cost - (i * deprecval) « endl;
-> ' }
'*■" return 0;
| ПРИМЕЧАНИЯ
Давайте повнимательнее посмотрим на этот код — особенно на функцию sld().
double sld(double cost, double salvage, int lifetime)
{
return (cost - salvage) / lifetime;
}
Как видите, функция принимает три входных параметра — начальную
цену, остаточную стоимость и время жизни имущества, за которое оно
амортизируется. Функция, в свою очередь, возвращает вызывающей программе
значение типа double. Оно равно сумме амортизации за год. Оператор return
возвращает значение, рассчитанное по уже известной вам формуле — цена
покупки минус остаточная стоимость, деленные на период амортизации имущества.
Финансовые расчеты 385
Программа sld.cpp получает от пользователя эти три значения и вызывает
функцию sld() для расчета амортизации за год. Затем, в конце функции
main(), она входит в цикл for и выводит значения текущей стоимости для
каждого года.
deprecval = sld(cost, salvage, lifetime);
cout « "The straight-line depreciation of the asset is: " «
deprecval « " dollars per year." « endl;
for (i-1; i < lifetime + 1; i++) {
cout « "The value of the asset after year " « i;
cout « " is: " « cost - (i * deprecval) « endl;
)
Так как программа пользуется значением амортизации в цикле,
возвращаемое функцией sld() значение присваивается сначала локальной переменной
(тем самым мы расходуем память, но экономим на вызовах функции, не
производя повторно одно и то же вычисление).
Если вы запустите эту программу в том виде, как она написана, то получите
результат, сходный с показанным ниже. Заметьте, что выдаваемые примером
числа соответствуют значениям из таблицы 10.1.
Enter the original cost of the asset: 10000
Enter the salvage value of the asset: 0
Enter the lifetime in years of the asset (integer): 5
The straight-line depreciation of the asset is: 2000 dollars per
year.
The value of the asset after year 1 is: 8000
The value of the asset after year 2 is: 6000
The value of the asset after year 3 is: 4000
The value of the asset after year 4 is: 2000
The value of the asset after year 5 is: 0
В зависимости от потребностей вашего приложения вы можете по-разному
реализовать функцию sld(). Например, если бы вам потребовалось рассчитать
текущую стоимость на определенный момент времени, то вы могли бы написать:
double sld(double cost, double salvage, int lifetime,
int currentyear)
{
double deprecval;
deprecval - (cost - salvage) / lifetime;
return cost - deprecval * currentyear;
В этом случае вы просто вносите вторую часть расчета амортизации —
определение текущей стоимости — внутрь функции, рассчитывающей
амортизацию за год.
Амортизация по сумме цифр лет
Хотя линейная амортизация является простейшим из методов
амортизации, в бизнесе она используется реже всего— как раз из-за своей простоты. На
самом деле очень мало вещей амортизируются линейно. Недаром же говорят,
13 Зал 1208
386
Глава 10
что автомобиль амортизируется на 20% в ту самую минуту, как вы выезжаете
со стоянки торговца. Однако в то же самое время большинство автомобилей
сохраняют стоимость намного дольше пяти лет. Очевидно, автомобили не
амортизируются линейно.
Второй обычный метод амортизации активов — это амортизация по сумме
цифр лет. Это метод с большой начальной нагрузкой, другими словами,
имущество амортизируется быстрее в первые годы жизни и медленнее к концу
времени жизни. Чтобы понять, как это работает, рассмотрим снова примере
сервером за 10000 долларов, который вы хотите амортизировать в течение
пяти лет. В соответствии с методом суммы цифр имущество будет
амортизироваться так, как указано в таблице 10.2.
Таблица 10.2. Амортизация по сумме цифр лет
Цена при покупке
$10000
$10000
$10000
$10000
$10000
Остаточная стоимость
$0
$0
$0
$0
$0
Текущий год
1
2
3
4
5
Текущая стоимость
$6666.66
$4000.00
$2000.00
$666.67
$0.00
Как видно из таблицы, стоимость в конце срока амортизации также равна $0.
Однако имущество здесь амортизируется быстрее в начале, чем в конце срока
службы — за первый год амортизация составляет 1/3 начальной стоимости, а
за последний всего 1/15. Еще лучше видна эта начальная амортизационная
нагрузка, если посмотреть на таблицу 10.3, рассчитанную для десятилетнего
срока амортизации.
Таблица 10.3. Амортизация по сумме цифр за 10 лет
Цена при покупке
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
Остаточная стоимость
$0
$0
$0
$0
$0
$0
$0
$0
$0
$0
Текущий год
1
2
3
4
5
6
7
8
9
10
Текущая стоимость
$8181.81
$6545.44
$5090.89
$3818.16
$2727.25
$1818.15
$1090.87
$545.41
$181.77
$0.00
Финансовые расчеты 387
Как видите, имущество к концу третьего года теряет почти половину своей
стоимости, а к середине срока почти три четверти. Вообще для активов,
подобных компьютерам, амортизация по сумме цифр лет дает гораздо более точную
оценку.
Расчет амортизации по сумме цифр лет ненамного сложнее, чем в случае
линенйной амортизации. Здесь также проще представить себе формулу
разбитой на две части. Для каждого конкретного года сумма амортизации может
быть рассчитана следующим образом:
(Остаток срока службы / Сумма лет) * (Цена - Остаток стоимости)
Под остатком срока службы понимается промежуток времени между
началом года, для которого производится расчет, и концом срока полезной
службы. Например, если вы производите расчет для 2-го года, число оставшихся до
конца срока службы лет будет равно
(10 - 2 + 1 = 8)
Значение суммы лет соответствует сумме числа прошедших лет. Если
имущество амортизируется за пять лет, то сумма чисел лет будет равна
(1 + 2 + 3 + 4 + 5 = 15)
Сумму лет можно рассчитать проще, применив такую формулу:
(Срок службы * (Срок службы + 1)) / 2
Если снова обратиться к пятилетнему примеру, мы получим
((5 * (5 + 1)) / 2 = 15)
В программе проще вычислять сумму лет по этой формуле, чем
суммировать в цикле последовательные числа.
После того, как вы вычислили сумму амортизации для каждого
конкретного года, нетрудно определить текущую стоимость после всех годовых
амортизации, как показано ниже:
Начальная цена - 2(Амортизация(1) . . Амортизация(К))
Другими словами, текущая стоимость будет равна первоначальной цене
минус амортизация за все годы вплоть до текущего. Например, для имущества на
третьем году амортизации его стоимость будет равна следующему значению:
Начальная цена - (Амортизация(1) + Амортизация(2) + Амортизация(З))
Таким образом, как и в случае линейной амортизации, вы можете
рассчитать амортизацию имущества для любого года в пределах его срока службы,
если знаете первоначальную цену, остаточную стоимость и число лет, за
которую оно амортизируется. Чтобы рассчитать амортизированную стоимость на
данный момент времени, нужно также знать, в какой точке амортизационного
цикла вы находитесь.
Код программы для амортизации по сумме цифр лет
Программа syd.cpp на прилагаемой дискете для вычисления амортизации
использует формулы амортизации по сумме лет.
13*
388
Глава 10
*#include <iostream>
^#include <cmath>
, 'using namespace std;
'^double syd(double cost, double salvage, double lifetime, int
I*, .deprecyear)
*i double multiplier, sumyears, valuetodeprec;
i valuetodeprec = cost - salvage;
sumyears = lifetime * ((lifetime + 1) / 2) ;
t multiplier = (lifetime - (deprecyear - 1)) / sumyears;
* * return multiplier * valuetodeprec;
. >
-3
*
my double roundtohundreds(double roundvalue)
{
J> Л roundvalue = ceil (roundvalue * 100);
-£* return roundvalue / 100;
'i>
Г !
Mint main()
-It
, *, double cost, salvage, deprecval, currentval;
"4 double lifetime;
- ' int i;
ti
i' cout « "Enter the original cost of the asset: ";
cin » cost;
cout « "Enter the salvage value of the asset: ";
* cin » salvage;
i
cout « "Enter the lifetime in years of the asset (integer): ";
4 cin » lifetime;
■
il * cout « "The sum-of-the years' digits depreciation of "
*~ \ « "the asset is as follows:" « endl;
] currentval = cost;
; \ for (i=l; i < lifetime + 1; i++) {
* deprecval — syd(cost, salvage, lifetime, i);
' t currentval = currentval - deprecval;
cout « "The depreciation for year " « i « " is: " «
j roundtohundreds(deprecval) « endl;
" и cout « "The value of the asset after year " « i;
cout « " is: " « roundtohundreds(currentval) « endl;
Л ' \
я- '
r\ return 0;
Финансовые расчеты
389
I ПРИМЕЧАНИЯ
Давайте повнимательнее рассмотрим этот код. Программа вводит новую
функцию roundtohundreds(). Кроме того, амортизация вычисляется
по-другому, чем в представленной ранее программе sld.cpp. Однако самое
существенное отличие заключается в функции, вычисляющей амортизацию — она
называется syd():
double syd(double cost, double salvage, double lifetime, int
deprecyear)
{
double multiplier, sumyears, valuetodeprec;
valuetodeprec = cost - salvage;
sumyears = lifetime * ((lifetime + 1) / 2);
multiplier = (lifetime - (deprecyear - 1)) / sumyears;
return multiplier * valuetodeprec;
)
Обратите внимание на отличие в параметрах функции — параметр lifetime
имеет тип double, а не int, как в первой программе. Это сделано из-за способа
вычисления значения sumyears. Если бы lifetime была целой, то при делении
C++ автоматически отбросил дробную часть результата (из-за соображений
точности), что могло бы значительно исказить значения амортизации. В
качестве альтернативы можно было бы привести тип значения.
I ЗАМЕЧАНИЕ ПРОГРАММИСТА
Чтобы увидеть, какое влияние на результат вычислений может иметь
точность представления значений, попробуйте изменить тип lifetime
на int (не забудьте поменять его также и в main() ). Вычисление на
самом деле амортизирует стоимость до отрицательных значений.
Во всяком случае, программный код в функции syd() объявляет некоторые
локальные переменные, большей частью для ясности. (Хотя все вычисления
можно было бы произвести прямо в операторе return, он оказался бы слишком
запутанным.) Первый оператор определяет амортизируемую сумму и
помещает полученное значение в локальную переменную value todeprec. Второй
оператор производит вычисление суммы цифр лет по уже знакомой вам формуле
и помещает значение в переменную sumyears типа double. Третий оператор
вычисляет долю от общей амортизируемой суммы, и в операторе return это
значение умножается на полную сумму амортизации. Функция возвращает
амортизацию для текущего года.
В программе syd.cpp для вывода амортизации и амортизированной
стоимости организован цикл for. Можно было бы, однако, так изменить функцию
syd(), чтобы она сразу возвращала для текущего года стоимость после
амортизации, написав код, подобный следующему:
double syd(double cost, double salvage, double lifetime,
int deprecyear)
i
390 Глава 10
double multiplier, sumyears, valuetodeprec, currentvalue;
valuetodeprec — cost - salvage;
sumyears = lifetime * ((lifetime + 1) / 2);
currentvalue = cost;
for (int i=0; i < deprecyear; i++) {
multiplier - (lifetime - i)) / sumyears;
currentvalue = currentvalue - (multiplier * valuetodeprec);
)
return currentvalue;
)
Такая модифицированная функция возвращала бы текущую стоимость в
конце указанного года амортизации.
Вторая функция программы выполняет округление с целью упрощения
вывода. Вообще обычно функции для округления не применяют, поскольку с
течением времени она будет вносить ошибку в значения амортизации (которая
может варьироваться от нескольких центов до тысяч долларов в зависимости
от общей стоимости имущества). Вместо этого округление производят только
при выводе, как и сделано здесь. Вот код функции roundtohundreds:
double roundtohundreds(double roundvalue)
{
roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
}
Функция умножает входное значение типа double на 100, округляет его до
целого значения и затем возвращает это целое, деленное на 100. Для
округления используется функция ceil() стандартной библиотеки C/C++.
Функция main() этой программы производит в сущности те же действия,
что и в программе sld.cpp. Самое большое отличие заключается в роли цикла
for в вычислении амортизации за каждый год и правильном отслеживании
амортизированной стоимости, что показано ниже:
currentval = cost;
for (i=l; i < lifetime + 1; i++) {
deprecval = syd(cost, salvage, lifetime, i);
currentval = currentval - deprecval;
cout « "The depreciation for year " « i « " is: " «
roundtohundreds(deprecval) « endl;
cout « "The value of the asset after year " « i;
cout « " is: " « roundtohundreds(currentval) « endl;
}
Перед тем, как войти в цикл, программа присваивает переменной
currentval просто начальную цену имущества. Затем на каждом проходе
цикла currentval корректируется вычитанием амортизации за текущий год. Если
вы запустите программу и введете значения, соответствующие таблице 10.3
($10000 для начальной цены, $0 для остаточной стоимости и 10 для периода
амортизации), то получите следующий вывод:
Enter the original cost of the asset: 10000
Enter the salvage value of the asset: 0
Enter the lifetime in years of the asset (integer): 10
The sum-of-the years' digits depreciation of the asset is as
Финансовые расчеты 391
follows:
The depreciation for year 1 is: 1818.19
The value of the asset after year 1 is: 8181.82
The depreciation for year 2 is: 1636.37
The value of the asset after year 2 is: 6545.46
The depreciation for year 3 is: 1454.55
The value of the asset after year 3 is: 5090.91
The depreciation for year 4 is: 1272.73
The value of the asset after year 4 is: 3818.19
The depreciation for year 5 is: 1090.91
The value of the asset after year 5 is: 2727.28
The depreciation for year 6 is: 909.1
The value of the asset after year 6 is: 1818.19
The depreciation for year 7 is: 727.28
The value of the asset after year 7 is: 1090.91
The depreciation for year 8 is: 545.46
The value of the asset after year 8 is: 545.46
The depreciation for year 9 is: 363.64
The value of the asset after year 9 is: 181.82
The depreciation for year 10 is: 181.82
The value of the asset after year 10 is: 0.01
ЗАМЕЧАНИЕ ПРОГРАММИСТА
[зм
Как видите, функция округления, хотя и полезна для форматирования,
время от времени дает явно ошибочный результат. Стоимость для
последнего года должна была бы равняться $0.00. Ошибка происходит
только при выводе, сами значения точны; их ошибка может
составлять разве что какие-то ничтожные доли цента.
Амортизация по балансу с двойным наклоном
Пока вы видели в действии два метода амортизации — линейную и
амортизацию по сумме цифр лет. Третьим типом, который можно встретить,
является амортизация по балансу с двойным наклоном. Он может быть реализован в
двух видах: «чисто математическом» и ориентированном на вычисление
налогов. Однако, поскольку «чисто математический» метод никогда не может
амортизировать стоимость имущества до остаточной стоимости, мы
сосредоточим внимание на налоговой методике амортизации по балансу с двойным
наклоном. Примечания к программному коду объяснят, как модифицировать
функцию для поддержки «чистого» метода амортизации.
Подобно методу суммы цифр лет, амортизация по балансу с двойным
наклоном вычисляет амортизацию имущества в ускоренном темпе, — самая
высокая амортизация приходится на первый период, а в последующий период
она будет всегда меньше, чем в предыдущий.
Апробированная для налогов реализация метода двойного наклона
использует в действительности два набора формул для вычислений амортизации.
В первую половину общего периода жизни имущество ежегодно
амортизируется в два раза быстрее, чем при линейной амортизации; однако сумма
амортизации всегда вычисляется исходя из текущей стоимости. Во вторую половину
392
Глава 10
периода жизни имущества оно амортизируется линейно в зависимости от
числа оставшихся лет.
Проще всего представить амортизацию себе амортизацию с двойным
наклоном по балансу можно, снова рассмотрев пример с 10000-долларовой
компьютерной системой, амортизируемой за десять лет с нулевой остаточной
стоимостью. При линейной амортизации система каждый год
амортизировалась бы на 10% в год. В методике с двойным наклоном она будет
амортизироваться первые пять лет на 20% от текущей стоимости. Другими словами, в
первый год сумма амортизации составит $2000 ($10000 * 20% = $2000),
уменьшив текущую стоимость до $8000. За второй год амортизация составит
$1600 ($8000 * 20% = $1600), что даст в конце года стоимость $6400. Этой
модели амортизация будет следовать вплоть до конца пятого года.
Далее будет применяться линейный метод с темпом амортизации 20% в год
(полная стопроцентная амортизация, деленная на оставшиеся пять лет).
Процесс амортизации для данного примера показывает таблица 10.4.
Таблица 10.4. Амортизация за 10 лет по балансу с двойным наклоном
Цена при покупке
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
$10000
Остаточная стоимость
$0
$0
$0
$0
$0
$0
$0
$0
$0
$0
Текущий год
1
2
3
4
5
6
7
8
9
10
. ■.. м-
Текущая стоимость
$8000.00
$6400.00
$5120.00
$4096.00
$3.276.80
$2621.24
$1966.08
$1310.72
$655.36
$0.00
Сравните таблицы 10.3 и 10.4, и вы увидите, что в первые два года метод с
двойным наклоном по балансу амортизирует быстрее, чем метод суммы цифр
лет, а затем более медленно. Вы также заметите, что начиная с шестого года в
методе двойного наклона скорость амортизации постоянна и равна $655.36 в год.
Нет нужды говорить, что вычисления текущей амортизации в методе
двойного наклона по балансу несколько сложнее, поскольку годовая сумма
амортизации зависит от стоимости имущества в начале года, формула амортизации
для текущего года следующая:
Текущая амортизация =
(Начальная стоимость - Остаточная стоимость)*(2 / Период)
Заметьте, что на период амортизации делится 2, поскольку темп линейной
амортизации удваивается.
С программной точки зрения выполняемая кодом обработка проще всего
выражается следующей формулой, где п — текущий год амортизации:
Финансовые расчеты 393
Текущая амортизация(л) =
(Конечная стоимость(л—1) - Остаточная стоимость)*(2 / Период)
В начале этого ряда должно выполняться следующее граничное условие:
Текущая амортизация(1) =
(Цена покупки - Остаточная стоимость)*(2 / Период)
Формула для стоимости в конце каждого года, очевидно, такова:
Конечная стоимость(л) =
Конечная стоимость(/г-1) - Текущая амортизация(л)
В зависимости от реализации такое вычисление может стать, возможно,
хорошим кандидатом для рекурсии. Однако в программе ddb.cpp вычисления
производятся просто в цикле for.
Перед тем, как двигаться дальше, вспомните, что только что рассмотренная
формула расчета применяется лишь к первой половине всего периода
амортизации. (В ddb.cpp эта половина округляется, так что для периода амортизации
в пять лет она будет равна трем годам.) Для оставшихся лет периода
амортизации будет применяться простой линейный метод. Другими словами, после
истечения половины всего периода вы используете для суммы ежегодной
амортизации следующую формулу:
Амортизация —
(Текущая стоимость — Остаточная стоимость) / (Оставшиеся годы)
В программе ddb.cpp для расчета амортизации на оставшийся срок
вызывается на самом деле функция sld() из самого первого примера. Теперь, когда мы
хорошо разобрались во всех моментах метода амортизации по балансу с двойным
наклоном, рассмотрите код программы ddb.cpp, реализующую данный метод.
(ЗА1\
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Метод баланса с двойным наклоном наиболее часто применяется в
бухгалтерских расчетах амортизации, поэтому понимание его работы
гораздо важнее, чем освоение предыдущих двух методов.
Код программы для амортизации по балансу
с двойным наклоном
Вот код программы ddb.cpp:
// double-declining balance depreciation
#inelude <iostream>
#inelude <cmath>
using namespace std;
double sId^double cost, double salvage, int lifetime)
{
return (cost - salvage) / lifetime;
}
394
Глава 10
7
double roundtohundreds(double roundvalue)
<
roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
}
double ddb(double cost/ double salvage, double lifetime,
int deprecyear)
;<
double multiplier, currentdeprec, valuetodeprec, currentvalue;
int breakpoint, i;
-i
if (ceil((lifetime /2) +0.5) > ceil((lifetime / 2) - 0.5))
breakpoint = ceil((lifetime / 2) + 0.5);
else
breakpoint = ceil((lifetime /2) -0.5);
multiplier = 2 / lifetime;
currentvalue = cost;
" valuetodeprec = currentvalue - salvage;
if (deprecyear < breakpoint)
j breakpoint = deprecyear;
,fj for (i я 1; i < breakpoint + 1; i++) {
*■ valuetodeprec — currentvalue - salvage;
currentdeprec = valuetodeprec * multiplier;
* cout « "Current depreciation for year " « i « " is: "
« currentdeprec « endl;
currentvalue = currentvalue - currentdeprec;
cout « "End-of-year depreciated value is: " « currentvalue «
endl;
. )
j if (deprecyear > breakpoint) {
4 currentdeprec = sld(currentvalue, salvage, (lifetime -
■breakpoint));
ь ii for (i=breakpoint + 1; i < deprecyear + 1; i++) {
* cout « "Current depreciation for year " « i « " is: "
"» « currentdeprec « endl;
currentvalue = currentvalue - currentdeprec;
, cout « "End-of-year depreciated value is: "
« roundtohundreds(currentvalue) « endl;
, return currentvalue;
'J)
* int main{)
double cost, salvage, currentval, lifetime;
int currentyear;
J cout « "Enter the original cost of the asset: ";
-i cin » cost;
Финансовые расчеты 395
4 cout « "Enter the salvage value of the asset: ";
cin » salvage;
cout « "Enter the lifetime in years of the asset (integer): ";
cin » lifetime;
cout « "Enter the year through which to compute depreciation: ";
cin » currentyear;
» i
' cout « "The double-declining balance depreciation "
« "of the asset is as follows:" « endl;
currentval = ddb(cost, salvage, lifetime, currentyear);
■ " return 0;
: j „__
( ПРИМЕЧАНИЯ
Объяснений в программе ddb.cpp заслуживает, по крайней мере, функция
ddb(). Функции sld() и roundtohundreds() вы уже видели, a main() не делает
ничего особенного; просто получает от пользователя значения и в конце
записывает текущую стоимость имущества в переменную currentval. (Заметьте,
что пока значение этой переменной никак не используется.)
Итак, давайте поближе рассмотрим обработку, выполняемую функцией
ddb(), которая наиболее интересна (и, вероятно, наиболее запутанна).
double ddb(double cost, double salvage, double lifetime,
int deprecyear)
{
double multiplier, currentdeprec, valuetodeprec, currentvalue;
int breakpoint, i;
Как и в случае других функций амортизации, ddb() принимает цену
покупки, остаточную стоимость, период амортизации и год, для которого нужно
рассчитать амортизацию. Функция также объявляет несколько переменных для
вычислений и определяет значение breakpoint, соответствующее точке
половины периода амортизации:
if (ceil((lifetime / 2) + 0.5) > ceil((lifetime / 2) - 0.5))
breakpoint = ceil((lifetime / 2) + 0.5);
else
breakpoint = ceil((lifetime /2) - 0.5);
Здесь оператор if проверяет, является ли период амортизации четным либо
нечетным. Если он четный, код устанавливает значение breakpoint равным
половине периода (так, для периода амортизации в 6 лет это значение будет
равно трем). Если нечетный, код устанавливает значение breakpoint на
первый год после половинного значения, т. е. для периода амортизации в 5 лет это
значение снова будет равно 3.
Переменная multiplier содержит процент амортизации — помните, что это
амортизация с двойным наклоном, и множитель вдвое больше того, что был
бы в случае линейной амортизации. Переменная currentvalue содержит стой-
396
Глава 10
мость (которая в данный момент равна первоначальной цене имущества), и
valuetodeprec содержит сумму, которая должна быть амортизирована. Вот код:
multiplier = 2 / lifetime;
currentvalue = cost;
valuetodeprec = currentvalue - salvage;
Следующий оператор if:
if (deprecyear < breakpoint)
breakpoint = deprecyear;
проверяет, следует ли год, для которого нужно возвратить амортизированную
стоимость, до или после точки половинного срока амортизации. Если этот год
наступает до половинной точки, breakpoint устанавливается равной этому
году. Такая методика упрощает циклы амортизации и позволяет избежать
дублирования кода.
Первый цикл for, показанный ниже, считает либо до половинной точки,
либо до года, на который рассчитывается амортизированная стоимость (до
меньшего из значений) и применяет к стоимости масштабированную
амортизацию вплоть до этого момента. Когда цикл заканчивается, переменная
currentvalue содержит амортизированную стоимость имущества.
for (i - 1; i < breakpoint + 1; i++) {
valuetodeprec = currentvalue - salvage;
currentdeprec = valuetodeprec * multiplier;
cout « "Current depreciation for year " « i « " is: "
« currentdeprec « endl;
currentvalue = currentvalue - currentdeprec;
cout « "End-of-year depreciated value is: " «
currentvalue « endl;
}
Второй оператор if:
if (deprecyear > breakpoint) {
служит двоякой цели; если год для расчета амортизации наступает до средней
точки, оператор гарантирует, что функция не будет далее производить
амортизацию. То же самое относится к случаю, когда эти два года совпадают. Если
расчетный год больше breakpoint, код выполняет линейную амортизацию для
каждого года вплоть до расчетного (или до конца всего срока амортизации,
если они равны).
В цикле for производится линейная амортизация текущей стоимости, пока
не будет достигнут конечный год, о котором говорилось выше:
currentdeprec = sld(currentvalue, salvage,
(lifetime - breakpoint));
for (i=breakpoint +1; i < deprecyear + 1; i++) {
cout « "Current depreciation for year " « i « " is: "
« currentdeprec « endl;
currentvalue = currentvalue - currentdeprec;
cout « "End-of-year depreciated value is: "
« roundtohundreds(currentvalue) « endl;
}
}
return currentvalue;
}
Финансовые расчеты 397
Если вы запустите программу и введете для года, на который нужно
рассчитать амортизацию, значение 10 (что соответствует всему периоду
амортизации), будет генерирован следующий вывод:
Enter the original cost of the asset: 10000
Enter the salvage value of the asset: 0
Enter the lifetime in years of the asset (integer): 10
Enter the year through which to compute depreciation: 10
The double-declining balance depreciation of the asset is as
follows:
Current depreciation for year 1 is: 2000
End-of-year depreciated value is: 8000
Current depreciation for year 2 is: 1600
End-of-year depreciated value is: 6400
Current depreciation for year 3 is: 1280
End-of-year depreciated value is: 5120
Current depreciation for year 4 is: 1024
End-of-year depreciated value is: 4096
Current depreciation for year 5 is: 819.2
End-of-year depreciated value is: 3276.8
Current depreciation for year 6 is: 655.36
End-of-year depreciated value is: 2621.44
Current depreciation for year 7 is: 655.36
End-of-year depreciated value is: 1966.08
Current depreciation for year 8 is: 655.36
End-of-year depreciated value is: 1310.72
Current depreciation for year 9 is: 655.36
End-of-year depreciated value is: 655.36
Current depreciation for year 10 is: 655.36
End-of-year depreciated value is: 0
Функции, связанные с рентой
До сих пор в этой главе мы говорили о том, как вычислять амортизацию
имущества со временем. Вы познакомились с тремя возможными методами и
функциями, которые требовались для их реализации. Однако, к счастью,
активы не всегда обесцениваются, — иногда их ценность со временем даже
возрастает. В совершенном мире активы бы все время росли, а долги уменьшались.
В оставшейся части главы вы будете изучать ренту и то, как с ней
обращаться при анализе финансовой информации, — текущей стоимости пакета
вложений, размера выплат по ссуде и т. д. Расчеты значений ренты часто
выглядят гораздо сложнее, чем операции с амортизацией, но для большинства из
нас результаты их оказываются гораздо приятнее. Более того, вы, вероятно,
будете производить расчеты ренты гораздо чаще, чем расчеты амортизации,
потому что рента имеет очень много разновидностей и применений.
Некоторые определения. При изучении ренты вам снова и снова будут
встречаться некоторые термины. Ниже даются определения самых
распространенных и вездесущих понятий; другие будут объясняться по ходу изложения.
♦ Процентная ставка. Рента включает в себя имманентное понятие
процентной ставки, выплачиваемой или взимаемой в зависимости от
размеров основного капитала. Величина процентной ставки в большинстве
398
Глава 10
случаев будет лежать где-то между 2.9% (для автомобилей) и 14-16%
(для высокооборотных вложений). В общем случае при расчетах ренты
вы задаетесь некоторым фиксированным значением процентной ставки
и строите все вычисления вокруг этой величины.
♦ Взносы. Хотя можно вычислять ренту исходя из предположения о
единственном начальном вложении денег, остающемся статическим в
течение всего срока ренты, обычно все же делаются регулярные взносы в
ренту (или производятся частичные выплаты ссуды, если вы обрабатываете
ссуду). Взносы занимают центральное место в расчетах ренты; в
следующих разделах вы будете разбирать ситуации, где взносов не делается, и
ситуации, требующие регулярных взносов,
♦ Конечная стоимость. Конечная стоимость ренты — это ее «зрелая»
стоимость, т. е. ценность ренты к моменту, когда истекает ее срок.
Вычисление конечной стоимости лежит в основе большинства финансовых
расчетов, и первая разбираемая нами программа будет посвящена расчету
конечной стоимости ренты для единственного начального взноса.
Вычисление конечной стоимости при единственном
начальном взносе
При вычислении конечной стоимости ренты нужно принимать во внимание
два главных момента: стоимость ренты в начале расчетного периода и то,
делаются ли в нее регулярные вложения на протяжении всего ее срока.
Простейшим является случай, когда для ренты устанавливается начальная стоимость
и дальнейших вкладов не делается.
Лучшим примером такого типа является перевод плана 401 (к) или другого
пенсионного плана в какой-либо план с фиксированной ставкой, когда вы
вкладываете все деньги в начале планового периода и оставляете фонд
нетронутым до выхода на пенсию.
Давайте снова воспользуемся таблицей, чтобы наглядно представить себе
работу ренты подобного рода. Таблица 10.5 показывает стоимость ренты в
конце каждого из первых семи лет ее срока.
Таблица 10.5. Стоимость ренты в первые семь лет ее срока
Стоимость в начале года
(Начальный вклад) $10000.00
$11000.00
$12100.00
$13310.00
$14641.00
$16105.10
$17715.61
Процентная ставка
10%
10%
10%
10%
10%
10%
10%
Стоимость в конце года
$11000.00
$12100.00
$13310.00
$14641.00
$16105.10
$17715.61
$19487.17
Как видите, к концу седьмого года стоимость ренты почти удвоилась. Этот
факт известен как правило семерки, которое гласит, что рента должна удваи-
Финансовые расчеты 399
вать свою стоимость каждые семь лет (или, как в данном случае, почти
удваивать). Правило семерки всегда принимается в расчет теми, кто занимается
финансовым планированием. Если рассчитанная стоимость ренты не достигает
значений отдачи, близких к правилу семерки, то такая рента вряд ли будет
удачным размещением капитала. •
Итак, вы видите, что рента с каждым годом увеличивает свой прирост, —
т. е. прирост увеличивается по мере того, как сама рента включает в себя
прирост предыдущих лет. Программа fv.cpp производит этот ряд вычислений и
информирует вас о конечной стоимости ренты по истечении ее срока.
Имеется несколько способов расчета прироста для данного случая; в этой
программе мы применим простую математическую формулу. Рассмотрите
следующее выражение для текущей стоимости ренты (где п представляет
текущий год):
стоимость(п) = стоимость(гс - 1) * (1 + ставка)
Как и другие формулы, которые вы видели, эта представляется
подходящим кандидатом для рекурсии. Конечным условием рекурсии было бы
следующее:
стоимость(1) = Начальная стоимость * (1 + ставка)
Но, как вы, вероятно, знаете, рекурсия не обязательно будет самым
эффективным способом для вычислений такого типа. На самом деле, если вы
рассмотрите текущую стоимость как функцию ряда предыдущих ее значений, то
увидите, что выражение можно упростить:
стоимость(п) = Начальная стоимость * (1 + ставка)"
Именно это соотношение и используется в программе fv.cpp.
Код для вычисления конечной стоимости
при единственном начальном взносе
Вот код программы fv.cpp.
#include <±ostream>
#include <cmath>
using namespace std;
double fv(double pv, double numyears, double rate)
i
return pv * pow((l + rate), numyears);
)
double roundtohundreds(double roundvalue)
{
roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
)
int main()
<
400
Глава 10
'; * double presentvalue, interest, lifetime;
i-** ^ cout « "Enter the present value of the annuity: ";
cin » presentvalue;
i
LV^ cout « "Enter the fixed interest rate for the annuity " « endl
'[ 1 « "(in the form O.xx for xx%) : ";
, ,i cin » interest;
■A
■n' cout « "Enter the lifetime in years of the annuity (integer): ";
*4j cin » lifetime;
Л-l cout « "The value of the annuity at maturation is: " «
' ~<i roundtohundreds(fv(presentvalue, lifetime, interest)) « endl;
. Чл return 0;
tTL________^_____________^
\j\PP
ПРИМЕЧАНИЯ
Программа fv.cpp довольно очевидна — вы просто получаете от
пользователя начальную информацию о ренте и затем вычисляете ее конечное значение.
Мы поближе рассмотрим некоторые моменты этой реализации, первым из
которых будет показанная здесь функция fv():
double fv(double pv, double numyears, double rate)
{
return pv * pow((l + rate), numyears);
}
Функция принимает настоящую стоимость ренты (начальный взнос), число
лет, составляющих срок ренты, и фиксированную ставку ренты. Как можно
видеть, функция просто возвращает в качестве своего значения результат
рассмотренной ранее формулы. Для вычисления (1 + ставка)11 вызывается стандартная
функция pow() из библиотеки C/C++. Первым ее параметром является
основание, а вторым — показатель степени, в которую нужно возвести основание.
Функцию roundlohundredsO вы уже видели, так же как и некоторые
неприятности, которые могут случиться при ее использовании. Здесь вам не
нужно их опасаться, так как программа вызывает roundtohundreds() только
при выводе стоимости ренты; функция не меняет никаких значений.
cout « "The value of the annuity at maturation is: " «
roundtohundreds(fv(presentvalue, lifetime, interest)) « endl;
Функция main() только производит ввод необходимых значений,
вычисляет конечную стоимость и выводит ее на экран. Следующий листинг
показывает примерный вывод программы fv.cpp.
Enter the present value of the annuity: 10000
Enter the fixed interest rate for the annuity
(in the form O.xx for xx%): .10
Enter the lifetime in years of the annuity (integer): 20
The value of the annuity at maturation is: 67275
Финансовые расчеты 401
Расчет конечной стоимости ряда взносов
Теперь, когда вы познакомились с простыми вычислениями,
определяющими конечную стоимость единственного взноса в ренту, будет нетрудно перейти
к вычислению стоимости ряда взносов.
Очевидно, если вы сегодня сделали вклад, по истечении 20 лет он станет
больше, чем если бы вы вкладывали те же деньги небольшими взносами. Если
вы вкладываете по $500 в год в течение 20 лет в 10%-ную ренту, с которой вы
работали в предыдущем разделе, то к концу этого срока рента будет стоить
всего $28637.50.
Однако большинство из нас, вероятно, будут делать взносы в ренту на
протяжении некоторого срока. Поэтому расчет стоимости ренты при взносах в
течение значительного времени является весьма важной задачей. На самом деле,
для IRA (пенсионный фонд) и других подобных вложений понимание
тонкостей ренты становится еще более ценным. Рассмотрите таблицу 10.6, которая
показывает стоимость IRA после того, как взносы размером $2000 делались в
течение 40 лет (с 25-летнего возраста), 30 лет (начиная с 35) и 20 лет (начиная
с 45). Расчеты в таблице делались исходя из фиксированной 10% -ной ставки.
Таблица 10.6. Стоимость ренты для различных сроков
Период накопления взносов, лет
20
30
40
Конечная стоимость
$114550.00
$328988.00
$885185.00
Заметьте, что разница в стоимости оказывается гораздо значительнее, чем
$40000, которую можно было бы ожидать (20 лет * $2000). Фактически, как
видите, 40-летняя рента стоит почти в восемь раз больше 20-летней. Это
согласуется с правилом семерки, которое говорит, что сумма должна удвоиться три
раза — 23 равняется 8.
Вычисления для стоимости ренты по прошествии некоторого срока и
регулярных взносах сходны с вычислениями ренты для фиксированного
начального взноса. Стоимость ренты к концу определенного года можно записать так:
стоимость(тг) = (стоимость(/г — 1) * (1 + ставка)) + Взнос
Здесь снова, как и в случае фиксированного взноса, можно было бы
применить рекурсию, с условием завершения, соответствующим концу первого года
срока ренты, стоимость в этот момент была бы
стоимость(1) = (Взнос * (1 + ставка)) + Взнос
Однако, как уже говорилось, рекурсия — не всегда наилучший способ
решения подобных задач, отчасти из-за того, что этот ряд можно легко свести к
единственному выражению, которое вы можете использовать для расчета
стоимости ренты к концу любого заданного срока, как показано ниже:
Конечная стоимость = Взнос * ((((1 + ставка)") - 1) / ставка)
402
Глава 10
Это уравнение описывает ренту, в которую вы делаете первоначальный
вклад и затем идентичные взносы в конце каждого периода начисления (для
взносов, делаемых в начале периода, уравнение будет несколько иным).
Код для вычисления конечной стоимости
при последовательных взносах
Программа fvseries.cpp показывает, как рассчитать стоимость ренты,
образуемой рядом фиксированных взносов. Вот ее код:
#include <iostream>
#include <cmath>
V using namespace std;
>■ double fvs (double payperperiod, double rate, double lifetime)
'" {
л.\ double multiplier;
Ц '
„ . multiplier = (pow((l + rate), lifetime) - 1) / rate;
c return multiplier * payperperiod;
- )
i
•i
* {double roundtohundreds(double roundvalue)
'<
, t roundvalue = ceil(roundvalue * 100);
\ ! return roundvalue / 100;
^ 'int main()
" Л
j double payment, rate, lifetime;
j
*1 cout « "Enter the value per period paid into the annuity: ";
cin » payment;
cout « "Enter the interest rate (in the form O.xx for xx%): ";
cin » rate;
' I
< | cout « "Enter the lifetime in years of the annuity (integer): ";
| cin » lifetime;
i ■
r* . cout « "The value of the annuity at maturation is: ";
>■ i cout « roundtohundreds (fvs (payment, rate, lifetime));
i* j
£»v j return 0 ;
ьл А*
Финансовые расчеты 403
| ПРИМЕЧАНИЯ
Как и в большинстве программ этой главы, ключевые вычисления
производятся в отдельной функции — здесь это fvs(). Давайте рассмотрим ее поближе.
double fvs(double payperperiod, double rate, double lifetime)
{
double multiplier;
multiplier = (pow((l + rate), lifetime) - 1) / rate;
return multiplier * payperperiod;
}
Как и функция fv(), которую вы видели в предыдущем разделе, fvs()
принимает три входных значения, два из которых те же самые, что и у fv() — это
rate и lifetime. Третий параметр, payperperiod, представляет размер
отдельных взносов в ренту. Переменная multiplier введена только для ясности —
можно было бы точно так же записать всю формулу в операторе return.
Главный оператор вычисляет в функции значение multiplier (он
соответствует выражению в скобках в последней из формул этого раздела). Затем
функция умножает результат на размер взноса за период начисления
(payperperiod) и возвращает стоимость ренты.
Как и в предыдущей программе, функция main() только передает входные
значения функции fvs() и выводит результат, что показано в следующем
листинге:
Enter the value per period paid into the annuity: 2000
Enter the interest rate (in the form O.xx for xx%): .10
Enter the lifetime in years of the annuity (integer): 40
The value of the annuity at maturation is: 885185
Часто оказывается, что требуется скомбинировать две формулы для конечной
стоимости, продемонстрированных в этой главе, что соответствует некоторому
начальному вкладу с последующими регулярными взносами стандартного
размера. Можно просто соединить соответствующие формулы, как показано ниже:
double fvs(double pv, double payperperiod, double rate,
double lifetime)
{
double multiplier, firsthalf;
firsthalf = pv * pow((l + rate), lifetime);
multiplier = (pow((l + rate), lifetime) - 1) / rater-
return (multiplier * payperperiod) + firsthalf;
>
Такую логику использует программа fvs2.cpp (находится на прилагаемой
дискете).
До сих пор вы, на основе имеющейся информации, вычисляли конечное
значение чего-либо в зависимости от некоторых условий. Часто вы заранее знаете,
что хотите получить в конце (например, определенную сумму денег при выходе
на пенсию), и хотите рассчитать, что нужно делать теперь, или в течение
какого-то времени, для достижения поставленной цели. В следующих разделах
исследуется вопрос, как рассчитать сегодняшнюю сумму вклада, необходимого
для достижения рентой заданной конечной стоимости.
404
Глава 10
Расчет одиночного взноса, достигающего заданной
конечной стоимости
Точно так же» как вычисление конечной стоимости при единственном
взносе проще вычислений конечной суммы для регулярных взносов, расчет
необходимого размера единичного взноса проще такого же расчета для регулярных
взносов. Первая программа, которую мы разберем, pv.cpp, вычисляет
необходимую начальную стоимость ренты, исходя из введенных пользователем
конечной стоимости, ставки и срока ренты.
Расчет начальной стоимости в такой ситуации довольно очевиден и основан
на уравнении, которое мы уже видели:
Конечная стоимость(п) = Начальная стоимость * (1 + ставка)"
Чтобы получить отсюда начальную стоимость, нужно только применить
самую элементарную алгебру, разделив обе части уравнения на (1 + ставка)11 и,
конечно, поменяв их местами:
Начальная стоимость = Конечная стоимость(п) / (1 + ставка)"
Эту формулу и использует pv.cpp в своих вычислениях.
Код для расчета взноса, достигающего
заданной стоимости
Вот код программы pv.cpp:
#include <iostream>
#include <cmath>
using namespace std;
double pv(double fv, double rate, double lifetime)
{
return (fv / (pow((l + rate), lifetime)));
}
double roundtohundreds(double roundvalue)
{
roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
}
int main()
{
double futureval, rate, lifetime;
cout « "Enter the future value of the annuity (at maturation) :
cin » futureval;
cout « "Enter the fixed interest rate for the annuity " « endl
« "(in the form 0. xx for xx%): ";
Финансовые расчеты 405
ЦшА cin » rate;
У* cout « "Enter the lifetime in years of the annuity (integer): ";
,' ' cin » lifetime;
»; ,*i
t" '
Г -"1 cout « "The present value of the annuity is:";
j'.\ cout « roundtohundreds (pv(futureval, rate, lifetime));
.:. return 0;
T3J '
I ПРИМЕЧАНИЯ
Как и в других программах, важнейшие вычисления производятся в
функции pv(). Давайте рассмотрим эту функцию, а затем вывод программы.
double pv(double fv, double rate, double lifetime)
{
return (fv / (pow((l + rate), lifetime)));
}
Как и следовало ожидать из приведенной выше формулы, функция
принимает в качестве конечную стоимость, ставку и срок ренты. Затем она применяет
к ним выражение для начальной стоимости и возвращает полученное значение.
Код функции main() просто получает вводимые пользователем числа и
выводит конечный результат программы. Это делает следующий оператор:
cout « "The present value of the annuity is:";
cout « roundtohundreds(pv(futureval, rate, lifetime));
Здесь снова появляется функция roundtohundreds() для вывода результата
в долларах и центах. Вы можете протестировать программу,
воспользовавшись значениями из примера fv.cpp, — введя в качестве конечной суммы
67275, вы должны получить начальный вклад размером $10000:
Enter the future value of the annuity (at maturation): 67275
Enter the fixed interest rate for the annuity
(in the form O.xx for xx%): .10
Enter the lifetime in years of the annuity (integer): 20
The present value of the annuity is:10000
Расчет взносов, необходимых для достижения
указанной конечной стоимости
Мы уже говорили, что люди обычно вкладывают деньги в ренту в виде
регулярных взносов в течение какого-то времени. Очень может быть, что им
захочется узнать, каков должен быть размер этих взносов, чтобы рента достигла
желаемой конечной суммы. Точно так же, как в предыдущем примере, для
расчета необходимой суммы взносов придется применить к исходной формуле
некоторый алгебраический трюк. Вот уже известная формула для конечной
стоимости ряда взносов:
Конечная стоимость = Взнос * ((((1 + ставка)") - 1) / ставка)
406
Глава 10
Разделив, как и раньше, обе ее части на множитель, стоящий справа, мы
получим формулу для расчета необходимого размера взносов по заданным
конечной сумме, процентной ставке и сроку ренты:
Взнос = Конечная стоимость / ((((1 + ставка)") - 1) / ставка)
Код для расчета ряда взносов, достигающих
целевой суммы
Программа pvseries.cpp выполняет расчет вносов, необходимых для
достижения заданной цели, пользуясь приведенной выше формулой.
F #include <iostream>
,#include <cmath>
lusing namespace std;
t
double pvs(double fv, double rate, double lifetime)
' _.{
V return fv / ((pow((l + rate), lifetime) - 1) / rate);
• )
' ^double roundtohundreds(double roundvalue)
\l
. roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
i
* jint main()
double futurevalue, rate, lifetime;
j
' cout « "Enter the future value of the annuity: ";
I cin » futurevalue;
I
cout « "Enter the interest rate of the annuity " « endl «
*■ Л "(in the form O.xx for xx%): ";
cin » rate;
■ i cout « "Enter the lifetime in years of the annuity (integer): ";
1 - cin » lifetime;
■■V
. \ cout « "The present value of the annuity is: " «
t"'wi roundtohundreds (pvs (futurevalue, rate, lifetime)) « endl;
'■ return 0;
.. )
Финансовые расчеты ; 407
| ПРИМЕЧАНИЯ
Все вычисления программы происходят в функции pvs(). Вот она:
double pvs(double fv, double rate, double lifetime)
{
return fv / (<pow((l + rate), lifetime) - 1) / rate);
}
Как уже говорилось, функция pvs() ставит вычисление конечной стоимости
с ног на голову, деля конечную стоимость на коэффициент накопления и
получая, таким образом, взносы, которые нужно делать для достижения
поставленной цели. Она возвращает результат вызывающей функции main(),
которая выводит его значение:
cout « "The present value of the annuity is: " «
roundtohundreds(pvs(futurevalue, rate, lifetime)) « endl;
Ваша старая знакомая, функция roundtohundreds(), преобразует сумму в
доллары и центы. Вы можете проверить результаты программы, инвертировав
значения из примера программы fvseries.cpp. Однако интереснее попробовать
что-то из реальной жизни. Например, предположим, вам сейчас 25 лет и вы
рассчитываете уйти на пенсию в 65 с пятью миллионами долларов. Программе
pvserics.cpp вполне по силам определить, сколько вы должны ежегодно
выплачивать в фонд, и вывод программы для этого случая показан ниже:
Enter the future value of the annuity: 5000000
Enter the interest rate of the annuity
(in the form O.xx for xx%): .10
Enter the lifetime in years of the annuity (integer): 40
The present value of the annuity is: 11297.1
Выходит, вам всего-то и нужно, что откладывать по $11297.10 в год — не
так уж и плохо.
В следующем разделе вы изучите другое приложение формул ренты:
калькулятор ссуды.
Написание простого калькулятора ссуды
На протяжении этой главы мы рассматривали разновидности ренты в
качестве инвестиций — долгосрочных вкладов, которые растут со временем. Но
есть иной вид ренты, который нужно рассмотреть: долгосрочные ссуды.
Наиболее распространенной их формой является закладная на дом (покупка дома
в рассрочку). Расчеты по закладной похожи на расчеты для ренты.
В общем случае, когда вы вычисляете взносы по закладной, вы должны
определить общую сумму ссуды (основную сумму), срок ссуды и процентную
ставку. В случае закладной пользователь, как правило, будет знать цену дома
и процент первого взноса за него. Программа loan.cpp запрашивает у
пользователя эту информацию и выдает размер ежемесячных взносов, а также
общую сумму первого взноса.
408
Глава 10
Код для расчета выплат по ссуде
Вот код программы loan.cpp:
#include <iostream>
#include <cmath>
using namespace std;
double payment(double loanamt, double rate, double years)
double months;
rate = rate / 12.00;
months = years * 12;
return loanamt * (rate / (1 - (1 / (pow((l + rate), months)))));
double roundtohundreds(double roundvalue)
<
roundvalue = ceil(roundvalue * 100);
return roundvalue / 100;
int main ()
{
double purchaseprice, loanamount, percentdown, term, interest;
cout « "Enter the purchase price of the house: ";
cin » purchaseprice;
cout « "Enter the percent down on the house " «
"(in the form O.xx for xx%): ";
cin » percentdown;
loanamount = purchaseprice - (purchaseprice * percentdown);
cout « "Enter the interest rate of the mortgage " « endl
« "(in the form O.xx for xx%): ";
cin » interest;
cout « "Enter the length of the mortgage in years: ";
cin » term;
cout « "The amount down will be; " «
purchaseprice - loanamount « endl;
cout « "The payments for the mortgage will be: " «
roundtohundreds(payment(loanamount, interest, term)) « endl;
return 0;
Финансовые расчеты
409
I ПРИМЕЧАНИЯ
Ключевой момент вычислений программы локализован в функции рау-
mcnt(), которая определяет ежемесячный взнос по закладной. Вот она:
double payment(double loanamt, double rate, double years)
{
double months;
rate = rate / 12.00;
months = years * 12;
return loanamt * (rate / (1 - (1 / (pow((l + rate), months)))));
}
Как вы видели и в случаях ренты, функция payment() принимает размер
ссуды, процентную ставку и число лет, на которое взята ссуда. Однако,
поскольку закладные вычисляются ежемесячно, функция выполняет
дополнительные преобразования параметров rate и years, приводя их к месячным
цифрам.
Функция делит на 12 параметр rate (вычисляя месячную ставку процента)
и умножает на 12 параметр years (получая число месяцев, за которое должна
быть выплачена ссуда). Наконец, вычисляется размер выплат, возвращаемый
в качестве значения функции. Выплаты рассчитываются исходя из размера
ссуды и функции ренты, которую вы видели много раз:
pow((l + rate), months)
Когда вы запустите программу, она выведет примерно следующее:
Enter the purchase price of the house: 200000
Enter the percent down on the house (in the form O.xx for xx%): .20
Enter the interest rate of the mortgage
(in the form O.xx for xx%): .075
Enter the length of the mortgage in years: 30
The amount down will-be: 40000
The payments for the mortgage will be: 1118.75
Объединение различных вычислений
Как вы видели в последнем разделе, применение описанных в главе
функций к решению реальных задач относительно просто. Перед тем как закончить
эту главу, давайте рассмотрим одну из обычных, повседневных ситуаций, с
которой, вероятно, придется столкнуться и вам.
Для большинства людей является важным вопрос, сколько денег они
должны скопить к моменту выхода на пенсию. Очень немногие сегодня
рассчитывают прожить в старости на выплаты по социальному страхованию. Неплохо
знать, сколько вам потребуется отложить.
Расчет желаемых сбережений к пенсии является функцией нескольких
переменных, из которых важнейшей является сумма, которую вы, после выхода
на пенсию, планируете брать из пенсионного фонда ежемесячно. Кроме того,
нужно оценить, сколько вы собираетесь прожить на пенсии, каков процент
продолжающихся начислений, ставка процента в период, когда вы делаете
взносы в фонд, и сколько лет вы будете делать эти взносы.
410
Глава 10
Определение предполагаемых ежемесячных взносов до выхода на пенсию
лучше проводить в два этапа, как это делается в программе retire.cpp.
Сначала, исходя из предполагаемой суммы выплат из фонда, пенсионного периода и
процентной ставки программа вычисляет, сколько денег должно быть на
счете, когда вы выйдете на пенсию. Для этого она использует разновидность
формулы, применение которой для расчета выплат по ссуде, которую вы видели в
программе loan.cpp. Однако вместо расчета взносов здесь производится расчет
начальной суммы. Формула имеет вид:
Начальная сумма = Выплата / (ставка / (1 - (1 / (1 + ставка)месяцы)))
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Вычисления в этой программе несколько отличаются от сходных
вычислений этой главы, потому что основная сумма будет уменьшаться
ежемесячно, а не ежегодно.
Как только необходимая сумма сбережений в пенсионном фонде определена,
вычисление необходимых взносов в фонд становится простой задачей,
решенной в программе pvseries.cpp, с небольшой поправкой на то, что взносы будут
ежемесячными. Программа retire.cpp производит все эти вычисления и
выводит сумму, которую вы должны ежемесячно вносить в свой пенсионный фонд.
Код для расчета взносов в пенсионный фонд
Ниже следует код программы retire.cpp.
i '#include <iostream>
* -J #include <cmath>
' ,ausing namespace std;
i
1 .double fpayoutamount(double payment, double rate, double years)
4<
j double months;
, - rate = rate / 12.00;
•' i months — years * 12;
^i return payment / (rate / (1 - (1 / (pow((l + rate), months)))));
\" }
idouble pvs(double fv, double rate, double lifetime)
double months;
! rate = rate / 12.00;
' months — lifetime * 12;
' fc return fv / ((pow((l + rate), months) - 1) / rate);
'i'
i ,,double roundtohundreds (double roundvalue)
Финансовые расчеты 411
с.
t- i{
"■ \ roundvalue - ceil (roundvalue * 100);
;* , return roundvalue / 100;
[ |int main()
> double stipend, outrate, outlife;
(J ' double valueatretirement;
I ] double inrate, inlifetime;
r ■.
" j cout « "Enter the amount you want to remove monthly from
(r ] the fund " « endl;
\ j cout « "after retirement: ";
' 1 cin » stipend;
* i
* \ // cin » futurevalue;
* i
'f ,** cout « "Enter the annual interest rate of the annuity " « endl
."' j « "(in the form O.xx for xx%) : ";
cin » outrate;
: \
cout « "Enter the lifetime in payout years of the retirement
fund (integer): ";
cin » outlife;
// Compute what the fund must be worth at 0-point
valueatretirement = fpayoutamount(stipend/ outrate, outlife);
cout « "Enter the annual interest rate during the contribution
', j period " « endl;
'- j cout « "(in the form O.xx for xx%) : " ;
I cin » inrate;
1 cout « "Enter the contribution lifetime in years of the fund
f,4 (integer) : ";
1 j cin » inlifetime;
i
s j cout « "You must pay the following amount monthly into the fund: $";
.' ! cout « roundtohundreds(pvs(valueatretirement, inrate, inlifetime));
$■ '
return 0;
ПРИМЕЧАНИЯ
| ПР»
Большинство вычислений в программе retire.cpp достаточно очевидно и не
отличается существенным образом от того, что вы уже видели. Давайте
рассмотрим, однако, две функции, производящие вычисления. Первая функция,
fpayoutamount(), рассчитывает необходимую сумму пенсионного фонда на
момент выхода на пенсию, исходя из ежемесячных выплат, процентной ставки и
продолжительности пенсионного периода:
412
Глава Ю
double fpayoutamount(double payment, double rate, double years)
i
double months;
rate = rate / 12.00;
months = years * 12;
return payment / (rate / (1 - (1 / (pow((1 + rate), months)))));
}
Как видите, эта функция принципиально не отличается от payment() в
программе loan.cpp. Единственным отличием является то, что вместо выплаты
рассчитывается начальная сумма. Для обращения соответствующей формулы
Выплата = Начальная сумма * (ставка / (1 - (1 / (1 + ставка)мссяцы)))
нужно только разделить обе части на множитель справа в скобках:
Начальная сумма — Выплата / (ставка / (1 - (1 / (1 + ставка)месяцы)))
Функция возвращает в качестве своего значения начальную сумму фонда.
Затем функция pvs() вычисляет плату, которую нужно ежемесячно вносить
в фонд для достижения этой начальной суммы, исходя из указанного вами
периода накопления и процентной ставки в течение этого периода. Функция
имеет такой вид:
double pvs(double fv, double rate, double lifetime)
i
double months;
rate = rate / 12.00;
months = lifetime * 12;
return fv / ((pow((l + rate), months) - 1) / rate);
}
Заметьте, что она несколько отличается от прежней функции pvs() —
расчеты здесь производятся не для ежегодных, а для ежемесячных взносов.
Вот и все — остальная часть программы занимается получением
информации от пользователя и выводом результатов. Если вы запустите retire.epp, ее
вывод будет выглядеть примерно так:
Enter the amount you want to remove monthly from the fund
after retirement: 4000
Enter the annual interest rate of the annuity
(in the form O.xx for xx%): .08
Enter the lifetime in payout years of the retirement fund (integer):
20
Enter the annual interest rate during the contribution period
(in the form O.xx for xx%): .12
Enter the contribution lifetime in years of the fund (integer): 40
You must pay the following amount monthly into the fund: $40.65
ГЛАВА
1
г
" С
1
■ ■. ■
**■
-*7 -f
J-4 %
t
^ t
шышаташл»^
ткшШ
И.л1
avg.cpp
moreavg.cpp
Ларе Кландер
prob.cpp
reganal.cpp
й ■
!4 .. ,*$
Щ' x i
414
Глава 11
Щ£ ак уже подчеркивалось в 10-й главе, сильнейшей стороной компьютеров
Жтявляется выполнение расчетов. Кроме того, компьютеры очень хорошо
могут комбинировать и анализировать огромные объемы данных. Одна из
дисциплин, в которых часто приходится производить подобную обработку — это
статистика.
В этой главе мы разберем некоторые из стандартных видов статистического
анализа и представим несколько программ, производящих необходимые
вычисления. Статистические задачи являются чем-то вроде вызова, брошенного
программисту; их чрезвычайно сложно запрограммировать аккуратно и без
ошибок. Комбинирование многочисленных фрагментов, а затем вывод из них
другой информации — это головоломная работа, способная поставить в тупик
даже самого опытного программиста. Здесь мы покажем только самые
простые статистические программы, уделяя главное внимание функциям,
реализующим фундаментальные статистические операции.
Введение в средние значения
Одним из фундаментальных понятий в статистике является среднее.
Однако даже это простое понятие может вводить в заблуждение тех, кто не имеет
опыта в статистике, — существует несколько типов средних значений,
каждый из которых имеет смысл в определенных ситуациях. Вообще говоря,
среднее подразумевает некоторое типичное значение, или наиболее вероятное, в
наборе численных данных. Когда вы собираете данные, результаты от
наблюдения к наблюдению могут варьироваться в разных отношениях, даже когда
условия по видимости предполагаются одинаковыми. Работа со средними
позволяет делать общие утверждения относительно природы данных, даже
меняющихся в широком диапазоне значений.
Диапазон значений может варьироваться в силу разных причин, наиболее
очевидной из которых является просто вариация самих событий. Например,
бросая две игральных кости, можно констатировать, что результат броска
будет лежать в диапазоне от 2 до12. Однако общее утверждение относительно
среднего значения для конкретной серии бросков может дать диапазон всего от
4 до 9 (к примеру).
Кроме того, во многих случаях — очень часто в социальных
дисциплинах — будет присутствовать ошибка наблюдения и измерения. Если вы,
например, в психологическом исследовании измеряете реакцию пациента, то
можете получить различные отсчеты из-за малых вариаций момента нажатия
кнопки секундомера.
Наконец, некоторые вариации совершенно нормальны, как это имеет место
в природных явлениях. Рассмотрите владельца виноградника, который
производит наблюдения относительно числа ягод в гроздьях. Если бы владелец
хотел получить информацию о распределении плодов по всему винограднику, он
мог бы взять первую лозу в каждом из рядов и сосчитать число гроздьев на
лозе. Просмотрев 20 рядов виноградника, он нашел бы, что каждая лоза
содержит от 1 до 6 гроздьев. В подобном случае для определения среднего
количества винограда может потребоваться одно из нескольких вычислений.
Статистические расчеты 415
Среднее арифметическое, медиана и модус
Несмотря на все многообразие средних, самыми распространенными
вычислениями среднего являются определение среднего арифметического, медианы
и модуса. Каждое из них отличается от двух других, но все они —
«правильные* средние, если используются в адекватном контексте. Чтобы получше
разобраться в этом, давайте поближе посмотрим на данные о винограднике.
Сами сырые данные приведены в таблице 11.1.
Таблица 11.1. Элементарные сведения, собранные владельцем виноградника
Номер лозы
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Число гроздьев
5
6
5
5
3
6
5
3
1
4
5
3
1
6
5
2
5
2
3
4
Среднее арифметическое — это то среднее, которым пользуется
большинство людей. Оно не представляет собой ничего другого, как просто сумму всех
значений, деленное на число этих значений. В математической нотации
среднее записывается таким образом:
среднее арифметическое = I {xi. . хп) / п
Сумма всех 20-ти значений в таблице 11.1 равна 79 и, следовательно,
среднее будет рассчитываться как (79 / 20) = 3.95. Мы получили в этом примере
среднее, равное 3.95, хотя нельзя ожидать, что в винограднике можно найти
лозу с 3.95 гроздьями; это бессмысленно.
416
Глава 11
Другим типом среднего является медиана. Это серединное значение в
наборе данных — т. е. такое, что ровно половина значений располагается выше, и
ровно половина ниже его. В случае нашего виноградника имеется 20
значений; это четное число, так что медиана будет расположена между двух
значений, — десять значений будут находиться выше, а десять ниже медианы.
В таблице 11.1 десять значений располагаются между 0 и 4, и десять между 5
и 7. Это означает, что медиана будет находиться на полпути между 4 и 5, т. е.
будет равна 4.5. Если бы число точек в наборе данных было нечетным, то
средняя точка — медиана — была бы действительной точкой данных, вверх и вниз
от которой находилось бы одинаковое число точек.
Модус тоже является своего рода средним; это значение, наиболее часто
встречающееся в наборе данных. Если нанести данные таблицы 11.1 на
диаграмму, как показано на рис. 11.1, вы сразу увидите модус данных. В
винограднике больше всего лоз с пятью гроздьями; это и есть модус.
Различные ситуации требуют применения различных средних. Если бы
данные были абсолютно симметричны, то среднее арифметическое, медиана и
модус равнялись бы одному и тому же числу. На самом деле для данных,
которые мы до сих пор анализировали, это почти верно. Если среднее, медиана и
модус не равны, то данные не будут расположены симметрично вокруг
среднего. Это различие является тестом на симметрию. В случае виноградных
гроздьев данные почти симметричны. Потому среднее арифметическое имеет
смысл «среднего типического». Но есть другие ситуации, когда попытки
принять в качестве среднего среднее арифметическое могут приводить к
поразительным заблуждениям. Например, вы рассматриваете суммы
вознаграждений в большой компании, совет директоров которой заработал в прошлом году
на биржевых операциях 500 миллионов долларов. Среднее арифметическое в
данной ситуации могло бы означать, что каждый работник компании
заработал в прошлом году более миллиона. Однако в реальности может быть так, что
большинство служащих получили только по 50000 долларов; в этом случае
медиана будет лучше отражать положение дел.
После того, как мы вкратце рассмотрим программу avg.cpp, которая берет
данные, представленные в таблице 11.1, и производит для них вычисление
трех типов средних, мы перейдем к рассмотрению двух других типов среднего,
которые вам, может быть, придется вычислять в других ситуациях.
о
О
5 X
XXX <j>X in X / X
X X X cnX^X/X
i—h
4
X
X
X
X
ф
5
о.
О
сох
Я
§
1Г)
<з-
X
X
X
X
X
X
X
Рис. 11.1. Простая диаграмма для данных о лозах и гроздьях
Статистические расчеты . 417
Код
Вот код программы avg.cpp, вычисляющей три типа средних — среднее
арифметическое, медиану и модус — для ряда из 20 значений.
// Простые средние.
[#include <iostream>
|#include <cmath>
J#include <cstdlib>
|#define numvals 20
using namespace std;
int compare_int(int *a, int *b)
f
if (*a < *b)
return(-1);
else if (*a = *b)
return(0);
else
return(1);
)
double median(int valarr[], int numels)
{
double returnval — 0;
for (int i=0; (i < numels / 2); i++)
returnval = valarr[i];
if (numels % 2)
returnval - valarr[i];
else
returnval = (returnval + valarr[i]) / 2;
return returnval;
}
double mode(int valarr[], int numels)
{
int instances = 0, tempinstances - 1, i = 1;
int tempmode, returnmode = 1;
tempmode = valarr[0];
while (i < numels) {
while (valarr[i] == tempmode) {
i++;
tempinstances++;
}
if (tempinstances > instances) {
returnmode - tempmode;
instances - tempinstances;
)
tempinstances = 1;
tempmode = valarr[i];
i++; "
14 Эш.1208
418
Глава 11
. >
return returnmode;
Ч
double arithjnean(int valarr[J, int numels)
■ ■ double average - 0;
*■
"*■ for (int i=0; i < numels; i++)
average = average + valarr[i];
- •" return average / numels;
int main()
\<
■ . int values[numvals] — { 5, 6, 5, 5, 3,
6, 5, 3, 1, 4,
5, 3, 1, 6, 5,
: 2, 5, 2, 3, 4 J;
double average =0;
cout « "Defined values:" « endl;
for (int i=0; i< numvals; i++)
'., cout « values [i] « " " ;
cout « endl ;
* cout « "The arithmetic mean of the values: ";
- ■* cout « arith_mean (values, numvals) « endl;
' qsort(values, numvals, sizeof(int),
(int (*) (const void*, const void*)) compare_int);
"' " cout « "Sorted values:" « endl;
for (i=0; i< numvals; i++)
J cout « values[i] « " ";
cout « endl;
cout « "The median of the values: ";
cout « median(values, numvals) « endl;
■j
cout « "The mode of the values: ";
cout « mode(values, numvals) « endl;
* return 0;
I пр^
ПРИМЕЧАНИЯ
Давайте рассмотрим элементы программы avg.cpp. Директивы в верхней
части программы довольно стандартны, за исключением следующего:
#define numvals 20
Это определение создает препроцессорную переменную numvals, которую
программа использует для указания размера массива. Это обычный метод, од-
Статистические расчеты 419
нако он работает только в случае, когда размер массива известен во время
разработки.
int compare_int(int *а, int *b)
{
if (*a < *b)
return(-1);
else if <*a = *b)
return(0);
else
return(1);
}
В стандартной библиотеке С определяется функция qsort(), которая
вызывается в нашей программе и которой мы еще будем говорить. Однако qsort()
принимает в качестве своего последнего параметра имя функции сравнения,
которая возвращает значение в соответствии со следующими спецификациями:
1. Если первый параметр меньше второго, функция возвращает -1.
2. Если первый параметр равен второму, возвращается 0.
3. Если первый параметр равен второму, возвращается 1.
В программе avg.cpp роль функции, передаваемой qsort(), играет сотра-
re_int(). Заметьте, что функция сравнения всегда должна возвращать целое
значение, хотя передавать ей можно любые типы. Однако вы должны
убедиться, что параметры функции сравнения соответствуют типу аргументов,
которые вы передаете qsort().
Как уже говорилось, медиана — это значение, расположенное прямо
посередине ряда значений, т. е. половина значений ряда находится выше, а
половина — ниже медианы. Функция median() в avg.cpp определяет это значение для
целого массива, с которым работает программа. Она принимает два параметра:
указатель на массив и значение, равное общему числу элементов массива.
double median(int valarr[], int numels)
{
double returnval = 0;
for (int i=0; (i < numels / 2); i++)
returnval = valarr[i];
Простой цикл for делит массив пополам (как вы помните, медиана
расположена посередине массива) и проходит по нему до этого серединного значения;
по выходе из цикла это значение будет содержаться в переменной returnval.
Заметьте — функция предполагает, что данные идут в восходящем порядке.
Если вы попытаетесь передать функции несортированные данные, то получите
неверный результат. По достижении серединного значения функция должнв
определить, является ли число элементов в массиве четным или нечетным, что
и делает следующий оператор if:
if (numels % 2)
returnval = valarr[i];
else
returnval — (returnval + valarr[i]) / 2;
Для определения четности числа элементов программа применяет
операцию взятия по модулю, возвращающую остаток от деления нацело. Если число
элементов нечетное, код устанавливает returnval равной следующему элемен-
14*
420
Глава 11
ту массива (деление нацело всегда округляет результат в меньшую сторону).
Если же число элементов четно, то в качестве медианы берется половина сумт
мы следующего элемента и returnval. В программе avg.cpp этот код
фактически складывает элементы valarr[9] и valarr[10] (по порядку это десятый и
одиннадцатый) и делит сумму пополам. Затем функция возвращает
полученное значение вызывающей программе оператором return.
return returnval;
}
Функция mode() несколько сложнее. Хотя на графике модус виден сразу,
как на рис. 11.1, определить модус ряда значений на C/C++ не так просто.
Придется пойти на некоторые ухищрения. Если отвлечься от смысла модуса,
алгоритм вычислений можно сформулировать примерно так: «Разбить
сортированный список значений на ряд меньших списков, каждый из которых содержит
одинаковые значения. Пересчитать число элементов в этих списках, и список с
наибольшим числом элементов будет соответствовать модусу данных».
Если вы абстрагируете модус таким образом, определение его в программе
на C/C++ станет гораздо более простым. Теперь можно представить
определение модуса в виде пары циклов, которые разбивают сортированный массив на
ряд меньших массивов.
( ЗАМЕЧАНИЕ ПРОГРАММИСТА
Такая абстракция модуса и функция mode() в этой программе будут
работать только с сортированным массивом. Если mode() не знает,
сортирован массив или нет, она должна перед обработкой
сортировать его. Возможно, вы захотите даже скопировать исходный массив в
другой массив и сортировать последний, если исходный массив
изменять нежелательно.
Функция mode() принимает два параметра: ссылку на массив и значение,
задающее число элементов в массиве. Затем функция определяет некоторые
локальные переменные для хранения информации о числе элементов и для
возвращаемого значения. Вот эти объявления:
double mode(int valarr[], int numels)
{
int instances = 0, tempinstances - 1, i = 1;
int tempmode, returnmode = 1;
Затем программа присваивает переменной tempmode значение первого
элемента массива и входит в цикл while, в котором выполняется основная
обработка. Этот цикл повторяется до тех пор, пока не будет обработан весь массив:
tempmode = valarr[0];
while (i < numels) {
Внутри внешнего цикла имеется второй, который повторяется для каждой
последовательности значений списка. Другими словами, если значение valarr[0]
равно 1, внутренний цикл будет повторяться до тех пор, пока valarr[i] не
станет отличным от 1. при каждом рпоходе внутреннего цикла программный код
увеличивает счетчик массива i, а также переменную tempinstances, которая
Статистические расчеты
421
хранит число найденных элементов с данным значением. Код внутреннего
цикла выглядит так:
while (valarr[i] ~ tempmode) {
i++;
tempinstances++;
}
Программа выходит из внутреннего цикла всякий раз, когда значение в
массиве меняется. Затем в операторе if определяется, каково число элементов
данной последовательности по отношению тому, которое было наибольшим до
сих пор. Для этого сравниваются переменные tcmpinstanees и instances. Если
число элементов в текущей последовательности больше того, что в данный
момент записано в instances, то код в блоке if присваивает возвращаемому
значению текущее значение массива и устанавливает наибольшее число элементов
равным текущему числу элементов:
if (tempinstances > instances) {
returnmode = tempmode;
instances - tempinstances;
)
Это сравнение является ключевым моментом определения модуса.
Оставшаяся часть внешнего цикла while просто сбрасывает временную переменную
для числа элементов и присваивает временному модусу значение текущего
элемента массива, после выхода из цикла функция возвращает значение
переменной returnmode вызывающей программе.
tempinstances = 1;
tempmode = valarr[i];
i++;
}
return returnmode;
}
Как вы видели на рис. 11.1, в программе avg.cpp функция mode() должна
возвращать 5. Но эта функция будет работать со 100-элементным массивом
значений ничуть не хуже, чем с 20-элементным, и как таковая она может
стать ценным дополнением вашей библиотеки. Наиболее привлекательной ее
чертой является, возможно, то, что ей не нужно проходить по массиву снова и
снова, когда внешний цикл проходит по всем элементам и внутренний
повторят каждый раз тот же проход; здесь эта проблема решается благодаря
сортированному массиву.
Последней функцией перед main() является arith_mean(), которая
принимает в качестве параметров массив и число элементов, суммирует значения и
возвращает эту сумму, поделенную на число элементов в массиве:
double arith_mean(int valarr[], int numels)
<
double average = 0;
for (int i=0; i < numels; i++)
average — average + valarr[i];
return average / numels;
)
422
Глава 11
Этот код очень просто адаптировать для обработки массивов значений float
или double, поскольку внутри функции нет ничего специфического для типа
int. Можно заставить функцию arith_mean() возвращать среднее для ряда
значений double, просто изменив ее заголовок, как показано ниже:
double arith_mean(double valarr[], int numels)
В программе avg.cpp мы работаем исключительно с целыми, что
чрезвычайно упрощает процесс сравнений. Функция main() программы объявляет и
инициализирует массив значений, а затем генерирует вывод для трех
различных типов среднего. Первая часть функции выглядит следующим образом:
int main()
{
int values[numvals] = { 5, 6, 5, 5, 3,
6, 5, 3, 1, 4,
5, 3, 1, 6, 5,
2, 5, 2, 3, 4 );
double average = 0;
cout « "Defined values:" « endl;
for (int i=0; i< numvals; i++)
cout « values[i] « " ";
cout « endl;
cout « "The arithmetic mean of the values: ";
cout « arithjmean(values, numvals) « endl;
Заметьте, что программа вычисляет среднее арифметическое до того, как
массив будет сортирован. Функция qsort(), упомянутая выше, производит
сортировку. В качестве первых двух параметров ей нужно передать массив и
число элементов; третий параметр соответствует размеру каждого элемента
массива. Последний параметр представляет собой обобщенное объявление
функции сравнения и ее имя. Вот вызов qsort():
qsort(values, numvals, sizeof(int),
(int (*) (const void*, const void*)) compare_int);
Затем программа выводит сортированный массив и переходит к выводу
медианы и модуса, вызывая соответствующие функции:
cout « "Sorted values:" « endl;
for (i=0; i< numvals; i++)
cout « values[i] « " ";
cout « endl;
cout « "The median of the values: ";
cout « median(values, numvals) « endl;
cout « "The mode of the values: ";
cout « mode(values, numvals) « endl;
return 0;
}
Статистические расчеты
423
Итак, когда вы соберете все это вместе, компилируете и запустите
программу, она вычислит три фундаментальных средних величины и выведет их на
экран. Вывод программы имеет следующий вид:
Defined values:
56553653145316525234
The arithmetic mean of the values: 3.95
Sorted values:
11223333445555555666
The median of the values: 4.5
The mode of the values: 5
Теперь, когда вы получили некоторое представление о работе со средними,
давайте перейдем к изучению некоторых других аспектов средних величин.
Другие распространенные виды средних
В дополнение к трем фундаментальным типам средних, обсуждавшихся до
сих пор, существуют еще четыре типа средних величин, которые часто
встречаются в статистике. Эти средние представляют собой величины, в принципе
отличные от тех, которыми мы занимались. Это взвешенное среднее, среднее
геометрическое, среднее квадратичное и квадратичная сумма. Для каждого
из них существуют свои методы расчета, так что мы рассмотрим их по порядку
перед тем, как перейти к программному примеру.
Среднее взвешенное
Взвешенное среднее является такой средней величиной, которая принимает
в расчет важность каждого значения для общей их суммы. Возьмем,
например, университетский курс по некоторому предмету. В данном конкретном
курсе доля оценок за домашние задания в окончательной оценке должна
составить 25%, доля зачета в середине семестра — 20%, доля контрольных
опросов — 15% и доля завершающего экзамена должна равняться 40%. Оценки
конкретного студента приведены в таблице 11.2.
Таблица 11.2. Оценки студента по конкретному предмету
Категория оценки
Домашние задания
Зачет
Контрольные
Экзамен
Оценка I
84
78
72
89 I
При таких условиях для вычисления окончательной оценки студента нужно
воспользоваться формулой взвешенного среднего, показанной ниже. Здесь w
соответствует весу конкретного значения, а х представляет само это значение:
взвешенное среднее — £ (w * х) / £ w
424
Глава 11
Другими словами, нужно умножить каждую оценку на соответствующий'
вес и сложить полученные значения. Затем эту сумму нужно разделить на
сумму весов (в данном случае она 1).
Для вычисления оценки студента сделать это довольно несложно:
оценка = ((.25 * 84) + (.20 * 78) + (.15 * 72) + (.40 * 89)) /
(.25 + .20 + .15 + .40)
Вычисление дает в итоге окончательную оценку 83 — отличную от 80.75,
которую студент получил бы, если мы просто взяли бы среднее арифметическое.
Если бы оценки студента по различным категориям сильнее отличались
друг от друга — скажем, он отлично сдал последний экзамен, — различие
между двумя средними стало бы еще яснее. Например, если студент получил 100
на последнем экзамене, то взвешенное среднее дало бы 87.4, в то время как
арифметическое среднее — 83.5. И наоборот, большое различие в оценках за
контрольную привело бы к незначительной вариации взвешенного среднего,
но могло бы сильно повлиять на среднее арифметическое.
Среднее геометрическое
Вычисление среднего геометрического очень часто проводятся в тех
случаях, когда данные не очень симметричны, особенно в биологических
исследованиях. Чтобы уяснить себе смысл среднего геометрического, рассмотрите
ситуацию, когда у вас есть 48 долларов, и в течение пяти дней вы тратите
каждый день половину из наличных денег. Данные относительно вашей
наличности будут в этом случае соответствовать таблице 11.3.
Таблица 11.3. Ваши наличные деньги за 5-дневный период
День
1
2
3
4
5
Сумма наличных денег
$48
$24
$12
$6
$3
Среднее арифметическое в данном случае
(48 + 24 + 12 + 6 + 3) / 5
равняется 18.6; это мало о чем говорит. Если вы теперь нанесете эти значения
на обычный график, то заметите, что соединяющая точки линия не прямая, а
следует гиперболе. Этот график показан на рис. 11.2.
С другой стороны, если нанести те же самые данные на
полулогарифмическую бумагу, то окажется, что точки легли на одну прямую. Такой вид
графика говорит, что для данных имеет смысл рассчитать среднее геометрическое.
Статистические расчеты . 425
Рис. 11.2.
График ваших
наличных денег за
5-дневный период
12 3 4 5
Дни
Для вычисления среднего геометрического нужно найти логарифм каждого
значения и определить логарифмическое среднее, т. е. среднее арифметическое
логарифмов:
(log 48 + log 24 + log 12 + log 6 + log 3) / 5
Это значение равняется 1.079. Его антилогарифм (или log"1) равен 11.99;
это и есть среднее геометрическое.
Вычисление среднего квадратичного
и квадратичной суммы
Два других средних встречаются в науке и иногда в технике (особенно в
электронике): среднее квадратичное (обычное техническое сокращение rms
означает root mean square) и квадратичная сумма (rss, root sum square).
Значение rms широко используется при расчетах электрических цепей и
некоторых других приложениях. Например, переменный электрический ток
представляет собой синусоидальную волну. «Среднее» значение синуса в
действительности всегда равно нулю, так как положительные и отрицательные
значения равны по величине и взаимно уничтожаются. В таком случае среднее
квадратичное оказывается более осмысленным (и, следовательно, более
важным). В случае переменного тока среднеквадратичное напряжение будет равно
0.707 Vp, где Vp соответствует пиковому напряжению. Для расчета rms в
данном случае требуются специальные вычисления — в частности, интегралы, —
которые выходят за рамки тематики данной книги, и мы не будем их здесь
рассматривать.
х
га
о.
CD
с;
а
со
СО
s
s
>i
о
50
40
30
20
10
426
Глава 11
Однако мы рассмотрим вычисление квадратичных сумм. Квадратичные
суммы применяются там, где нужно комбинировать не связанные между
собою данные. Например, шумовые сигналы в электронике являются ошибками
и могут происходить из различных источников. Предположим, что имеется п
независимых источников шумового потенциала (vni, vn2,...vnn). Когда они
действительно независимы, их нельзя складывать линейным образом. Вместо
этого их комбинируют с помощью метода rss, возводя напряжение каждого
источника в квадрат (vn2) и извлекая квадратный корень из суммы всех этих
квадратов. Смысл этого метода станет яснее, когда мы рассмотрим его в
контексте программы moreavg.cpp, детально разбираемой в следующих разделах.
Код
Ниже следует код программы moreavg.cpp, которая вычисляет взвешенное
среднее, среднее геометрическое и квадратичную сумму.
*'■ . // Средние (продолжение) .
j. '.#include <iostream>
". ;t#include <cmath>
•■ *#define numweightvals 4
'using namespace std;
|
' .double arith_mean(int valarr[], int numels)
*'+. double average = 0;
i for (int i=0; i < numels; i++)
.■* " average = average + valarr[i] ;
" ■* return average / numels;
„''int weight_mean(double weight[] , int scores[][4], int weights, int
-■rows)
;** int j = 0;
double totalscores = 0, totalweights = 0;
** . for (int i =0; i < rows; i++) {
" -.1 cout « "Weighted grade for Student #: " « (i + 1) « " is: "
"■ ' for (j = 0; j < weights; j++) {
■■ * totalscores = totalscores + (weight[j] * scoresfi][j]);
H. "y; totalweights = totalweights + weight [j];
; \ )
\ *!■ cout « (totalscores / totalweights) « endl;
.%. )
! i*! return 0;
I')
"■"'f double geo_mean(int values[], int numvals)
;'■ \ i
£+*:. double logmean = 0;
Статистические расчеты
427
1 for (int i = 0; i < numvals; i++)
\ logmean = logmean + log(values[i]);
logmean = logmean / numvals;
return exp(logmean);
i .}
'double rss(double vals[], int numvals)
({
• •, double sums = 0;
for (int i= 0; i < numvals; i++)
sums = sums + pow(vals[i], 2);
* return pow(sums, .5);
)
*
4int main()
• .<
■ ■ double weightings[numweightvals] = {0.25, 0.20, 0.15, 0.40);
i int weightvals[4][numweightvals] = {{84, 78, 72, 79),
] {84, 78, 72, 100},
I {84, 78, 45, 79),
i. J {84, 85, 72, 95}};
i
I double average = 0;
• i
„ • cout « "Computing Weighted Means: " « endl;
weight_mean(weightings, weightvals, numweightvals, 4);
• cout « endl;
U 1
int spending[5] = {48, 24, 12, 6, 3);
cout « "Computing Geometric Mean: " « endl;
cout « "The arithmetic mean of the values: ";
i \ cout « arith_mean(spending, 5) « endl;
cout « "The geometric mean of the values: ";
cout « geo_mean(spending, 5) « endl;
i ' double noise[10] = {0.705, 0.387, 0.215, 0.476, 0.121,
0.325, 0.888, 0.287, 0.414, 0.542};
s
\ I cout « "Computing the root sum square:" « endl;
ш' i cout « "Noise values: " « endl;
for (int i = 0; i < 10; i ++)
1 , cout « noise[i] « " ";
I ' cout « endl « "The root sum square is: " « rss(noise, 10) «
*,endl ;
И return 0;
428
Глава 11
I ПРИМЕЧАНИЯ
Давайте подробно разберем показанный выше код, который почти в
точности повторяет то, что мы уже делали ранее.
double arith_mean(int valarr[], int numels)
{
double average = 0;
for (int i=0; i < numels; i++)
average = average + valarr[i];
return average / numels;
}
Функция arith_mean() точно такая же, как в программе avg.cpp. Она
используется здесь только для того, чтобы показать разницу между средним
геометрическим и средним арифметическим. Первой новой функцией в этой
программе является weight_mean, которая принимает четыре параметра: массив
весовых значений (процентов), двумерный массив оценок, целое,
соответствующее количеству оценок для одного студента, и целое, соответствующее
числу студентов. Код функции объявляет локальные переменные — счетчик и
две переменные для суммирования.
int weightjmean(double weight[], int scores[][4], int weights, int
rows)
{
int j = 0;
double totalscores = 0, totalweights = 0;
Затем программа входит во внешний цикл, перебирающий все наборы
оценок и генерирующий для каждого студента вывод результата на экран.
Программа выводит информационный заголовок и входит во внутренний цикл.
Это цикл выполняет сложение взвешенных оценок (weight * score) в
переменной totalscores. Когда происходит выход из внутреннего цикла, программа
выводит на экран взвешенную сумму, деленную на полный вес. Вот код,
выполняющий эти вычисления:
for (int i =0; i < rows; i++) {
cout « "Weighted grade for Student #: " « (i + 1) « " is: ";
for (j = 0; j < weights; j++) {
totalscores = totalscores + (weight[j] * scores[i][j]);
totalweights = totalweights + weight[j];
}
cout « (totalscores / totalweights) « endl;
}
return 0;
}
Вычисление среднего геометрического в функции geo_mean() достаточно
очевидно. Как говорилось в разделе о среднем геометрическом, сначала нужно
рассчитать среднее логарифмическое, а затем взять антилогарифм
полученного значения. Функция принимает в качестве параметров массив значений и и
число элементов в массиве. Для суммирования логарифмов значений
организуется цикл for, как здесь показано:
Статистические расчеты
429
double geo_mean(int values[]f int numvals)
{
double logmean = 0;
for (int i = 0; i < numvals; i++>
logmean = logmean + log(values[i]);
Обратите внимание, что для вычисления логарифмов программа вызывает
функцию математической библиотеки С log(). Затем программа делит сумму
логарифмов на общее число элементов, получая логарифмическое среднее.
Наконец, вызывается функция математической библиотеки ехр(),
возвращающая антилогарифм среднего логарифмического:
logmean = logmean / numvals;
return exp(logmean);
}
Последняя функция программы вычисляет квадратичную сумму. Как вы
видели, квадратичная сумма (rss) несложна: нужно возвести в квадрат каждое
значение ряда, сложить их и извлечь и полученной суммы квадратный
корень. Это и делает функция rss(), используя функцию математической
библиотеки pow(). Функция принимает, как обычно, два параметра — массив
значений и число элементов массива. Она объявляет локальную переменную для
суммирования:
double rss(double vals[], int numvals)
{
double sums = 0;
Цикл for проходит по массиву, возводя каждый элемент в квадрат и
накапливая сумму в переменной sums. Когда цикл заканчивается, функция
возвращает корень из значения этой переменной, для чего снова вызывается pow() со
степенью 0.5:
for (int i= 0; i < numvals; i++)
sums = sums + pow(vals[i], 2);
return pow(sums, .5);
}
Функция main(), как и в программе avg.cpp, является просто оболочкой
для объявления массивов и вызова различных функций, выполняющих их
обработку. В данной программе объявляется, в отличие от avg.cpp, объявляется
несколько массивов, потому что мы не можем использовать для всех примеров
одни и те же данные. Функция main() начинается с объявления массива, в
который записаны весовые коэффициенты оценок для примера взвешенного
среднего, и объявления двумерного массива, содержащего оценки четырех
студентов.
int main()
i
double weightings[numweightvals] = {0.25, 0.20, 0.15, 0.40};
int weightvals[4][numweightvals] = {{84, 78, 72, 79},
{84, 78, 72, 100},
{84, 78, 45, 79),
{84, 85, 72, 95}};
430
Глава 11
Затем, после вывода заголовка, вызывается функция вычисления
взвешенных средних, как показано ниже:
cout « "Computing Weighted Means: " « endl;
weight_mean(weightings, weightvals, numweightvals, 4) ;
cout « endl;
После этого мы переходим к вычислению среднего геометрического. Как
отмечалось выше, программа для сравнения выводит среднее арифметическое
для тех же значений:
int spending[5] = {48, 24, 12, 6, 3};
cout « "Computing Geometric Mean: " « endl;
cout « "The arithmetic mean of the values: ";
cout « arith__mean(spending, 5) « endl;
cout « "The geometric mean of the values: ";
cout « geo_mean(spending, 5} « endl;
Наконец, код функции main() объявляет десятиэлементный массив
шумовых значений, каждое из которых меньше 1. Затем эти значения выводятся на
экран и вычисляется их квадратичная сумма, которая также выводится:
double noise[10] = {0.705, 0.387, 0.215, 0.476, 0.121,
0.325, 0.888, 0.287, 0.414, 0.542};
cout « "Computing the root sum square:" « endl;
cout « "Noise values: " « endl;
for (int i = 0; i < 10; i ++)
cout « noise[i] « " ";
cout « endl « "The root sum square is: "
« rss(noise, 10) « endl;
return 0;
)
Когда вы компилируете и запустите программу moreavg.cpp, она выведет
все рассчитанные значения средних. Вот вывод программы:
Computing Weighted Means:
Weighted grade for Student #: 1 is: 79
Weighted grade for Student #: 2 is: 83.2
Weighted grade for Student #: 3 is: 80.45
Weighted grade for Student #: 4 is: 82.0375
Computing Geometric Mean:
The arithmetic mean of the values: 18.6
The geometric mean of the values: 12
Computing the root sum square:
Noise values:
0.705 0.387 0.215 0.476 0.121 0.325 0.888 0.287 0.414 0.542
The root sum square is: 1.54141
Оценка вероятностей
Теперь, когда мы посмотрели на применение ср дних при исследовании
данных, давайте обратимся к другой фундаментальной области статистики —
вероятностям. Вероятность оперирует не абсолютными, а возможными сущ-
Статистические расчеты 431
ностями. Она позволяет нам предвидеть, как часто следует ожидать
определенных результатов опыта в ситуациях, когда возможно несколько различных
исходов — например, в классических бросаниях монетки или костей. Теория
вероятности является неотъемлемым инструментом статистического анализа.
В задачах на вероятность для ее обозначения пользуются буквой Р с
последующим аргументом в квадратных или круглых скобках, показывающим,
какое событие имеется в виду. Например, P(x) означает вероятность события х.
При бросании монеты можно написать P(heads) для обозначения вероятности
выпадения «орла» или P(tails) для «решки*.
Значение Р всегда является дробью (иногда записываемой в десятичной
форме) в диапазоне между 0 и 1.
♦ Р(х) = 0 означает, что событие х никогда произойти не может.
♦ Р(х) = 1 означает, что событие х не может не произойти.
♦ Значения Р(лг) между 0 и 1 являются указанием на относительную
возможность события х. Например, если вероятность х равна 0.5, то следует
ожидать, что в половине случаев событие х произойдет, а в половине
случаев оно не произойдет. Данная вероятность не предсказывает
абсолютного исхода конкретного события в будущем, а только то, насколько
возможен тот или иной исход.
Предположим, мы, к примеру, знаем, что 4.5% программистов во всем
мире пользуются клавиатурой DVORAK. Другими словами, этот тип
клавиатуры используют 4.5 программиста из каждой сотни, что в долевом
выражении равно 0.045. Какова вероятность, что некоторый программист из всех
случайно встреченных нами программистов работает с клавиатурой DVORAK?
Вероятность равна 0.045, т. е. примерно 1 из 22. Статистически один из
каждых 22 программистов пользуется клавиатурой DVORAK.
Такая оценка вероятности будет верной только в том случае, если
выполнены два условия: (а) все программисты имеют равные шансы встретиться с
вами (т. е. программисты с клавиатурой DVORAK не находятся все на другом
краю земли) и (Ь) все возможные программисты входят в число людей,
которых вы можете встретить.
Различные аспекты теории вероятности охватывают два широких класса
событий — простые и составные события. Простые события не могут быть
сведены к комбинации других событий. Например, бросание монеты может дать в
результате только «орел» или «решку», бросание кости может дать только 1,
2, 3, 4, 5 или 6. Вы не можете взять грань кости с тремя очками и рассечь ее на
какие-то меньшие элементы «троичности» — тройка либо выпадает, либо нет.
Если простое событие может иметь более одного исхода, и все исходы равно
возможны, то вероятность любого исхода в одиночном независимом
испытании равно доле этого исхода среди всех возможных исходов испытания. Если
имеется N различных исходов события А, и п из них считаются
«успешными», то вероятность события А — т. е. Р(А) — равна n/N. Вероятность А
является дробью между 0 и 1. Таким образом, при бросании монеты имеются две
возможности («орел» и «решка»), и вероятность угадать результат одиночного
броска соответствует одной стороне из двух возможных, т. е. одной второй. Это
правило иногда называют первым, законом вероятности.
Составные события могут быть разложены на несколько возможных
событий. Например, вероятность выпадения на кости четного (2, 4, 6) или нечетно-
432
Глава 11
го (1, 3, 5) числа является составным событием. Вероятность события равна
сумме вероятностей отдельных событий. Например, вероятность выбросить
четное число может быть составлена следующим образом:
Р(четное) = Р(2) + Р(4) + Р(6)
что равняется 1/6 + 1/6 + 1/6, т. е. половине.
Второй закон вероятности
Если событие может иметь более одного исхода и все возможные исходы
равно возможны, то результаты практически всегда несколько отличаются от
идеальной вероятности. Но для большого числа испытаний вариация будет
меньше, если события действительно равновероятны. Другими словами, чем
больше испытаний вы производите, тем ближе результаты испытаний будут к
действительной вероятности.
Обычным заблуждением является предположение о том, что закон больших
чисел (второй закон вероятности) требует, чтобы пропорция для каждого
исхода была точной. Например, при бросании монеты вероятность того, что Р(орел)
и Р(решка) будут после 10000 испытаний равняться в точности 0.5000,
чрезвычайно низка; напротив, вероятность того, что они будут близки к 0.5,
чрезвычайно высока.
Третий закон вероятности
Если что-то может иметь более одного исхода и все возможные исходы
равновероятны, то вероятность одного из взаимоисключающих исходов
одиночного испытания будет суммой их индивидуальных вероятностей. Это называется
законом сложения. Мы уже видели этот закон в действии, когда
рассматривали составные события.
Как вы тогда видели, если вероятность любого из событий равна 1 из 6 и мы
рассматриваем три возможных события, то вероятность одного из трех
событий равна 3 из 6, или половине.
Четвертый закон вероятности
Осталось рассмотреть еще один закон, и затем мы перейдем к
программному примеру, если нечто может иметь несколько возможных исходов и все они
равновероятны, то вероятность любой отдельной комбинации исходов двух
или более испытаний равна произведению их отдельных вероятностей. Этот
закон известен как закон умножения.
Если события действительно независимы и не указан порядок, в котором
они должны произойти, то вероятность одного из событий в одиночном
испытании равна сумме их вероятностей. Эта ситуация управляется законом
сложения. Но чему равна вероятность конкретной комбинации событий в
нескольких испытаниях? Другими словами, чему равна вероятность, бросая
кость, выбросить подряд 4 и 5? Чтобы вычислить эту вероятность, нужно
обратиться к четвертому закону:
Р(А и В) = Р(А) * Р(В)
Статистические расчеты
433
В приведенном примере вероятность равна Р(4) * Р(5), т. е. 1/6 * 1/6, что
дает 1/36. Но это еще не все. Четверку можно выбросить как на первом, так и
на втором броске — и наоборот для 5. Вероятность каждого из этих исходов
равна 1/36 — тем самым вероятность одного из них равна 1/36 + 1/36 = 1/18.
Наше обсуждение далеко не исчерпывает предмет вероятности, но это
введение дает хорошую основу для понимания некоторых сопутствующих
вопросов. Программа prob.cpp, используя генератор случайных чисел, проводит ряд
тестов, показывающих приближение вероятностей к своему пределу.
Код
Ниже следует код программы prob.cpp.
!// Вероятности.
#include <iostream>
#include <cmath>
|#include <stdlib.h>
using namespace std;
int gen_rand(int numposs)
return ((rand() % numposs) + 1);
int out_flips(int heads, int tails, double flips)
cout « endl « "Total Flips: " « flips « endl;
cout « "Total Heads: " « heads « endl;
cout « "Total Tails: " « tails « endl;
cout « "Prob Head: " « heads / flips « endl;
cout « "Prob Tails: " « tails / flips « endl;
return 0;
}
[int out_rolls(int val[], double rolls)
{
cout « endl « "Total Rolls:
cout « "Total Is:
cout « "Prob Is:
cout « "Total 2s:
cout « "Prob 2s:
cout « "Total 3s
cout « "Prob 3s:
cout « "Total 4s
cout « "Prob 4s:
cout « "Total 5s
cout « "Prob 5s:
cout « "Total 6s
cout « "Prob 6s:
return 0;
' « val[0]
« val[0]
• « val[l]
« val[l]
* « val[2]
« val[2]
■ « val[3]
« val[3]
' « val[4]
« val[4]
' « val[5]
« val[5]
" « rolls « endl;
« "; ";
/ rolls « endl;
« "; ";
/ rolls « endl;
« "; '*;
/ rolls « endl;
« "; ";
/ rolls « endl;
« "; ";
/ rolls « endl;
« "; »;
/ rolls « endl;
434
Глава 11
int main()
{
int headinstances = 0, tailinstances =0;
double totalflips =0;
cout « "Coin Flip Probabilities:" « endl;
for (int i = 0; i < 50; i++) (
if (gen_rand(2) - 1)
headinstances-H-;
else
tailinstances-H-;
totalflips++;
}
out_flips(headinstances, tailinstances, totalflips);
cout « endl « "Coin Flip Probabilities (Trial 2):" « endl;
headinstances = 0;
tailinstances - 0;
totalflips =0;
for (i = 0; i < 500; i++) {
if (gen_rand(2) - 1)
{ headinstances++;
,\ else
tailinstances++;
totalflips++;
}
out_flips(headinstances, tailinstances, totalflips);
cout « endl « "Coin Flip Probabilities (Trial 3):" « endl;
headinstances - 0;
tailinstances = 0;
totalflips = 0;
for (i = 0; i < 5000; i++) {
if (gen_rand(2) - 1)
headins tances++;
i else
tailinstances++;
totalflips++;
out_flips(headinstances, tailinstances, totalflips);
int val[6] = {0, 0, 0, 0, 0, 0);
^ m double totalrolls = 0;
J cout « endl « "Die Roll Trials (one-sixth - 0.167)" « endl;
1 for (i = 0; i < 50; i++) {
val[(gen_rand(6) - 1)]++;
totalrolls++;
}
out_rolls(val, totalrolls);
cout « endl « "Die Roll Trial 2 (one-sixth = 0.167)" « endl;
for(i=0; i < 6; i++)
val[i] =0;
Статистические расчеты 435
totalrolls - 0;
for (i = 0; i < 500; i++) {
val[(gen_rand(6) - 1)]++;
totalrolls++;
}
out_jcolls (val, totalrolls) ;
cout « endl « "Die Roll Trial 3 (one-sixth = 0.167)" « endl;
for(i=0; i < 6; i++)
val[i] = 0;
totalrolls = 0;
for (i = 0; i < 5000; i++) {
val[(gen_rand(6) - 1)]++;
totalrolls++;
}
out_rolls(val, totalrolls);
return 0;
)
Ls
ПРИМЕЧАНИЯ
ямиг
Давайте разберем код программы prob.cpp. Первой ее функцией является
gen_rand(), которая просто вызывает функцию rand() и возвращает значение в
указанном пользователем диапазоне. (В библиотеке имеется также функция
random(), позволяющая задать диапазон в качестве аргумента.) функция
get_rand() использует арифметику по модулю, чтобы привести случайное
число к диапазону возможных значений, и затем прибавляет 1, чтобы ряд
значений начинался с 1 (арифметика по модулю всегда дает ряд значений от нуля).
Функция выглядит так:
int gen_rand(int numposs)
{
return ((rand() % numposs) + 1);
}
Заметьте, что rand() в действительности генерирует число в диапазоне от О
до RAND_MAX, значения, определяемого в заголовке cstdlib стандартной
библиотеки С. Операция взятия про модулю необходима, чтобы получить
результат в диапазоне, который требуется программе.
Следующие две функции, out_flips() и out_rolls(), генерируют вывод для
двух различных задач, которые мы здесь рассматриваем. Первая функция
генерирует выходную информацию о моделировании бросания монеты:
int out_flips(int heads, int tails, double flips)
{
cout « endl « "Total Flips: " « flips « endl;
cout « "Total Heads: " « heads « endl;
cout « "Total Tails: " « tails « endl;
cout « "Prob Head: " « heads / flips « endl;
cout « "Prob Tails: " « tails / flips « endl;
return 0;
}
436 ; Глава 11
Функция выводит общее число бросков монеты, число выпавших «орлов» и
«решек», и вероятность каждого из исходов, рассчитанную по результатам
испытаний. Заметьте, что параметр flips объявлен как double, чтобы
компилятор не генерировал операцию деления нацело, отбрасывающую дробную часть
результата. Функция out_roIls() выполняет сходные действия, хотя здесь
выводится большее число строк из-за большего числа возможных исходов
бросания кости:
int out_rolls(int val[], double rolls)
i
cout « endl « "Total Rolls: " « rolls « endl;
cout « "Total Is:
cout « "Prob Is:
cout « "Total 2s:
cout « "Prob 2s:
cout « "Total 3s
cout « "Prob 3s:
cout « "Total 4s
cout « "Prob 4s:
cout « "Total 5s
cout « "Prob 5s:
cout « "Total 6s
cout « "Prob 6s:
return 0;
}
« val[0] « "; ";
« val[0] / rolls « endl;
« val[l] « "; ";
« val[l] / rolls « endl;
« val[2] « "; ";
« val[2] / rolls « endl;
« val[3] « ";
« val[3] / rolls « endl;
« val[4] « "; ";
« val[4] / rolls « endl;
« val[5] « "; ";
« val[5] / rolls « endl;
Как видите, out_rolls() принимает в качестве параметров массив с
результатами и общее число бросков. Здесь также rolls объявляется как double,
чтобы программа не производила целых делений. Массив содержит общие
результаты для каждого из выброшенных чисел. Программа выводит вероятность в
одной строке с числом бросков, чтобы сэкономить место при выводе.
Функция ma in () просто получает случайные числа и затем выводит
результаты испытаний и значения вероятностей. Она начинается с объявления
переменных для бросания кости, показанных ниже:
int main()
{
int headinstances = 0, tailinstances = 0;
double totalflips - 0;
cout « "Coin Flip Probabilities:" « endl;
for (int i = 0; i < 50; i++) {
if (gen_rand(2) - 1)
headinstances++;
else
tailinstances++;
totalflips++;
}
out_flips(headinstances, tailinstances, totalflips);
Здесь проводится короткая серия испытаний всего из 50 бросков. Цикл
увеличивает на единицу соответствующие результаты в зависимости от
результата броска, определяемого оператором if по значению, полученному от функции
get_rand(). По завершении серии программа вызывает out_flips() для вывода
Статистические расчеты
437
информации о результатах испытания. Та же структура повторяется еще два
раза, отличаясь только числом итераций цикла. Во второй серии производится
500 бросков и в третьей — 5000.
После испытаний с бросанием монеты программа переходит к бросаниям
кости. Для хранения числа каждого из возможных выпадающих значений
объявляется массив. Переменная tot air oils отслеживает общее число бросков.
int val[6] = {0, 0, 0, 0, 0, 0};
double totalrolls =0;
cout « endl « "Die Roll Trials (one-sixth = 0.167)" « endl;
for (i = 0; i < 50; i++) {
val[(gen_rand(6) - 1)]++;
totalrolls++;
}
out_rolls(val, totalrolls);
Код очень похож на код для бросания монеты. Заголовок вверху сообщает
значение 1/6 в десятичном представлении — просто для ясности, поскольку в
этой главе мы все время говорили об этой одной шестой. Затем программа
входит в цикл for и моделирует серию бросков. Однако в цикле вместо того, чтобы
использовать операторы if, как при бросании монеты, или структуру switch,
код просто увеличивает на единицу элемент массива, соответствующий
полученному от get_rand() значению. Когда цикл заканчивается, вызываается
функция out_rolls() для вывода результатов испытаний.
cout « endl « "Die Roll Trial 2 (one-sixth = 0.167)" « endl;
for(i=0; i < 6; i++)
val[i] = 0;
totalrolls = 0;
for (i = 0; i < 500; i++) {
val[(gen_rand(б) - 1)]++;
totalrolls++;
}
out_rolls(val, totalrolls);
cout « endl « "Die Roll Trial 3 (one-sixth ■ 0.167)" « endl;
for(i=0; i < 6; i++)
val[i] = 0;
totalrolls = 0;
for (i = 0; i < 5000; i++) (
val[(gen_rand(6) - 1)]++;
totalrolls++;
}
out_rolls(val, totalrolls);
return 0;
}
И снова второе и третье испытания повторяют те же самые действия, что и в
первое, но с большим числом итераций — 500 бросков во второй серии и 5000 в
третьей. Как вы увидите из результатов программы, цель увеличения числа
итераций состоит в том, чтобы показать приближение действительных
результатов к идеальным вероятностям событий.
438
Глава 11'
Вывод программы ргоЬ.срр проясняет сказанное. Он будет напоминать
показанный ниже, отличаясь, возможно, на вашем компьютере из-за различий в
генераторах случайных чисел:
Coin Flip Probabilities:
Total Flips: 50
Total Heads: 26
Total Tails: 24
Prob Head: 0.52
Prob Tails: 0.48
Coin Flip Probabilities (Trial 2):
Total Flips: 500
Total Heads: 255
Total Tails: 245
Prob Head: 0.51
Prob Tails: 0.49
Coin Flip Probabilities (Trial 3) :
Total Flips: 5000
Total Heads: 2486
Total Tails: 2514
Prob Head: 0.4972
Prob Tails: 0.5028
Die Roll Trials (one-sixth - 0.167)
Total
Total
Total
Total
Total
Total
Total
Rolls:
Is: 12
2s: 8;
3s: 7;
4s: 9;
5s: 9;
6s: 5;
50
Prob Is
Prob 2s:
Prob 3s:
Prob 4s:
Prob 5s:
Prob 6s:
: 0.24
0.16
0.14
0.18
0.18
0.1
Die Roll Trial 2 (one-sixth = 0.167)
Total
Total
Total
Total
Total
Total
Total
Rolls:
Is: 77
2s: 81
3s: 80
4s: 75
5s: 95
6s: 92
500
Prob
Prob
Prob
Prob
Prob
Prob
Is:
2s:
3s:
4s:
5s:
6s:
0
0
0
0
0
0
154
162
16
15
19
184
Die Roll Trial 3 (one-sixth « 0.167)
Total Rolls: 5000
Total
Total
Total
Is:
2s:
3s:
811;
886;
840;
Prob
Prob
Prob
Is:
2s:
3s:
0.1622
0.1772
0.168
Статистические расчеты
439
Total 4s: 824
Total 5s: 793
Total 6s: 846
Prob 4s: 0.1648
Prob 5s: 0.1586
Prob 6s: 0.1692
Из испытаний с бросками монеты вы можете видеть, как с увеличением
числа бросков вероятность каждого из событий быстро приближается к 50%.
Для серии из 5000 бросков различие в вероятности между двумя возможными
событиями составляет примерно шесть тысячных. Хотя в случае бросания
кости эта тенденция выражена не столь явно — из-за большего числа возможных
событий, — она все же очевидна. Мы ожидаем, что с ростом числа бросков
каждая из вероятностей в конце концов приблизится к 0.167.
Регрессионный анализ
Давайте исследуем последний тип статистического анализ: процедуру,
известную под названием регрессионного анализа. Регрессионный анализ
представляет собой метод получения общих утверждений относительно ряда или
набора конкретных единиц данных, — другими словами, это средство
«регрессии» от конкретной информации к обобщенным данным.
В статистических исследованиях часто данные сначала записываются, а
затем наносятся на диаграмму распределения, подобную показанной на рис. 11.3.
Горизонтальная ось, или ось х, используется для независимой переменной.
Вертикальная, или ось у, представляет зависимую переменную. Это полезный
метод поиска ответов на самые разные вопросы. Часто эти вопросы касаются
отношений между величинами, например, «Зависит ли вес от роста?» и т. п.
Чтобы лучше понять сказанное, давайте рассмотрим пример.
Предположим, в сосуд с водой помещен электронагревательный элемент, и вы
предполагаете, что температура воды является функцией электрического тока,
протекающего через нагревательный элемент. Вы могли бы отложить по
горизонтальной оси силу тока (I), а по вертикальной — температуру (Т), назвав эту ось
Т или Т(1).
Рис. 11.3.
Диаграмма распределения
показывает точки данных (х, f{x))
m
• * • •
• • •-•
• • •
► • • •
■••.*•
• ♦ •
I I I
J—I ►
440
Глава 11
Когда все данные нанесены на график, может выявиться закономерность,
предполагающая связь этих величин. Другими словами, выявляется
корреляция между х и f(x). На рис. 11.3, например, видно, что f(x) возрастает с
ростом значения х. Этот факт подразумевает корреляцию между х и f(x). Так как
f(x) растет с ростом я, мы говорим, что между ними существует
положительная корреляция.
Рис. 11.4 показывает различные типы корреляции, которые могут
встретиться в наборах данных. На рис. 11.4а обнаруживается сильная
положительная корреляция, в то время как на рис 11.4Ь видна не менее сильная
отрицательная корреляция (другими словами, с ростом х f(x) уменьшается). В
данных на рис 11.4с наблюдается слабая до умеренной положительная
корреляция, показывающая, что либо связь этих величин не очень сильна, либо,
возможно, присутствует ошибка измерения, либо существует некий неучтенный
фактор, влияющий на данные. Когда корреляции между f(x) и х нет, то нет
или почти нет никакой закономерности в расположении точек данных, что
демонстрирует рис. 11.4d.
Когда на диаграмме распределения мы видим сильную корреляцию между х и
f(x)> возникает искушение предположить, что между этими величинами
существует причинная связь. Иногда такое предположение верно, иногда нет. Когда оно
оказывается верным, мы можем построить математическую модель отношения
между х и f(x), предсказывающую значения f(x) для значений #, отличных от
реально измерявшихся. Сила модели раскрывается в достоверности таких
предсказаний для данных вне диапазона измерений. Имеется причинная связь или
нет, это на самом деле не так важно, если знание х позволяет предсказать f(x).
Другими словами, является ли х «причиной» f(x), для математической модели
не важно или менее важно, чем наличие соответствия между х и f(x).
Для построения такой модели в вашем распоряжении имеются различные
методы, но наиболее распространенным является вычисление прямой линии,
наиболее точно суммирующей отношение между двумя переменными. Эта
линия известна как регрессионная прямая или линия наименьших квадратов.
При построении такой прямой делается предположение, что зависимость
линейна, и что в ^-компоненту каждой точки данных может входить некоторая
малая ошибка а (малая греческая сигма). Другими словами, функциональная
зависимость может быть записана следующим образом:
оценка(у) = а + Ьх ± <т
В словесном выражении это означает, что значение у в любой точке может
быть описано, в пределах небольшой зоны ошибки, прямой с данными
коэффициентами. Целью подбора по наименьшим квадратам является
минимизация расстояний между каждой из точек набора данных и выбранной прямой.
Формула для нахождения Ь в вышеприведенном уравнении довольно сложна и
может быть описана как сумма х * у минус сумма х, умноженная на сумму у.
Затем результат этого вычисления делится на число точек, умноженное на
сумму квадратов х, минус квадрат суммы х.
Как все это работает, показывает программа reganal.cpp. Она предлагает
пользователю ввести набор значений х и у и затем, применяя метод
наименьших квадратов, пытается по полученным данным вывести уравнение искомой
прямой. В следующем разделе представлен код и примечания к программе
reganal.cpp.
Статистические расчеты
441
W
ФО
.•••:••
• •
•••:•.
• "•
• • •.
(а)
<Ь)
Дх)
«*)
• ~ ■
• • •
(с)
(<*)
Рис. 11.4. Четыре диаграммы распределения, показывающие различные типы корреляции
Код
Ниже приводится код программы reganal.cpp.
// Регрессионный анализ.
#include <iostream>
linclude <cmath>
#include <cstdlib>
using namespace std;
[double arith mean (double valarr[] , int numels)
I
double average = 0;
for (int i=0; i < numels; i++)
average = average + valarr[i];
442
Глава 11
return average / numels;
i)
г'int main()
{
int numvals ~ 0;
i double xmean, ymean;
double xmultiplier, constant, sumxbysumy = 0;
i double xbyy = 0, sumx = 0, sumy ~ 0, sumxsq = 0;
•1 double x[30], у[30];
7*double x[] = { 6.6, 9.1, 17.4, 17.9, 12.9, 13.3,
] 13.6, 18.1, 29.1, 25.6, 36.5, 35.0,
: 30.0, 30.7, 39.3, 47.5, 40.9, 48.5 };
' double y[] = {20.1, 45.0, 67.4, 73.4, 95.9, 98.9,
103.1, 90.6, 92.7, 114.7, 119.0, 109.8,
121.8, 117.3, 145.8, 171.6, 174.8, 206.5 };
. */
cout « "Enter the number of values (integer, less than 30) :
! cin » numvals;
if (numvals — 0)
return 0;
for (int i = 0; i < numvals; i++) {
cout « "Enter the " « i « "th X value: ";
cin » x[i];
r
cout « "Enter the " « i « "th У value:
cin » y[i];
}
xmean = arith__mean(x, numvals);
ymean = arith_mean(y, numvals);
for (i = 0; i < numvals; i ++) (
xbyy = xbyy + (x[i] * y[i]);
sumx = sumx + x[i];
sumy = sumy + y[i];
sumxsq = sumxsq + (x[i] * x[i]);
}
xmultiplier = ((double) numvals * xbyy) - (sumx * sumy);
xmultiplier = xmultiplier / (((double) numvals * sumxsq)
- (sumx * sumx));
constant = ymean - (xmultiplier * xmean);
cout « "The mean of the x values: " « xmean « endl;
cout « "The mean of the у values: " « ymean « endl;
1' H
[i i cout « "The line's definition: у = " « constant «
Jv i « xmultiplier « "x" « endl;
Лл return 0;
?2i
I™
ПРИМЕЧАНИЯ
Код программы reganal.cpp довольно прямолинеен и реализует вычисления
по методу наименьших квадратов, описанные выше. Здесь используется функ-
Статистические расчеты 443
ция arith_mean() из программы avg.cpp, но в остальном все вычисления
производятся прямо в функции main(). Поскольку вы уже видели arith_mean()
раньше (единственное отличие состоит в том, что теперь функция принимает массив
double, а не int), мы сразу приступим к обсуждению main().
Функция начинается с объявления разных переменных, используемых при
обработке данных. В их число входят переменные для хранения числа
элементов в выборке, для средних арифметических обоих наборов значений и для
коэффициентов прямой.
int main{)
<
int numvals =0;
double xmean, ymean;
double xmultiplier, constant, sumxbysumy = 0;
double xbyy = 0, sumx = 0, sumy = 0, sunucsq = 0 ;
double x[30], y[30];
В показанном виде программа позволяет пользователю ввести в набор до 30
значений для х и у> исходя из которых программа будет строить прямую
линию. Если вы хотите избежать ввода значений, то можете воспользоваться
предусмотренным в программе кодом предопределенных массивов,
содержащих по 18 значений каждый; мы пользовались ими при создании и
тестировании программы. Вывод, который вы увидите далее, генерирован именно для
этих 18-ти точек. Вот эти предопределенные массивы:
/*double x[] = { 6.6, 9.1, 17.4, 17.9, 12.9, 13.3,
13.6, 18.1, 29.1, 25.6, 36.5, 35.0,
30.0, 30.7, 39.3, 47.5, 40.9, 48.5 };
double y[] = {20.1, 45.0, 67.4, 73.4, 95.9, 98.9,
103.1, 90.6, 92.7, 114.7, 119.0, 109.8,
121.8, 117.3, 145.8, 171.6, 174.8, 206.5 };
*/
Затем код программы просит пользователя ввести число элементов в
массивах и сохраняет полученное значение в переменной numvals. Если
пользователь вводит 0 или нечто такое, что оператор cin не сможет распознать,
программа завершается.
cout « "Enter the number of values (integer, less than 30): ";
cin » numvals;
if (numvals == 0)
return 0;
После этого программа входит в цикл for, запрашивающий у пользователя
значения х и у, помещая каждую точку в соответствующую ячейку массива.
for (int i = 0; i < numvals; i++) {
cout « "Enter the " « i « "th X value: ";
cin » x[i];
cout « "Enter the " « i « "th Y value: ";
cin » y[i];
}
Закончив ввод данных, программа приступает к математической их
обработке, требуемой для вычисления искомой прямой. Прежде всего
вычисляются средние для каждого набора, которые программа помещает в переменные
xmean и ymean:
444
Глава 11
xmean = arith_mean(x, numvals);
ymean - arith_mean(y, numvals) ;
Затем программа входит в другой цикл, который проходит по всему набору
данных и генерирует значения, которые потребуются при вычислении
прямой. В переменной xbyy накапливается сумма произведений х * у, а
переменным sumx и sumy присваиваются суммы всех элементов х и у. Наконец, в
переменной sumxsq вычисляется сумма квадратов элементов х. Вот код,
производящий все эти вычисления:
for (i =0; i < numvals; i ++) {
xbyy = xbyy + (x[i] * y[i]);
sumx — sumx + x[i];
sumy = sumy + y[i];
sumxsq = sumxsq + (x[i] * x[i]);
}
Комбинируя эти значения, программа вычисляет обсуждавшийся ранее
коэффициент при х и константу в уравнении прямой. Вычисление коэффициента
xmultiplier можно было бы записать в одну строку, но в целях ясности это
вычисление разбито на два оператора. Константа вычисляется как ymean минус
xmultiplier, умноженный на xmean. Эти вычисления показаны ниже.
xmultiplier = ((double) numvals * xbyy) - (sumx * sumy);
xmultiplier - xmultiplier / (((double) numvals * sumxsq)
- (sumx * sumx));
constant = ymean - (xmultiplier * xmean);
Затем программа выводит некоторые значения и показывает получившееся
уравнение. Три оператора cout выполняют эту задачу, после чего код
возвращает управление:
cout « "The mean of the x values: " « xmean « endl;
cout « "The mean of the у values: " « ymean « endl;
cout « "The linefs definition: у = " « constant « " + "
« xmultiplier « "x" « endl;
return 0;
}
Регрессионный анализ этим не исчерпывается — к сожалению, необходимо
также вычислить значение, называемое стандартным отклонением* как для
значений */, так и для х. Используя это значение и набор исходных данных, вы
можете найти число между 0 и 1, называемое коэффициентом корреляции.
Это значение показывает, насколько близко прямая аппроксимирует
данные, — 0 показывает, что между значениями на самом деле нет взаимосвязи, а
при значении 1 точки идеально ложатся на прямую. Вообще говоря, это
значение никогда не будет равно 0 или 1, но может только приближаться к ним.
Стандартное отклонение мы вам оставили для самостоятельного
исследования. Вычисление его достаточно сложно, но его математические основания вы
можете найти в любом учебнике статистики.
Как уже говорилось, если вы запустите программу, используя значения
предопределенных массивов, то получите следующий результат:
Enter the number of values (integer, less than 30): 18
The mean of the x values: 26.2222
The mean of the у values: 109.356
The line's definition: у = 28.3846 + 3.08787x
Ларе Кландер
FractalsDoc.cpp
FractalsView.cpp
-ч« я
3.":.
-lb',!
afs^l^i
'Ok
*V i
Mi
«•is
* '-rf.
446
Глава 12
Для большинства людей, программистов и пользователей компьютеров,
машинная графика — это нечто забавное и интригующее. Но конечные
пользователи могут просто считать машинную графику интересной, в то время
как программисты порой бывают ею очарованы, потому что они понимают,
насколько сложны порождающие ее программы. В области программирования
графики можно выделить фрактальные образы, подобные показанному на
рис. 12.1, которые строятся на основе изощренных математических уравнений
в сочетании с не менее изощренными программными методиками.
I -■
Рис. 12.1. Сложный фрактальный графический образ
Фрактальные образы можно генерировать и отображать на любой
платформе, поддерживающей графику. В этой главе представлен программный код,
ориентированный, ради простоты, на отображение образов в GUI системы
Windows. Как вы увидите далее, основной объем обработки, производимой
кодом программы, связан исключительно с математикой. Таким образом, для
переноса рисующих фракталы функций на другие платформы потребуется
только изменить вызовы собственно графических методов (которые здесь
реализованы классами GUI Windows).
Введение во фракталы
В этой главе мы рассмотрим программу Visual C++, которая по
математическим уравнениям строит красочные изображения. Математические формулы
могут быть простыми и сложными.
Создание фракталов 447
Изображения, которые вы здесь увидите, могут, как и живописные
картины, быть выполнены в различных стилях. Слово абстрактный означает, что
образ не похож на то, что встречается в природе. Реалистический
подразумевает, что вы можете легко понять, что здесь нарисовано. Например,
художественный критик мог бы классифицировать как абстрактную картину,
изображающую синий круг на белом фоне (хотя круги вовсе не абстрактны), потому
что картина не обозначает конкретный предмет. По тем же соображениям
картина с изображением заката была бы признана реалистической. Фрактальные
образы, которые будут создаваться в этой главе, пользуются абстрактными
средствами для образования изображений, которые пользователь может
визуализировать в качестве «реальных* объектов.
Абстрактные или реалистические, фрактальные образы по большей части
состоят из мельчайших кружочков. Точно так же, как и картины, которые
обычно складываются из очень коротких линий (которые художник наносит
на холст отдельными мазками кисти). В этой главе для рисования
изображений, образующих фрактал, мы будем пользоваться функцией SetPixelV()
класса CDC (контекста устройства) библиотеки MFC. Эта функция позволяет
установить желаемый цвет отдельного пиксела. Кроме того, мы будем
рисовать на экране круги с помощью функции Ellipse() класса CDC.
Если вы рассмотрите изображение на телевизионном экране с близкого
расстояния, то увидите, что оно состоит из крохотных цветных прямоугольников.
Если отойти от экрана, вы снова увидите цельное изображение. Встроенные
методы SetPixelV() и Ellipse() в этой главе будут вызываться для рисования
цветных прямоугольников и кругов небольшого диаметра, определяемых
математическими уравнениями. Если вы отойдете от компьютерного экрана
(другими словами, когда вы воспримете все изображение целиком, а не
отдельные кружки и прямоугольники), то увидите образ, называемый
фракталом. Перед тем, как вы станете разрабатывать проект Visual C++, мы
рассмотрим краткое введение во фракталы, которое поможет вам лучше понять, как
они работают.
Впервые фракталы записал Гастон Джулия в начале двадцатого века.
Понятно, что из математического выражения для двух переменных (такого, как
Y = X) можно получить одно или много значений. (Очевидно, создание
сложных образов требует гораздо более сложных выражений.) Джулия вручную
рассчитал огромное число значений X и Y, а затем нанес их на бумагу в виде
точек в позициях, представляющих эти значения. По мере того, как он
рисовал все больше и больше точек, образ обретал форму. Эти образы
заинтриговали Джулию, и он описал свое произведение, известное теперь как множество
Джулии.
В середине 20-го века множества Джулии открыл Бенуа Мандельброт. Он
подумал о том, что для построения фракталов можно применить компьютеры.
Нарисовав свои множества, он и пустил в ход словечко «фрактал», образовав
его от латинского fractus, что значит «раздробленный» или «беспорядочный».
Например, если вы разобьете бокал для вина, то увидите беспорядочно
разбросанные осколки стекла. Мандельброт также описал свою работу; его фрактал
называется множеством Мандельброта.
Важно понять, что в обоих случаях множеств, и Джулии, и Мандельброта,
на полученную графику решающее влияние оказывают тонкие различия в
начальных значениях множества. Для одного из описанных в этой главе мно-
448
Глава 12
жеств, множества Kam Torus, программа генерирует начальные значения
случайным образом. Случайные начальные значения могут привести к
разительно отличающимся результатам для двух множеств, порождаемых одной и той
же математической функцией. Конкретные множества Мандельброта и
Джулии, построенные нашей программой, конечно, интересны, но можете
получить другие, возможно, еще более интересные результаты, варьируя
начальные значения, из которых выводится множество.
В проекте Fractals мы будем по заданному уравнению рассчитывать
значения, приводить их к форме, пригодной для отображения на экране в
определенном цвете, а затем рисовать по ним изображение. В математическом
выражении результат зависит от входного значения. Например, в выражение Y = X
можно подставить X - 2; тогда Y тоже будет равно 2; соответственно если X
равно 100, то и Y равно 100 и т. д. Чтобы построить по математическому
уравнению графическое изображение, вам придется рассчитать много значений х и
у, а затем в определяемых ими точках экрана нарисовать кружки или линии.
Краткое замечание о графиках
В проекте Fractals для определения позиции каждой точки фрактала в
изображении вы будете пользоваться методам графика. Когда программа
вызывает методы SetPixelVQ или ЕШрзе(), чтобы нарисовать прямоугольник или
кружок, Windows интерпретирует вызов и рисует каждый кружок или
прямоугольник в определенной позиции графика на экране компьютера. График
представляет позицию двумя значениями, называемыми х- и у-координатой.
Экран компьютера двумерный. Если вы покажете пальцем на какую-то его
точку, Windows получит значения х и у, отмечающие данную точку. График
представляет левый верхний угол компьютерного экрана значением х- и у-ко-
ординаты (0,0). По мере движения вниз вдоль левого края экрана значение у
будет расти, а при движении вправо вдоль верхнего края будет расти
координата х. Рис. 12.2 показывает логическую модель координатной системы,
отображенную на экран Windows.
Рис. 12.2.
Windows использует
координатный метод графика
(0,0)
(640,0)
(0,480)
(Экран Windows)
(640,480)
В зависимости от размера экрана правый нижний угол его может иметь
различные значения для х и у. В проекте Fractals функции написаны для экрана
размером 640x480 пиксел. Поскольку программа будет генерировать для
каждого уравнения тысячи значений X и Y, вы должны убедиться, что экран дос-
Создание фракталов
449
таточно велик и изображение попадает в его пространство. Манипулирование
пикселами (отдельными точками, которые показывает экран и из которых
образуется цельное изображение) позволяет вашей программе точно управлять
рисованием фрактального образа.
Как вы, вероятно, знаете, пикселы являются маленькими прямоугольными
элементами экрана компьютера, которые могут иметь любой цвет. Обычно они
бывают размером с булавочную головку; это мельчайшая единица измерения
на телевизионном или компьютерном экране. Когда вы приближаетесь к
экрану, то можете видеть маленькие точки, или пикселы. Отодвигаясь от экрана,
вы видите отображаемый в данный момент образ. Так как каждый экран —
компьютера или телевизора — составлен из многих пикселов, изображение
будет тем четче, чем больше пикселов имеется на экране.
В проекте Fractals вы будете выводить на экран много изображений
различного размера, от единственного пиксела до круга размером в половину экрана.
По мере рисования пикселов и кругов будет обретать очертания больший
образ, составленный из тысяч образов размером с пиксел. Программа помещает
каждый такой пиксел в точку на графике с определенными значениями х и у.
Обзор архитектуры документ/вид
В основе приложения MFC лежат концепции объекта документа и
соответствующего ему окна вида. Объект документа обычно служит
представлением файла, открытого приложением. Окно вида обеспечивает визуальное
представление данных документа и воспринимает интерактивные действия
пользователя. Отношение документа и вида — это отношение типа одного-ко-мно-
гим. Другими словами, документ может иметь много видов, но вид можно
ассоциировать только с одним документом.
В приложении Visual C++ объекты документов представляются классами,
производными от базового класса MFC CDocument. Классы окон вида выво-
. дятся из класса CView. Проект Fractals в этой главе объявляет один документ
с классом вида. Такая структура известна как интерфейс простого
документа (SDI).
Приложения SDI, которые вы генерируете с помощью AppWizard,
работают с единственным документом, имеющим единственный тип вида, и создают
только по одному представителю классов CDocument и CView. Рис. 12.3
показывает, какие классы могут поддерживать простое приложение SDI, которое
вы реализуете на основе объектов MFC.
В генерированных с помощью AppWizard приложениях SDI класс CMain-
Frame реализует само окно обрамления. В таких случаях AppWizard
определяет класс CMainFrame в файле заголовка Mainfrm.h и реализует его в
исходном файле Mainfrm.cpp. Класс CMainFrame наследует большую часть своих
функциональных свойств от класса CFrameWnd, представляющего собой
оболочку MFC для простого окна. Сам класс CFrameWnd в приложениях SDI
мало что делает. Важное исключение: если вы снабдили приложение строкой
состояния или стыкуемыми инструментальными линейками, то CFrameWnd
будет управлять созданием и инициализацией этих объектов.
13 За*. 1208
450
Глава 12
Объект приложения
(CWinApp)
указатель на:
Шаблон документа
(CSingleDocTemplate)
указатель на:,
Объект документа
(CDocument)
указатель на:
Объект вида
(CView)
указатель на:
Окно обрамления
(CFrameWnd)
указатель на:
Рис. 12.3. Модель отношений между пятью базовыми классами приложения SDI
Предоставляемый MFC класс CDocument обеспечивает базовое
функционирование объектов документа вашего приложения. Сюда входит создание
новых документов, сериализация (запись на диск) данных документа,
обеспечение взаимодействия документа с окном вида и многое другое. В MFC также
предусмотрен ряд производных от CDocument, предназначенных для создания
определенных типов приложений. Например, CRecordset и CDAORecordset
упрощают создание видов для баз данных. Взаимодействие между видами и
документами иллюстрирует рис. 12.4.
Рис. 12.4.
Отношение одного-ко-многим
между документом и его
видами
указатель на
CViewl
Представитель
(содержит все данные)
указатель на:
CScrollView
указатель на:
CView2
Для каждого производного от CDocument класса, предусматривающего
визуальный интерфейс с пользователем, имеется производный от CView класс,
обеспечивающий этот интерфейс. Этот класс при посредстве окна вида
формирует визуальное представление данных документа и обрабатывает
интерактивные действия пользователя.
Окно вида, со своей стороны, является дочерним окном окна обрамления.
В приложении SDI окно вида является дочерним для главного окна
обрамления. В MDI-приложениях окно вида является дочерним окном дочернего
Создание фракталов 451
MDI-окна. Окно обрамления, в свою очередь, может содержать несколько окон
вида (например, при разделении окна).
В программе Fractals мы используем всего один документ и единственный
вид. В документе хранится информация о выбранном пользователем
фрактале, который нужно нарисовать. Действительное рисование выполняет вид; он
содержит весь код, необходимый для вычисления и отображения точек,
составляющих фрактал.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Кстати, здесь важно отметить, что программа, так, как она
спроектирована, пишет непосредственно в контекст экрана, т. е. нигде не
сохраняется никакой копии. Если вы откроете другой экран поверх уже
нарисованного фрактала, Windows его сотрет. Вы можете обойти эту
неприятность, рисуя не прямо в окне, а в контексте устройства в
памяти, а затем копируя образ из памяти на экран. Такая методика
позволяет сохранять постоянным изображение, которое выводит программа,
и стирать его только тогда, когда вы сами решите это сделать.
Поскольку программа Fractals очень велика, давайте сразу приступим к ее
разбору. Мы обсудим содержимое всего двух ее файлов, так как только их код
существенно отличается от того, что автоматически генерирует AppWizard.
Более того, мы будем обсуждать только относящиеся к делу выдержки из этих
файлов. Обратитесь к коду программы на прилагаемой дискете, если захотите
получить какие-то дополнительные сведения — например, ID-определения в
используемом программой файле ресурсов.
Код
Как вы уже знаете, в классе документа хранится информация программы.
В случае CFractalsDoc класс просто хранит целое значение, указывающее,
какой фрактал выбрал пользователь, и возвращает его процедуре рисования в
классе CFractaisView, когда она его запросит. Вот код класса CFractalsDoc:
#include "stdafx.h"
#include "Fractals.h"
«include "FractalsDoc.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = FILE ;
#endif
j // CFractalsDoc
4 ^^ ^^
| IMPLEMENT DYNCREATE(CFractalsDoc, Сdocument)
ззиоэ Орт-[вл^Jessy: :ооаетв^ов^лэ ртол^Т^
эпаяа- jepjx# '
w
//////////////////////////////////////////////////////////////////A\
<i
едэц эроэ Битрвох ppe :оа01 // Ь *
} *
asTa f, ',
{ '
эдэц эроэ Butjo^s ррв :оа01 // * -^
> N
{() fiu-pao^ssi' **) ЗТ ' ч
{х* s3AT4oavo)ezTt«"c^as: :эоавх«ЗЭ*ллэ РТ°л*.
uoT^wzT^pxjBB ooasiBq.O'BJ.ao //[
{-'
i
{q.ueumoop зтщ вапъх ХТТ* s^ueumoop ias) // f J
элэх{ эроэ uoT^FZTt«T^Tuxsa ppp :oaOI // [!■»■!
г *<
•'ЗСТТО и-т^эд ' -^
(() зиэшпэоамэдио: : ч.иэитэоаэ i) ЗТ- * <
}
О^иэшпооалэмио: :=>oasxe^oBajD 1008»
OS.
>; .,
<г
.' О = итых^эвллзиэддго
эдэц эроэ иот^эпл^биоэ эшт^-эио ррв :оаОХ //
>
() зоавте^оелдэ: : Doasx*}o**.W к'
uoT^onj^s9p/uoTq.otu^suoo эоазхс^эедлэ //j
п ~ ~ &-
OdYW 3DVSS3W ONSj
_ «avw osw эотШ/ s <
(^одяхэриви8х«^овадио /j^)Ha,iacttww_s,rvxDY>iLa_ai)aNvwwoojio ' .л
(эвтхпгзхв^эвллио 'SYI'IQr_S,IYi3\rai[_ai)aNVWW00_N0 '
(8рпохЭ8Х«ЗЭ*;гдио ySOnO'ID_STYiDYHJ_ai) ONVWWODJMO
(jjnasxF^oEj^uo /5.ana_s,ivi0VH^_ai) okvwwoojto
(8пло^игехвх«^ЗЕДлиО 'SflHOIWra STYXOYSLI a£)ONVWW0D NO
(ooasxB^opajD)d^W_3SW XWU// j ,
(^иэитэоаэ 'voqbxviovx&o)ачя aovssaw Ni9aal6i*
1. J.
г/ esBirj
ZS*
Создание фракталов ] 453
CDocument: :AssertValid() ;
Ъ jvoid CFractalsDoc::Dump(CDumpContexts dc) const
I
CDocument::Dump(dc);
lendif //_DEBUG
///////////////////////////////////////////////////////////////////
// CFractalsDoc commands
p|
;r, Lnt CFractalsDoc: :GetCurrentFractal ()
1 {
return Cur rentFr actalNum;
)
iroid CFractalsDoc: : SetCurrentFractal (int FracNum)
{
CurrentFractalNum - FracNum;
i
void CFractalsDoc::OnFractalsKamtorus()
, SetCurrentFractal(1);
UpdateAllViews(NULL);
}
void CFractalsDoc::OnFractalsDuff()
{
SetCurrentFractal(2);
UpdateAllViews(NULL);
}
void CFractalsDoc::OnFractalsClouds()
SetCurrentFractal(3);
UpdateAllViews(NULL);
void CFractalsDoc::OnFractalsJulias()
i
SetCurrentFractal(4);
UpdateAllViews (NULL) ;
}
void CFractalsDoc::OnFractalsMandelbrot()
tLJL
SetCurrentFractal(5);
UpdateAllViews (NULL) ;
454
Глава 12
i lint CFractalsDoc::GetCurrentFractalO
return CurrentFractalNum;
}
■•nvoid CFractalsDoc: :SetCurrentFractal (int FracNum)
■"-I1
.•1 CurrentFractalNum = FracNum;
r.h
П
^Jvoid CFractalsDoc::OnFractalsAbstract{)
£.*] SetCurrentFractal(3);
r"V1 UpdateAllViews (NULL) ;
£:iL__ - . . ___
| ПРИМЕЧАНИЯ
Давайте рассмотрим некоторые фрагменты этого кода. Многие функции
делают одно и то же, поэтому мы разберем только обобщенную их структуру.
Первая специализированная функция, объявленная как открытый метод
класса CFractalsDoc — это GetCurrentFractalO; она возвращает целое
значение, соответствующее выбранному пользователем фракталу.
int CFractalsDoc::GetCurrentFractalO
{
return CurrentFractalNum;
}
Как вы можете видеть, CurrentFractalNum является элементом данных
класса. В соответствии с принципами корректного проектирования элемент
объявлен как закрытый и доступен извне только при посредстве
интерфейсных функций. Если GetCurrentFractalO возвращает его текущее значение» то
SetCurrentFraetalO принимает значение в качестве параметра и присваивает
его элементу CurrentFractalNum, что показано ниже:
void CFractalsDoc::SetCurrentFractal(int FracNum)
{
CurrentFractalNum = FracNum;
}
Все другие функции класса CFractalsDoc являются функциями обработки
сообщений. Когда пользователь выбирает фрактал в меню Fractals
приложения, программа направляет сообщение одной из определенных в классе
функций. Каждая из них устанавливает номер текущего фрактала а затем вызывает
функцию MFC UpdateAllViews(), которая заставляет программу перерисовать
вид. Функция OnFractalsKamtorusO, например, устанавливает номер
фрактала равным 1 и запускает обновление вида:
void CFractalsDoc::OnFractalsKamtorus()
<
SetCurrentFractal(1);
UpdateAllViews(NULL);
}
Создание фракталов 455
Значение, устанавливаемое каждым из обработчиков сообщений,
интерпретируется затем в функции OnDraw() класса CFractalsView оператором switch.
Главная обработка, таким образом — по крайней мере там, где речь идет о
рисовании фракталов, — производится в классе CFractalsView. Этот класс
является центром всей программы, и мы теперь переходим к рассмотрению его
компонентов.
Код
Ниже приводится код реализации класса CFractalsView. Как и в случае
класса CFractalsDoc, мы показываем только специализированный код,
введенный в файл. Когда вы посмотрите на сам файл, то увидите некоторые
определения и функции, генерированные AppWizard при создании оболочки
приложения.
Итак, вот код класса CFractalsView:
* ^include "stdafx.h"
-' J#include "Fractals.h"
■v ,,# include <cstdlib>
ь i #include <cmath>
*include <complex>
j#include "FractalsDoc.h"
I #include "FractalsView.h"
■■ J
Ittifdef __DEBUG
. ,tfdefine new DEBUG__NEW
■ -ttundef THIS_FILE
r jstatie char THISJFILEU = FILE ;
#endif
?-' •
' 'using namespace std;
Г ///////////////////////////////////////////////////////////////////
// CFractalsView
t MPLEMENT_DYNCREATE (CFractalsView, CView)
\ ,BEGIN_MESSAGE_MAP{CFractalsView, CView)
//{(AFX_MSG_MAP(CFractalsView)
J // NOTE - the ClassWizard will add and remove mapping macros
1 ' // here.
// DO NOT EDIT what you see in these blocks of generated code!
//))AFX_MSG_MAP
■ END MESSAGE_MAP()
• ///////////////////////////////////////////////////////////////////
// CFractalsView construction/destruction
- CFractalsView::CFractalsView()
{
мм. // TODO: add construction code here
456
Глава 12
'')
' CFractalsView::"CFractalsView()
{
}
BOOL CFractalsView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
void DuffDraw (CDC *pDC, double x, double y, int cl)
{
double il, jl, c;
int i, k;
DWORD Color;
CPen* pOldPen;
CPen DrawPen;
// CBrush* pOldBrush;
// CBrush DrawBrush;
Color = RGB(rand() % 255, rand() % 255,rand() % 255);
DrawPen.CreatePen(PS_SOLID, 4, Color);
pOldPen = pDC->SelectObject(&DrawPen);
// DrawBrush.CreatesolidBrush(Color);
1 // pOldBrush ■ pDC->SelectObject(&DrawBrush);
\
Л il = 150.0*x + 320.0;
jl = -176.0*y + 240.0;
к = 7;
for (i=l;i<7;i++) {
с ■ 0.09*i;
pDC->Ellipse((int)(il+c),(int)(il-c), (int)(jl+c),
(int)(jl-c));
// circle ((int)(il+c),(int)(jl+c),k);
k— ;
}
pOldPen = pDC->SelectObject(p01dPen);
// pOldBrush = pDC->SelectObject(pOldBrush);
return;
}
void DrawDuff(CDC *pDC)
{
int kl;
double a, cl, t, x, y, xl, yl, pi=3.141592653589793, twopi;
} x « 1.0
'# у = 0.0
] t = 0.0
i a = 0.3
Создание фракталов
457
twopi = 2.0*pi;
for (int loop = 0; loop < 500; loop++) {
kl++;
xl - x + y/twopi;
yl = у + (-(x*x*x) + x -0.25*y + a*cos(t))/twopi;
I ] t = 0.01*(kl % 628);
x = xl ;
у = yl;
if (t > pi)
cl = 0;
else
cl = 7;
DuffDraw(pDC, x, y, cl);
r
К i
t I
}
\ ivoid DrawKamTorus(CDC *pDC)
t-
b
int a, c, nx, ny;
time_t t;
double an, can, san, canl, sanl, e, ax, ay;
r . double x, xa, xl, x2, x3, y, yl, y2, уЗ, randl, rand2;
!§ 1 CPen* pOldPen;
j \ CPen DrawPen;
i
г
I.
DrawPen.CreatePen(PS_SOLID, 1, RGB(rand{) % 255, rand()
% 255,rand() % 255));
pOldPen = pDC->SelectObject(SDrawPen);
nx = 320;
ny = 240;
ax = 400.0;
ay = ax;
с = 1;
srand((unsigned) time(fit));
randl = rand() % 20000;
rand2 = rand() % 20000;
randl = 5.0e-5*randl;
rand2 = 5.0e-5*rand2;
an = 10.0*(randl-rand2);
can - 0.99*cos(an);
san = 0.99*sin(an);
canl = 1.01*cos(an);
sanl
x3 =
y3 =
do {
xa
x2
= 1.01*sin(an);
0.01;
0.01;
= x3*x3 - y3;
= x3*canl + xa*sanl;
y2 = x3*sanl - xa*canl;
x3 = x2;
y3 = y2;
458
Глава 12
.а
х = х2;
У - у2;
а = 0;
do {
ха — х*х - у;
xl » x*can + xa*san;
yl - x*san - ха*сап;
х = xl;
у = yl;
а++;
i pDC->SetPixelV((int)(ax*x+nx) -1, (int)(ay*y+ny) +1, с);
1 } while ((fabs(xl)<=2.0e3) && (fabs(yl)<=2.ОеЗ) &S a <=100);
e = e + 0.075;
с = rand О % 32767;
} while ((fabs(x2) <= 2.ОеЗ) &£ (fabs(у2) <= 2.ОеЗ));
Ь
1
Лdouble absrandom(void)
{
int random_integer, temp_integer;
double random_double, temp_double;
i random_integer - rand();
randoro_double = (double)random_integer / RAND_MAX;
temp_integer = rand() % 32767;
] temp_double = (double)temp^integer / 1000000000L;
random_double += temp__double ;
j return(random_double);
i>
ivoid DrawAbstract(CDC *pDC)
I int Raio[5000] , Cx[5000], Cy[5000], color, npix, npiy, iopt;
, float Tcor, Cor[5000];
int xpix, ypix, Cmax, ic, index, dx, dy, Rmax;
\
1 npix - 640;
I npiy - 480;
'l Cmax = (rand() % 2000) + 500;
* Rmax = (rand() % 320) -I- 1;
i
for (ic=l; ic < Cmax; ic++) {
Cx[ic] — (int)(npix*abs_random{));
Cy[ic] = (int)(npiy*abs_random());
I* Raio[ic] = (int) (Rmax*abs_random()) ;
4 if (iopt == 1)
■ ■* Cor[ic] = (float) (16.0*abs random() + 15.0);
"-■* else
i Cor[ic] = (float) (256.0*abs_random());
■< )
] for (xpix=0; xpix < (npix-l); xpix++) (
j for (ypix-0; ypix < (npiy-l); ypix++) {
лJ index = 0;
"' 1! Tcor = 0;
Создание фракталов 459
V'l
\ *j for (ic-1; ic < Cmax; ic++) {
; /J dx = xpix - Cx[ic] ;
|! *I dy - ypix - Cy[ic];
if ((int)(dx*dx + dy*dy) <= Raio[ic]*Raio[ic]) {
index++;
* Tcor = Tcor + Cor[ic];
> }
}
i.1
t
if (index > 0)
color - (int)(Tcor/index);
else
I. color = 0;
*• .■■
pDC->SetPixelV(xpix,ypix,(color * 16))
4
jvoid DrawJulias(CDC *pDC)
, M
"' i double xmin, xmax, ymin, ymax, fact=1.0;
- ( double ypy, x, y, x0, y0, xp, yp, const_scr=l.0;
J" | double deltax, deltay, pmin, qmin, ya, xkpl, ykpl, r;
r I int npix=640, npiy-480, kcolor;
s int k, np, nq, npy, ipen;
*■ !
' j pmin = -0.74356;
у \ qmin = 0.11135;
^ t a xmin = -2.0;
■ ' xmax - 2.0;
,ft * ymin = -2.0;
, j ymax = 2.0;
!' *1 kcolor = 255;
\'\ if(fact>=1.0 || fact <==0.0)
- ' fact = 1.0;
else {
npix = (int)(npix*fact);
, A npiy = (int)(npiy*fact);
' ' 1
• i УРУ - (double)npiy - 0.5;
i «' deltax ~ (xmax-xmin)/(npix-1);
t .j deltay = (ymax-ymin)/(npiy-1);
» ■] for (np=0; np<=npix-l; np++) {
' i xO = xmin -I- (double)np * deltax;
4' 'l for (nq=0; nq<=npiy-l; nq++) {
yO = ymin + (double)nq * deltay;
x ■ xO;
У = y0;
к = 0;
do {
xkpl = (x+y)*(x-y) + pmin;
ya = x*y;
ykpl = ya + ya + qmin;
•Л
\
t
460
Глава 12
г = xkpl*xkpl + ykpl*ykpl;
к++;
if (г >= kcolor) {
ipen = к;
i хр - const_scr*(double)пр;
j yp = (double)nq;
J pDC->SetPixelV(xp,yp,ipen);
}
if (k = kcolor) (
j ipen - RGB(0, 0, 255);
1 xp == const_scr* (double) np;
\ УР — (double)nq;
' pDC->SetPixelV(xp,yp,ipen);
! )
J x = xkpl;
I y = ykpl;
1 } while (r <= kcolor && k<=kcolor);
>
■ )
\>
i
Jdouble MandelSetPoten(double ex, double cy, int maxiter)
t ,<
double x, y, x2, y2, temp, potential;
int iter;
* x = ex;
J x2 = x*x;
у = cy;
у2 = y*y;
iter = 0;
do {
■ i temp = x2 - y2 + ex;
1 у = 2.0*x*y + cy;
x = temp;
x2 = x*x;
y2 = y*y;
iter++;
} while ((iter<maxiter) && ((x2+y2)<10000.0));
i if (iter<maxiter)
potential = 0.5*log(x2+y2)/powl(2.0,iter);
' 1 else
i potential = 0.0;
I return (potential);
!>
и ■
'void DrawMandelbrot(CDC *pDC)
* int nx, ny, iy, ix, ipen, maxiter = 16000, iflag-0, iset = 1;
complex<double> c;
1 * double xmin=-2.25, ymin=-1.25, xmax=0.75, ymax=1.25;
ex, cy, potent;
■ J double diff=0.6482801, testl, test2;
Создание фракталов
461
1 4
. I
** _
if ((maxiter>=16000) || (maxiter<=0))
maxiter = 16000;
пх = 640;
ny ~ 480;
ymin = -1.125;
ушах = 1.125;
for (iy-0;iy<=ny-l;iy++) {
cy = ymin + iy*(ymax-ymin)/(ny-1);
for (ix=0;ix<=nx-l;ix++) {
"\j ex — xmin + ix*(xmax-xmin)/(nx-1);
• ,j c.real (ex) ;
c.imag(cy);
t -jj testl = 2.0;
if ((ex >= -7.55e-l) && (ex <= 4.0e-l)>
if ((cy >= -6.6e-l) && (cy <= 6,6e-l))
testl = abs(1.0 - sqrt{1.0-4.0*c));
}
test2 = 2.0;
if ((ex >= -1.275*0) && (ex <« -7.45e-l)) {
if ((cy >= -2.55e-l) £& (cy <= 2.55e-l))
test2 = abs(4.0*(c+1.0));
}
if (testl<=1.0) {
potent = 0;
iflag = 1;
if (iset != 0)
ipen = 126;
else
ipen = 32;
>
else if (test2<=1.0) {
potent = 0;
iflag = 1;
if (iset != 0)
ipen = 104;
else
ipen = 32;
}
else {
potent = MandelSetPoten(cx,cy,maxiter);
iflag = 0;
}
if ((potent = 0.0) && (iflag=*0))
ipen = 32;
else if ((potent !=0) &fi (iflag==0))
ipen = (int)(33.0 + 15.0*(potent-33.0)/diff);
pDC->SetPixelV(ix,iy,ipen);
}
}
}
void CFractalsView::OnDraw(CDC* pDC)
{
CFractalsDoc* pDoc = GetDocument();
462
Глава 12
"' ч ASSERT__VALID(pDoc) ;
pDC - GetDC() ;
switch (pDoc->6etCurrentFractal() )
case 0:
*i break;
f--"a| case 1:
; a, | DrawKamTorus (pDC) ;
гЩ pDoc->SetCurrentFractal(0) ;
„* \ break;
l:' j case 2:
DrawDuff(pDC);
pDoc->SetCurrentFractal(0);
break;
case 3:
DrawAbstract(pDC);
pDoc->SetCurrentFractal(0);
break;
case 4:
DrawJulias(pDC);
pDoc->SetCurrentFractal(0);
break;
case 5:
DrawMandelbrot (pDC) ;
pDoc->SetCurrentFractal(0);
break;
r. - :
Т-Л
V',5-
J-">
}
( ПРЕ
ПРИМЕЧАНИЯ
Как видите, объем вычислений при рисовании фракталов получается не
маленький, хотя обработка от фрактала к фракталу будет меняться. Много кода
в процедурах различных фракталов дублируется; однако достигаемая при
этом сравнительная автономность упрощает извлечение их из файла и
использование в других программах.
Директивы include в верхней части файла достаточно понятны. В
дополнение к стандартным заголовкам MFC нам нужно подключить математическую
библиотеку С и определение типа complex из библиотеки стандартных
шаблонов. (Вы поближе познакомитесь с типом complex ниже, когда мы будем
обсуждать функцию построения множества Мандельброта.) Как обычно, мы
используем пространство имен std, чтобы упростить программирование вызовов
библиотеки стандартных шаблонов.
#include "stdafx.h"
#include "Fractals.h"
#include <cstdlib>
#include <cmath>
#include <complex>
#include "FractalsDoc.h"
#include "FractalsView.h"
using namespace std;
Создание фракталов 463
[ ОБСУЖДЕНИЕ DUFFINGS-ОСЦИЛЛЯТОРА
Первая из больших функций программы рисует Duff-фрактал (также
называемый Duffings-осциллятором), Этот фрактал прорисовывает ряд
окружностей в «трехмерном» пространстве, давая в результате нечто вроде
перевернутого конуса. Нарисовать Duff-фрактал в MFC является непростой задачей,
если только вы не используете технологию DirectDraw, потому что метод ЕШр-
se() класса CDC не позволяет при рисовании эллипса задать прозрачный фон.
Поэтому вместо концентрических колец, в виде которых должен отображаться
Duff-фрактал, программа выводит ряд колец, каждое из которых нарисовано
поверх предыдущих и выглядящих несколько странно. Однако DirectDraw
выходит далеко за рамки этой книги; а рисование окружностей в других
операционных системах, например, DOS, не приводит к такому их перекрытию.
Рисованием Duffings-осциллятора управляют три функции: одна
вычисляет х-координату, другая — у-координату, а третья определяет значение t (в
радианах), которое осциллятор использует на следующем шаге для определения
у-координаты (и косвенно х-координаты). Вот эти функции:
х<п+1) = х(п) + (у(п) / (2 * pi))
у(п+1) = у(п) + (-(х(п)*3) + х(п) - 0.25*y(n) + a*cos(t)) / (2 * pi)
t = 0.01 * (kl % 628)
Значение t находится в диапазоне от 0 до 2 * pi, и обычно увеличивается с
шагом 0.01, хотя вы можете изменить этот шаг и соответственно характер
осцилляции рисуемой формы.
Если вы запустите программу Fractals и выберете Duff-фрактал, то в
результате увидите фигуру, подобную показанной на рис. 12.5.
■ ;■■;'--; \ лг*ял^- *та£" ^Ф>эд?тяге .^•■^лу-'fcy. ^■v^-v:-'-^'4''- \- ~'''
■■■ч-
Рис. 12.5. Duff-фрактал, нарисованный программой Fractals
464
Глава 12
В рисовании фрактала участвуют на самом деле две функции. В основном
это сделано потому, что функция Ellipse() рисует свои фигуры текущими
пером и кистью. Из-за особенностей контекста устройства в Windows
графические объекты должны выходить из области действия во избежание ошибок
при управлении пером и кистью. Поэтому процедура DuffDraw() рисует
несколько (а именно семь) эллипсов и возвращает управление функции Draw-
Duff() для определения следующей начальной точки для рисования эллипсов.
DuffDraw() принимает три параметра: контекст устройства и пару координат х
и у, определяющих позицию рисуемых эллипсов. DuffDraw() также объявляет
некоторые локальные переменные, в частности, il и jl, которые сохраняют
текущую информацию о рисуемых кольцах, и с, которая содержит смещение
для текущего кольца. Наконец, функция объявляет объекты СРеп для
управления цветом. Вот функция DuffDraw():
void DuffDraw (CDC *pDC, double x, double y, int cl)
{
double il, jl, c;
int i, k;
DWORD Color;
CPen* pOldPen;
CPen DrawPen;
// CBrush* pOldBrush;
// CBrush DrawBrush;
Обратите внимание на заглушки для объявлений объектов CBrush. Хотя в
данном варианте программа их не использует, вы можете с тем же успехом
рисовать не пустые кольца, а заполненные. Однако полученное в этом случае
изображение будет выглядеть совсем по другому. Как говорилось выше,
оптимальным было бы рисование прозрачных колец, но такая возможность в
интерфейсе графического устройства MFC отсутствует.
Color *= RGB (rand () % 255, rand{) % 255, rand () % 255);
DrawPen.CreatePen(PS_SOLID, 4, Color);
pOldPen - pDC->SelectObject(SDrawPen);
// DrawBrush.CreateSolidBrush(Color);
// pOldBrush = pDC->SelectObject(«DrawBrush);
После объявления объектов программа создает случайное RGB-значение и
затем инициализирует перо толщиной 4 (пиксела) случайного цвета,
присвоенного переменной Color. Затем перо выбирается в контекст устройства, как
этого требует MFC. Если бы вы рисовали кружки с заполнением цветом кисти,
то кисть сейчас также нужно было бы выбрать в контекст устройства, как
показывает заглушка.
Затем программа определяет экранные координаты для рисования
кружков, присваивая их значения переменным il и jl. Это необходимо сделать, так
как Duff-фрактал описывает точки в ограниченном диапазоне значений.
Значения для х лежат между -2 и 2, а для у между -1 и 1:
il = 150.0*х + 320.0;
jl = -176.0*у + 240.0;
Фиксированные значения в этих уравнениях смещают изображение в центр
экрана; если вы работаете с экраном большим, чем 640 х 480, вам, вероятно,
целесообразно будет увеличить значения смещений — и множителей, — чтобы
получить более сбалансированное и информативное изображение.
Создание фракталов ___™ 46$
Теперь, когда мы знаем точки экрана, соответствующие значениям,
определяемым уравнениями фрактала, можно пойти дальше и нарисовать кружки на
экране:
for (i=l;i<7;i++) {
с = 0.09*i;
pDC->Ellipse{(int)(il+c),(int)(il-c), (int)(jl+c),
(int) (jl-c));
}
Цикл просто рисует семь эллипсов, все одного и того же цвета, в некотором
диапазоне точек. Вы можете увидеть этот процесс рисования, когда программа
запущена, так как приложение по видимости «зацикливается», рисуя семь
кружков, возвращаясь в вызывающую функцию, чтобы получить новую пару
начальных координат, и снова исполняя цикл рисования.
pOldPen = pDC->Select0bject(p01dPen);
// pOldBrush = pDC->SelectObject(pOldBrush);
return;
}
Когда семь кружков нарисованы, программа перед возвратом из функции
приводит все в порядок, возвращая цвет пера к значению, которое он имел до
входа в функцию, и делая то же самое для цвета кисти, если вы ее используете.
После того, как функция завершилась, управление возвращается
вызывающей процедуре — в данном случае это DrawDuff(), которая производит
математические вычисления значений фрактала и вызывает процедуру рисования.
Функция DrawDuff() вызывается из CFractalsView::OnDraw(); последняя
активируется при выборе пользователем пункта Duff в меню Fractals. Функция
принимает указатель на текущий контекст устройства и объявляет ряд
локальных переменных. Как и во всех других функциях фракталов, которые вы
увидите в дальнейшем, все эти разнообразные переменные используются
функциями программы для вычисления каждой из точек, составляющих
фрактал. Вот DrawDuff():
void DrawDuff(CDC *pDC)
{
int kl;
double a, t, x, y, xl, yl, pi=3.141592653589793, twopi;
x = 1.0; // Начальное значение должно быть между -2.0 и 2.0 *
у = 0.0; // Начальное значение должно быть между -1.0 и 1.0
t - 0.0; // Начальное значение должно быть между 0 и 6.25
а = 0.3; // Начальное значение должно быть между 0 и 1.0
twopi = 2.0*pi;
Мы часто в дальнейшем будем объявлять переменные индивидуально, а не
в одну строчку, чтобы вам проще было «поиграть» со значениями и увидеть
получившийся результат. В случае Duff-фрактала можно изменять четыре
переменные с комментариями, получая в результате различные графики
фрактала. Можно также изменить значение kl, что также меняет вид фрактала.
Переменная twopi является, конечно, просто удвоенным математическим к. После
установки начальных значений программа входит в цикл, задающий число
итераций вычисления. Заметьте, что это число можно сделать гораздо боль-
466 Глава 12-
П1им, и программа будет осциллировать большее число раз; 500 раз — это
просто, так сказать, разумное число «для начала». Вот этот цикл:
for (int loop = 0; loop < 500; loop++) {
kl++;
xl = x + y/twopi;
yl = у + (-(x*x*x) + x -0.25*y + a*cos(t))/twopi;
Фрактал так быстро меняет характер выводимой им информации из-за
взаимозависимой природы переменных х и у в уравнениях. Как можно видеть,
при каждом проходе цикла программа вычисляет xl (новое значение х) как
функцию значений старого х и старого у. Затем она вычисляет yl (новое
значение у) как функцию старых х и у и результата умножения косинуса t на
константу, определенную перед началом цикла. Отсутствие прямой зависимости
значений у от значений х является решающим условием для порождения
фракталов; когда вы перейдете к более сложным фракталам, то поймете, что
Duff-уравнение на самом деле совсем простое.
После расчета новых значений для xl и yl программный код увеличивает t
на некоторую дробную величину:
t = 0.01* (kl % 628);
х = xl ;
у = yl;
DuffDraw(pDC, x, y);
}
}
(Однако если число циклов превосходит 628, значение t фактически будет
осциллировать между 0 и 6.28, что равняется примерно двум я. Вы можете
также увеличивать этот итератор каждый раз на 0.02, написав оператор вида
t = 0.02*(kl % 314). После этого код присваивает переменным х и у их новые
значения и вызывает процедуру рисования DuffDraw(). Результат
выполнения всех этих циклов уже был показан на рис. 12.5.
Duffings-осциллятор, хотя и интересен — и ценен, если рассматривать его
как отправную точку, — генерируется сравнительно просто. Более интересные
фигуры можно получить, если рисовать на уровне пикселов и генерировать
ряды значений по более сложным формулам. Последние три из
рассматриваемых здесь фракталов на самом деле прорисовывают каждую точку на
поверхности дисплея, даже если в этой точке ничего нет. Но сначала мы познакомимся с
фракталом под названием Kam Torus, который все-таки не заходит так далеко.
[фрактал kam torus
Фрактал Kam Torus рисует последовательность торов. Тор является
поверхностью 1-го порядка и, таким образом, имеет одну «дырку». Обычный тор
в трехмерном пространстве имеет форму бублика, но понятие тора
оказывается чрезвычайно полезным и в пространствах высшей размерности. Обычное
трехмерное «кольцо» в старой литературе называлось якорным кольцом.
График Kam Torus порождается наложением рядов точек в некотором локусе
точек (в литературе по фракталам часто называемом орбитой), генерируемых
набором уравнений, в которых переменная на каждом шаге увеличивается на
единицу. Три уравнения, управляющих рисованием, имеют такой вид:
Создание фракталов 467
х(0> = у{0> - orbit / 3
x(n+l) - x(n)*cos(a) + (x2(n) - y(n))*sin(a)
y(n+l) =x(n)*sin(a) - <х2(п) - у(n))*cos(a)
После каждого прохода цикла значение orbit получает некоторое
фиксированное приращение (величина шага). Параметры, задающие поведение
функции, включают в себя угол а (в радианах), величину шага для переменной
orbit, конечное значение этой переменной и количество точек на орбиту,
задающее число проходов цикла. Можно также создать трехмерный вариант
фрактала, рассматривая значение orbit в качестве z-координаты каждого ряда. Как
в двумерном, так и в трехмерном варианте можно варьировать значение,
определяющее максимальное число итераций, чтобы управлять количеством
построенных орбит и, соответственно, сложностью рисунка.
При каждом запуске программы Fractals графики, построенные
процедурой DrawKamTorus(), будут меняться, поскольку для генерации начальной
точки уравнений код вызывает рандомизирующие функции. На самом деле
при любых двух запусках получаемые графики могут выглядеть столь же
различно, как показанные на рис. 12.6.
468
Глава 12
Подобно процедуре DrawDuff(), DrawKamTorus() принимает единственный
параметр — указатель на контекст устройства. Затем DrawKamTorus()
определяет ряд переменных типа int и double, используемых при вычислении точек
фрактала. Процедура устанавливает некоторые значения, которые можно изменить,
если вы работаете экраном большим, чем 640 х 480. Врт начало процедуры:
void DrawKamTorus(CDC *pDC)
i
int a, c, nx, ny;
time_t t;
double an, can, san, canl, sanl, e, ax, ay;
double x, xa, xl, x2, хЗ, у, yl, y2, уЗ, randl, rand2;
nx ■ 320;
ny = 240;
ax = 400.0;
ay = ax;
с = 1;
Затем программа «засевает» генератор случайных чисел с помощью
функции srand() и генерирует несколько случайных значений, служащих основой
для вычисления фрактала. После этого она также вычисляет две пары синусов
и косинусов, умножая одну пару на значение чуть меньшее, а другую — чуть
большее единицы. Вот этот код:
srand((unsigned) time(fit));
randl = rand(> % 20000;
rand2 = rand() % 20000;
randl = 5.0e-5*randl;
rand2 = 5.0e-5*rand2;
an = 10.0*(randl-rand2);
can = 0.99*cos(an);
san - 0.99*sin(an);
canl = 1.01*cos(an);
sanl = 1.01*sin(an);
Наконец, программа присваивает начальные значения переменным хЗ и уЗ
и входит в главный цикл do, рисующий фрактал. Цикл завершится, когда
значение либо х2, либо у2 выйдет за допустимые границы.
хЗ = 0.01;
уЗ = 0.01;
do (
ха = хЗ*хЗ -
х2 = x3*canl
у2 - x3*sanl
хЗ - х2;
уЗ - У2;
х = х2;
у = у2;
а = 0;
уЗ;
+ xa*sanl;
- xa*canl;
При входе в главный цикл программа рассчитывает некоторые начальные
значения для вычисления последовательности графиков. Фрактал Каш Torus
на самом деле представляет собой совокупность отдельных графиков.
Внутренний цикл, приведенный ниже, рисует ряд точек, число которых не превыша-
Создание фракталов
469
ет 100. Внешний цикл, начальный фрагмент которого показан выше, будет
повторяться, пока значение х2 или у2 не превысит заданного предела.
Внутри цикла вычисляются значения ха, х2 и у2, исходя из текущих
значений хЗ и уЗ, а также некоторой стохастической информации,
генерированной в начале функции. Затем программа входит во внутренний цикл do и
генерирует действительные точки графика:
do {
ха — х*х - у;
xl = x*can + xa*san;
yl - x*san - ха*сап;
Внутренний цикл начинается с расчета текущей точки. По текущим
значениям х и у вычисляется ха, значение которой, вместе с текущими х и у и ранее
рассчитанными синусом и косинусом, используется для вычисления значений
xl и yl.
После этого программа присваивает х и у новые значения, равные xl и yl,
увеличивает значение счетчика (который гарантирует, что во внутреннем
цикле будет выведено не более 100 точек) и рисует на экране пиксел:
х = xl;
у = yl;
а++;
pDC->SetPixelV((int)(ax*x+nx) -1, (int)(ay*y+ny> + 1, с);
} while ((fabs(xl)<=2.0e3) && (fabs(yl)<=2.0e3) && a <=100);
Цвет пиксела задается переменной с, которая при каждом проходе
внешнего цикла получает новое случайное значение. Заметьте, что цикл завершается
не только в случае, если а превысит 100, но и тогда, когда абсолютная
величина xl или yl превысит 2000.
е = е + 0.075;
с = rand() % 32767;
} while ((fabs(x2) <= 2.0еЗ) && (fabs(y2) <= 2.0еЗ));
}
Возвратившись во внешний цикл, программа устанавливает новое значение
цвета для рисования следующей серии пикселов. Затем она проверяет, не
превзошло ли значение х2 или у2 по абсолютной величине двух тысяч. В этом
случае происходит выход из цикла и из функции. Если же абсолютные значения
х2 и у2 меньше 2000, внешний цикл повторяется сначала, вычисляя новую
начальную точку и рисуя выведенные из нее новые 100 точек.
| АБСТРАКТНЫЙ ФРАКТАЛ
Следующий фрактал, называемый абстрактным, является первым из тех,
что используют стандартный метод построения: установку цвета каждой точки в
области рисунка. Как вы увидите далее при разборе фракталов Джулии и Ман-
дельброта, применение данного метода позволяет строить чрезвычайно сложные
образы. Кроме того, в абстрактном фрактале вы увидите, что метод позволяет
генерировать случайные фигуры без каких-либо узнаваемых форм. Хотя
абстрактный фрактал (рис. 12.7) интересен, он не кажется «осмысленным».
470
Глава 12
й^^'ЛФЙ^^
Рис. 12.7. Абстрактный фрактал, нарисованный красным и черным
Абстрактный фрактал не обязательно выводится из какой-либо известной
порождающей функции, он просто применяет фрактальную теорию к
рисованию последовательностей. В нем используется генератор случайных чисел
типа double — несколько более сложный, чем тот, что генерирует случайные
целые. Для генерации случайных значений двойной точности вызывается
функция abs_random(). Функция не имеет параметров и возвращает
вызывающей программе одиночное значение типа double.
В функции объявляются четыре локальные переменные, две из которых
хранят случайные целые, а другие две — случайные значения double.
double abs_random(void)
i
int random_integer, temp_integer;
double random_double, temp_double;
Затем генерируются два случайных целых числа, одно в диапазоне от 0 до
RAND_MAX, другое в диапазоне от 0 до 32767.
random_integer = rand() ;
random__double = (double)random_integer / RAND MAX;
temp_integer = rand() % 32767;
temp_double = (double)temp_integer / 1000000000L;
random_double +- temp__double;
return(random double);
Создание фракталов 471
Функция использует два случайных целых числа, чтобы генерировать одно
число двойной точности, значение которого лежит где-то между 0 и 1.
Полученное число возвращается в качестве результата функции.
Функция DrawAbstract() принимает единственный параметр — указатель
на контекст устройства (как и все остальные функции фракталов) — и
объявляет локальные переменные, используемые в процессе вычислений.
void DrawAbstract(CDC *pDC)
<
int Raio[5000], Cx[5000], Cy[5000], color, npix, npiy, iopt;
float Tcor, Cor[5000];
int xpix, ypix, Cmax, ic, index, dx, dy, Rmax;
npix = 640;
npiy * 480;
Cmax = (rand() % 2000) + 500;
Rmax = (randj) % 320) + 1;
Как и другие процедуры фракталов, DrawAbstraet() работает с экраном
размером 640x480 точек. Если ваш экран больше, вы можете увеличить
значения npix и npiy. Кроме того, программа генерирует случайное число между
500 и 2500, определяющее число фигур во фрактале, и еще одно число между 1
и 320, задающее максимальный размер (радиус/ширину) отдельной фигуры.
Затем программа входит во внешний цикл for, считающий от 1 до
максимального числа фигур, определенного ранее и записанного в переменную
Cmax. Программа записывает случайное значение х для текущей фигуры в
массив Сх и случайное значение у в массив Су, а также случайное значение
для размера фигуры в массив Raio. Вот этот код:
for (ic=l; ic < Cmax; ic++) {
Cx[ic] = (int)(npix*abs_random());
Cy[ic] - (int) (npiy*abs__random()) ;
Raio[ic] = (int) (Rmax*abs__random()) ;
Далее оператор if выясняет, какие цветовые значения будут использоваться —
либо черно-серые из палитры с 16 цветами, либо все 256 цветов из 256-цветной
палитры. По умолчанию (как выше) установлена 256-цветная палитра.
if (iopt — 1)
Cor[ic] = (float) (16.0*abs_random() + 15.0);
elsa
Cor[ic] = (float) (256.0*abs_random());
)
После инициализации массивов значениями, по которым будет
формироваться конечное изображение, программа начинает обход всех пикселов
экрана. Внешний цикл for перебирает значения х, внутренний — значения у.
Затем программа попадает в третий цикл, который проходит по
инициализированным ранее массивам:
for (хрхх=0; xpix < (npix-1); xpix++) {
for (ypix=0; ypix < (npiy-l); ypix++) {
index - 0;
Tcor = 0;
for (ic=l; ic < Cmax; ic++) {
472
Глава 12
В третьем цикле генерируются два значения, dx и dy, исходя из текущего
значения счетчиков первого и второго циклов в комбинации со случайными
значениями из массивов Сх и Су для текущей фигуры. Затем полученные
значения сравниваются со случайным радиусом из массива Raio:
dx = xpix - Cx[ic];
dy — ypix - Cy[ic];
if ((int)(dx*dx + dy*dy) <= Raio[ic]*Raio[ic]) {
Если генерированный результат меньше, чем квадрат радиуса, программа
увеличивает переменные Тсог и index, — index на единицу, а Тсог на
соответствующее значение цвета из массива Cor:
index++;
Тсог — Тсог + Cor[ic];
}
}
Если по выходе из цикла index будет больше 0 (так будет практически
всегда), программа устанавливает значение цвета, равное общей сумме цветовых
значений (Тсог), деленной на индекс, что дает среднее значение цвета. Этим
цветом и закрашивается текущий пиксел:
if (index > 0)
color = (int)(Tcor/index);
else
color = 0;
pDC->SetPixelV(xpix,ypix,(color * 16));
}
)
}
Функция абстрактного фрактала использует интересное сочетание
предопределенных установок со случайными числами, что дает результат,
напоминающий по виду фрактал; однако это не совсем настоящий фрактал из-за той
роли, которую в нем играет стохастика при порождении образа. Случайные
числа необходимы при генерировании фракталов, но их функция должна
ограничиваться заданием исходного «семени»; они не должны быть
существенной составляющей каждого шага вычислений.
[ МНОЖЕСТВО ДЖУЛИИ
Вы видели пользу от применения генератора случайных чисел, когда мы
обсуждали фрактал Kara Torus. Однако два самых известных фрактала —
множество Джулии и множество Мандельброта — вообще не используют
случайных значений. Эти множества порождают свои фракталы исходя из известных
начальных значений. (Хотя, конечно, можно варьировать эти значения
случайным образом, исследуя из влияние на вид конечного фрактала.) Результат
работы функции фрактала Джулии показан на рис. 12.8.
Как уже упоминалось, эти множества названы по имени математика Гасто-
на Джулии. Они могут быть генерированы путем простого изменения в
процессе, описанном позже в параграфе о множестве Мандельброта. Для описания
множества Джулии нужно начать с заданного значения С, комплексного числа
Создание фракталов
473
Рис. 12.8. Множество Джулии порождает сложные фракталы
с действительной и мнимой частями. Комплексные числа являются числами в
форме а + (b * i), где i представляет квадратный корень из -1 (мнимую
единицу). Для множества Джулии начальное значение Z соответствует этому
комплексному числу. Действительная компонента соответствует координате х, а
мнимая координате у, умноженной на i (корень из -1). Чтобы нарисовать
фрактал, нужно последовательно применить уравнение Z(n+1) = Z(n)"2 + С
для каждого из значений Z из ряда (0..п).
Для каждой точки комплексной плоскости имеется свое множество Джулии,
другими словами, существует бесконечное число различных множеств Джулии.
Но наиболее интересны визуально бывают полученные из таких значений С,
для которых образ М-множества (т. е. родственного точечного множества Ман-
дельброта) наиболее плотен. Если слишком далеко зайти вглубь диапазона
значений, множество Джулии станет кругом. Если слишком далеко выйти за его
пределы, множество распадется на рассеянные по плоскости точки.
Давайте рассмотрим функцию DrawJulias(), рисующую фракталы
множеств Джулии. Как и все остальные функции фракталов, она получает в
качестве единственного параметра указатель на контекст устройства и объявляет
переменные для внутреннего использования при вычислениях. Вот начало
функции:
void DrawJulias(CDC *pDC)
{
double xmin, xmax, ymin, ymax, fact=1.0;
double ypy, x, y, xO, yO, xp, yp, const_scr=l.0;
474
Глава 12
double deltax, doItay, pmin, qmin, ya, xkpl, ykpl, r;
int npix=640, npiy=480, kcolor;
int k, np, nq, npy, ipen;
Обратите внимание, что функция объявляет больше переменных, чем
любая из рассмотренных до сих пор; смысл большинства из них станет
выясняться по ходу разбора функции. Однако некоторые из переменных должны
показаться вам знакомыми, прежде всего npix и npiy, которые задают внешние
границы области рисунка и могут быть изменены в зависимости от
характеристик вашей системы.
pmin ■ -0.74356;
qmin ■ 0.11135;
xmin ~ -2.0;
хгаах « 2.0;
ymin = -2.0;
ymax = 2.0;
kcolor = 255;
Как говорилось только что, каждой из этих переменных можно присвоить
другое значение; вы можете «поиграть» со значениями в исходном коде или
создать панель диалога, которая позволит пользователям устанавливать
значения во время выполнения (а в системах DOS или UNIX можно задавать их
как аргументы командной строки). Переменная fact, как показано ниже,
позволяет также указать, какую часть экранного пространства следует
использовать. Значение по умолчанию 1.0 соответствует всему экрану, но вы можете
присвоить ему любое значение между 0.0 и 1.0.
if(fact>=1.0 || fact <=0.0)
fact = 1.0;
else {
npix = (int)(npix*fact);
npiy = (int)(npiy*fact);
)
УРУ ж (double)npiy - 0.5;
deltax = (xmax-xmin)/(npix-1) ;
deltaу = (ymax-ymin)/(npiy-1) ;
Переменные deltax и deltay соответствуют разности максимальных и
минимальных значений х и у, поделенной на размер экрана. Теперь программа, как
и код для абстрактного фрактала, в двух циклах for — по х и по у — проходит
по всем пикселам области рисунка. Переменная пр будет соответствовать
текущему значению х, а переменная nq значению у. При каждом проходе
внешнего цикла переменной хО присваивается значение, равное минимальному
значению х плюс текущее значение счетчика, умноженное на приращение х, как
показано ниже:
for (np=0; np<=npix-l; np++) {
хО = xmin + (double)np * deltax;
for (nq=0; nq<=npiy-l; nq++) {
Во внутреннем цикле точно так же вычисляется значение уО — по
минимальному значению и текущему счетчику, умноженному на приращение по у.
Программа устанавливает в х и у эти начальные значения и инициализирует
нулем константу к:
Создание фракталов
475
уО = ymin + (double)nq * deltay;
x = xO;
У = уО;
k = 0;
После этого начинается цикл, рисующий составляющие фрактал реальные
точки. В отличие от предыдущих двух здесь применен цикл do, который
повторяется, пока значения г и к не превосходят значения kcolor,
установленного ранее равным 255 (оно может быть и меньше).
do {
xkpl = (х+у)*{х-у) + pmin;
уа = х*у;
ykpl - уа + уа + qmin;
г = xkpl*xkpl + ykpl*ykpl;
Затем программа вычисляет действительную и мнимую части комплексного
числа Z, определяемого порождающим уравнением множества Джулии. Здесь
представлением комплексного числа служат две переменных типа double.
(Комплексным типом мы пользуемся при рисовании множества Мандельброта, и
вы также можете реализовать в своей программе комплексные числа, когда
захотите.) Переменная xkbpl содержит действительную часть комплексного
числа, a ykbpl — его мнимую часть. Обе части числа возводятся в квадрат и
складываются, давая в результате значение для переменной г. Программа
увеличивает значение к и проверяет, не превысило ли значение г максимума,
заданного kcolor:
k++;
if (r >= kcolor) {
Если г больше максимального значения, говорят, что точка фрактала
стремится к бесконечности. Такие точки закрашиваются цветом, определяемым
значением к, и выводятся в позиции, заданной координатами пр и nq, как
показано ниже:
ipen = k;
хр = const_scr*(double)пр;
ур = (double)nq;
pDC->SetPixelV(xp,yp,ipen);
)
С другой стороны, множество Джулии описывает также точки с тенденцией
к притяжению — точки, стремящиеся к центру. Для таких точек значение к
всегда равно максимальному, и мы рисуем их синим. (Их можно рисовать
любым цветом, лишь бы он явно отличался от всех остальных цветов фрактала.)
if (k == kcolor) {
ipen = RGB(0, 0, 255);
хр = const_scr*(double)пр;
ур = (double)nq;
pDC->SetPixelV(xp,yp,ipen);
}
Важно отметить, что точка может показаться стремящейся к бесконечности
и в то же время иметь значение к, равное kcolor. В этом случае точка всегда
будет нарисована синим, а не каким-либо иным цветом.
476
Глава 12
х ■ xkpl;
у = ykpl;
} while (r <= kcolor fifi k<=kcolor) ;
}
}
}
Цикл рисования точки завершается, когда г либо к превзойдет значение
kcolor, что будет означать переход к следующей точке фрактала.
МНОЖЕСТВО МАНДЕЛЬБРОТА
[мн.
Множество Джулии, безусловно, очень интересно и представляет один из
наиболее известных в математике фракталов. Однако, по-видимому, самыми
знаменитыми являются фракталы, определяемые множеством Мандельброта.
Это множество, как и множество Джулии, рисует фрактал по его уравнению,
используя комплексные числа и предопределенные отправные точки. Хотя
существует огромное число вариаций множества Мандельброта (как и
множества Джулии), все они выглядят подобно рис. 12.9.
**■■*
1 "-*«•/. :■. ,
-. *•-.
Рис. 12.9. Множество Мандельброта, нарисованное программой Fractals
Множество Мандельброта — классический фрактал; во многих
графических программах реализовано одно только множество Мандельброта, и оно же
является источником большинства печатных репродукций фракталов, опубли-
Создание фракталов 477
кованных в последние годы. Несмотря на всю славу и всеобщее признание,
множество Мандельброта остается просто графиком: горизонтальная (х) и
вертикальная (у) координаты представляют области изменения двух
независимых величин. В двумерном представлении для передачи различных уровней
третьей величины, зависящей от двух первых, используют цвет — как это
делали мы при рисовании множества Джулии. На трехмерном графике цветовое
значение заменила бы z-координата.
Так же, как и во множестве Джулии, ось х опять же представляет обычные,
действительные числа, а ось у — мнимые. Итак, фрактал начинается от любой
точки комплексной плоскости — С, комплексной константы. Затем берется
другое комплексное число, которое, однако, уже может меняться — Z,
комплексная переменная. Чтобы построить фрактал, вы начинаете с Z = 0 и
вычисляете выражение фрактала следующим образом:
z(n) = plot
z(n+l) = zA2 + с
В математике говорят, что здесь производится итерация функции Z(n+1) =
Z(n)"2 + С. для некоторых значений С результат через некоторое время
«выравнивается». Для остальных он беспредельно растет.
Оказывается, что проверка того, принадлежит ли точка множеству
Мандельброта, может дать важную информацию даже в том случае, когда точка не
входит в множество. Можно получить ценные сведения, изучая
электростатический потенциал, создаваемый множеством вне той области, которую оно
занимает.
Чтобы ухватить смысл этого на языке физики, представьте себе
металлическую трубу очень большого диаметра, стоящую на одном из концов. В
середине этой трубы стоит очень тонкий, напоминающий палку предмет той же
длины, что и труба, и имеющий в разрезе форму множества Мандельброта. Если
потенциал палки — ноль, а труба находится под высоким потенциалом, то в
пространстве между палкой и трубой создается электрическое поле. Если
диаметр трубы увеличивать до бесконечности, то плоскость, пересекающая эту
систему по горизонтали-, будет представлением комплексной плоскости с
множеством Мандельброта в центре. Бесконечная область с электрическим полем
является дополнением множества Мандельброта.
В этой области плоскости, дополнительной к множеству, можно построить
эквипотенциальные линии, т. е. линии, соединяющие точки с равным
потенциалом. Эти линии на больших расстояниях от центра будут почти
идеальными окружностями, а по мере приближения к области множества Мандельброта
будут все более искажаться и искривляться. Эквипотенциальные, а также
силовые линии поля, пересекающиеся с первыми под прямым углом, могут
многое сказать о форме и других характеристиках множества Мандельброта.
Замечательным математическим свойством такой системы является то, что
потенциал любой точки в области, дополнительной к множеству
Мандельброта, связано простой функцией с ее временем расходимости. Время
расходимости определяется как число итераций, после которого значение функции
Мандельброта выходит за пределы (сколь угодно) большого круга с центром в
исходной точке множества. Так как все множество ограничено окружностью с
радиусом 2, можно использовать круг с любым радиусом, большим или
равным этому значению. Однако чем больше радиус, тем точнее будет оценка
дополнительного множества.
478
Глава 12
Говоря попросту, потенциал точки из дополнения к множеству Мандельб-
рота измеряется тем, насколько быстро значение устремляется к
бесконечности. В программном примере мы измеряем потенциал точек множества
исключительно в качестве средства определения цвета каждой отображаемой точки.
На рисунке, таким образом, цвета примерно соответствуют цвету точек
множества, определяемому по времени расходимости. Для измерения потенциала
точки, заданной параметрами сх и су, программа вызывает функцию Mandel-
SetPoten():
double MandelSetPoten(double ex, double cy, Int maxiter)
<
double x, y, x2, y2, temp, potential;
int iter;
x = ex;
x2 = x*x;
у = су;
У2 = y*y;
iter =0;
При входе в функцию программа устанавливает значения локальных
переменных х и у равными значениям переданных х- и у-координат. Затем
вычисляются их квадраты (представленные переменными х2 и у2) и
инициализируется нулем переменная iter. Далее цикл do будет следить, не превысил ли
счетчик iter максимального значения, заданного параметром maxiter.
В цикле do происходит собственно измерение потенциала. Для этого по
текущим значениям х и у, и по входным значениям сх и су вычисляются новые
значения. Эти значения х и у снова возводятся в квадрат. Довольно быстро мы
уже сможем сказать, стремится ли точка к бесконечности или же к конечному
пределу. При любом исходе программа завершает цикл и выполняет
соответствующий оператор if. Вот цикл do:
do {
temp = х2 - у2 + сх;
у = 2.0*х*у + су;
х = temp;
х2 = х*х;
у2 « у*у;
iter++;
} while ((iter < maxiter) && ((x2 + y2) < 10000.0));
Оператор if определяет, почему произошел выход из цикла — из-за
превышения максимального числа итераций или же из-за того, что сумма квадратов
х и у превысила 10000 (т. е. стремится к бесконечности). В последнем случае
(iter < maxiter == TRUE) потенциал определяется как натуральный логарифм
суммы квадратов х и у, деленный на 2 в степени числа итераций:
if (iter < maxiter)
potential - 0.5 * log(x2 + y2) / powl(2.0,iter);
Если же точка стремится к конечному пределу, ее потенциал полагается
равным нулю и переменной potential присваивается 0. В любом случае
происходит выход из функции с возвратом значения potential:
Создание фракталов 479
else
potential — 0.0;
return (potential);
)
Единственной задачей функции DrawMandelbrot() является вычисление
текущих значений х и у, а также присвоение пикселу цветового значения,
соответствующего возвращаемому MandelSetPoten() потенциалу. При входе в
функцию определяется ряд значений, используемых впоследствии при расчете
текущих значений х и у. DrawMandelbrot() объявляет также переменную с
шаблонного типа complex, определенного в библиотеке стандартных
шаблонов. Обратите внимание, что аргументом шаблона является тип double.
Флаг maxiter задает максимальное число итераций, производимых
программой при проверке потенциала точек в функции MandelSetPotent().
Программа по умолчанию обрывает итерации после 16000, но вы можете изменять
это значение. Большие значения позволяют точнее определить цвет, а при
меньших раскраска фрактала становится менее точной. Значение iset
указывает, как должны отображаться потенциалы; при нулевом значении
потенциалы будут отображаться одним и тем же цветом. При ненулевом значении iset
различным потенциалам будут соответствовать различные цвета. Вот начало
функции DrawMandelbrot():
void DrawMandelbrot(CDC *pDC)
{
int nx, ny, iy, ix, ipen, maxiter = 16000, iflag=0, iset — 1;
complex<double> c;
double xmin=-2.25, ymin=-1.25, xmax=0.75, ymax=1.25;
cx, cy, potent;
double diff=0.6482801, testl, test2;
Следующий оператор if проверяет, находится ли значение maxiter в
безопасных пределах. Вы, конечно, можете изменить значения в этом операторе
или предоставить их выбор пользователю.
if ((maxiter>=16000) || (maxiter<=0))
maxiter - 16000;
Следующие операторы устанавливают пределы для некоторых значений,
соответствующие характеристикам вашей системы. Все эти значения,
управляющие разрешением и масштабом изображения, можно настраивать:
пх - 640;
пу = 480;
ymin = -1.125;
ушах ~ 1.125;
Далее программа входит во внешний из двух циклов, внутри которых
происходит построение фрактала; внешний цикл соответствует у-координате
области рисунка, а внутренний — х-координате. На каждом проходе цикла
программа определяет значение су в зависимости от заданных пределов
изменения у и текущего значения счетчика, — т. е. строки развертки экрана, на
которой программа находится. Затем начинается внутренний цикл, который
перебирает значения х-координаты, пока не дойдет до края экрана, и
раскрашивает каждый пиксел. Вот код заголовков обоих циклов:
480
Глава 12
for (iy=0;iy<=ny-l;iy++) {
су = ymin + iy*(ymax-ymin)/(ny-1);
for (ix=0;ix<=nx-l;ix++) {
Далее программа определяет значение сх. Как и су, оно является функцией
максимального и минимального значений х и текущего значения счетчика.
Затем значения сх (соответствующее текущей х-координате) и су
(соответствующее у-координате) присваиваются действительной и мнимой частям
комплексной переменной с:
сх = xmin + ix*(xmax-xmin)/(nx-l);
c.real(ex);
c.imag(cy);
Максимальное значение testl и test2, используемых при определении
цвета отдельного пиксела, равно 1. Программа устанавливает для переменных
значения по умолчанию вне этого диапазона и проверяет значения сх и су.
Если они попадают в пределы некоторого диапазона, код меняет значения
тестовых переменных. В первом случае результат для testl равен абсолютному
значению 1 минус корень из 1 минус комплексное число с, умноженное на 4:
test! = 2.0;
if ((сх >= -7.55е-1) ££ (сх <= 4.0е-1))
if ((су >= -6.6е-1) && (су <= 6.6е-1))
testl = aba(1.0 - sqrt(1.0-4.0*с));
}
Похожая проверка, но для другого диапазона координат, производится для
test2, и результат в этом случае равен абсолютному значению переменной с
плюс 1, умноженной на 4:
test2 ■= 2.0;
if ((сх >= -1.275е0) SS (сх <= -7.45е-1)) {
if ((су >= -2.55е-1) £& (су <= 2.55е-1))
test2 = abs(4.0*(c+1.0));
}
Теперь, имея в testl и test2 некоторые значения, программа переходит к
выяснению того, каким цветом нужно нарисовать пиксел:
if (testl <= 1.0) {
potent = 0;
iflag = 1;
if (iset != 0)
ipen - 126;
else
ipen — 32;
>
В данной проверке, если сх и су попадают в заданный диапазон, программа
устанавливает цвет пера равным 32 или 126 в зависимости от того, должны ли
потенциалы отображаться в различных цветах. Если же условие данного if
ложно, то:
else if (test2 <= 1.0) {
potent = 0;
iflag =1;
if (iset '= 0)
Создание фракталов 481
ipen = 104;
else
ipen - 32;
)
Вторая проверка устанавливает цвет пера равным 32 или104, также в
зависимости от заданной опции раскраски. Как и первой проверке, установка
цвета происходит только в случае, если значения сх и су попадают в заданный
диапазон. В противном случае происходит переход к последнему else, где
вызывается функция MandelSetPoten() для определения потенциала точки:
else {
potent — MandelSetPoten(cx/cy,maxiter);
iflag = 0;
)
Наконец, производится последняя проверка. Если переменная iflag равна 0 —
это означает, что вызывалась функция MandelSetPoten(), — проверяется
переменная potent. Если она также 0, программа рисует цветом 32 (черным); в
протином случае программа генерирует цветовое значение пиксела в
зависимости от полученного потенциала. (Это цветовое значение лежит где-то между
0 и 32; вы можете его изменить, адаптировав к числу отображаемых цветов и
установкам палитры.) Вот код окончательной проверки:
if ((potent — 0.0) && (iflag==0))
ipen = 32;
else if ((potent !=0) && (iflag=0))
ipen = (int)(33.0 + 15.0*(potent-33.0)/diff);
После выяснения того, каким цветом должен быть нарисован пиксел,
программа вызывает функцию SetPixelVQ для действительного вывода его на
экран. Затем циклы повторяются, рисуя по очереди каждый пиксел экрана:
pDC->SetPixelV(ix,iy,ipen);
}
}
}
Как и во всех функциях фракталов, вы можете менять используемые
функцией значения, чтобы посмотреть на получающийся результат. Как и в случае
множества Джулии, существует бесконечное число возможных множеств Ман-
дельброта. Однако множества Мандельброта стремятся вернуться к одной и
той же форме (показанной на рис 12.9). Это не так в случае множества
Джулии, которое демонстрирует гораздо меньшее единообразие.
Все обсуждавшиеся функции фракталов вызываются из метода OnDraw()
класса CFractalsView. Этот метод автоматически активируется окном
обрамления всякий раз, когда в последнем происходят какие-то изменения
(например, изменение размера). OnDraw() вызывается также в случае, если документ
вызывает Update AUViews(). В программе Fractals мы просто проверяем
значение, возвращаемое GetCurrentFractal(), и определяем, была ли вызвана
функция OnDraw() из-за того, что пользователь сделал выбор. Если это так,
оператор switch обрабатывает запрос; если нет, OnDraw() просто очищает экран. Вот
эта функция:
void CFractalsView::OnDraw(CDC* pDC)
{
CFractalsDoc* pDoc = GetDocument();
16 Зак. 1208
482
Глава 12
ASSERT_VALID(pDoc) ;
pDC = GetDC() ;
switch (pDoc->GetCurrentFractal ()) {
case 0:
break;
При каждом вызове функции проверка меток case приводит к вызову той
или иной процедуры фрактала, а затем значению текущего фрактала
присваивается 0, так что при следующем вызове функции программа не будет
пытаться перерисовать уже запрошенный фрактал.
case 1:
DrawKamTorus(pDC);
pDoc->SetCurrentFractal(0);
break;
case 2:
DrawDuff(pDC);
pDoc->SetCurrentFractal(0);
break;
case 3:
DrawAbstract(pDC);
pDoc->SetCurrentFractal(0);
break;
case 4:
DrawJulias(pDC);
pDoc->SetCurrentFractal(0);
break;
case 5:
DrawMandelbrot(pDC);
pDoc->SetCurrentFractal(0);
break;
}
}
Как уже отмечалось, вы могли бы использовать контекст устройства в
памяти, а не текущий контекст устройства, чтобы нарисованный образ
сохранялся в памяти и мог бы служить для перерисовки экрана. Чтобы реализовать
такой вариант программы, вам нужно ввести в конструктор класса CFractals-
View вызов CDC::CreateCompatibleDC(). Переменная, в которой вы сохраните
полученный контекст, должна находиться в области действия при всех
вызовах функций класса и может, таким образом, использоваться OnDraw() и
функциями фракталов для восстановления изображения при всяком
обновлении дисплея.
Примеры этой главы могут служить отличным введением в мир фракталов,
но ими, конечно, ни в коей мере не исчерпывается разнообразие возможных
применений фракталов в ваших программах. Вы можете модифицировать
показанные здесь процедуры. Вы можете также обратиться к математическим
текстам (по линейной алгебре или специально по фракталам) и изучить другие
интересные функции, которыми можно воспользоваться для реализации
фрактальных образов. Важно не забывать, что почти все фрактальные
функции для порождения х- и у-координат используют комплексные числа, а
также определяют цвет для индивидуальных пикселов экрана, — этот цвет, как
правило, служит в функциях рисования фракталов для представления
значений или шкалы z-координат.
ГЛАВА
*а
г, ш—ъ
™
ч- *
*« ' ;
i * • х
} .
ГбШЫяппш
^№Ш|РШГ1
• IП U.
Ь . ■ ». ■ . ■ • ■ - » »
? . . * 1 * - - . -* ,« * :i
Херберт Шильдт
41. '
,1 ,1-4
gjasrer.h
parserl.h
parser2.h
4$
* .& -
?'' ' ' L^-
484
Глава 13
В этой главе будет разработан объектно-ориентированный анализатор
выражений, позволяющий производить оценку алгебраических
выражений вроде (10 - 8)*3. Анализаторы выражений весьма полезны и находят себе
применение в самых разнообразных областях. Но в то же время они очень
часто выпадают из поля зрения программистов. По ряду причин процедуры
создания синтаксических анализаторов мало где изучаются или описываются. На
самом деле даже для опытных, во всех остальных отношениях, программистов
процесс синтаксического разбора выражений часто остается загадкой.
В действительности процесс анализа выражений довольно очевиден и
оказывается много проще некоторых других программных задач. Это так, поскольку
задача четко определена и действия производятся по строгим правилам
алгебры. В этой главе разрабатываются три варианта класса с именем parser,
реализующих то, что обычно называют анализом по методу рекурсивного спуска.
Первые два варианта не являются общеприменимыми. Последний вариант
опирается на шаблоны и может применяться к любому численному типу.
Основы синтаксического анализа
Перед тем, как обсуждать разработку анализатора, необходим краткий
обзор выражений, синтаксического разбора и разбиения на лексемы.
Выражения
Поскольку анализатор выражений оценивает алгебраическое выражение,
важно понимать, из каких составных частей оно образуется. Хотя в
выражения могут входить любые типы информации, в этой главе мы занимаемся
только численными выражениями. С учетом стоящей перед нами задачи
можно считать, что численные выражения составляются из следующих элементов:
♦ Чисел
♦ Операций +, -, /, Л, %, =
♦ Круглых скобок
♦ Переменных
Знак операции Л означает здесь возведение в степень, как в BASIC, a =
является операцией присваивания, как в C++. Различные элементы могут
комбинироваться в соответствии с правилами алгебры. Вот несколько примеров:
10-8
(100 - 5) * 14 / 6
а + b - с
10 Л 5
а = 10 - b
Объектно-ориентированный анализатор выражений 485
Предполагается следующее старшинство операций:
наивысшее
наинизшее
+ - (одноместные)
-
* / %
+ -
=
Операции равного приоритета оцениваются слева направо.
В наших примерах все переменные являются одиночными буквами
(другими словами, всего имеется 26 доступных переменных от А до Z). Регистр не
различается (а и А рассматриваются как одна переменная). В первых
вариантах анализатора все численные значения представляются типом double, хотя
вы можете легко написать процедуры для обработки других типов значений.
Наконец, чтобы сохранить простоту и ясность логики, примеры включают
только минимальные средства обнаружения ошибок.
Анализ выражений: проблема
Если вы никогда особенно не задумывались над задачей синтаксического
разбора выражений, то можете предположить, что для этого не требуется
значительных усилий. Однако, чтобы лучше понять суть проблемы, попробуйте
оценить вот это простое выражение:
10 - 2 * 3
Вы знаете, что оно равно четырем. Хотя можно с легкостью написать
программу, оценивающую именно это выражение, вопрос состоит в том, как
создать программу, которая дает правильный ответ для любого произвольного
выражения. Поначалу вы можете придумать нечто вроде следующего алгоритма:
а = взять первый операнд
while (имеются операнды) {
ор =*= взять операцию
b = взять второй операнд
а = a op b
}
Эта процедура берет первый операнд, операцию и второй операнд, а затем
производит первую операцию. Затем она берет следующую операцию и
операнд, производит следующую операцию и т. д. Однако если вы попытаетесь
применить этот незамысловатый подход, выражение 10 - 2 * 3 вместо 4
получит оценку 24 (т. е. 8 * 3), так как данная процедура пренебрегает
старшинством операций. Нельзя просто брать операнды и операции по порядку слева
направо, потому что правила алгебры требуют, чтобы умножение выполнялось
перед сложением et cetera. Более того, проблема еще усложняется, когда вы
вводите скобки, возведение в степень, переменные, одноместные операции и
прочее.
Хотя имеется несколько способов реализовать набор процедур,
оценивающих выражения, разработанный здесь анализатор с рекурсивным спуском яв-
486
Глава 13
ляется самым простым из тех, что можно написать. Он же является наиболее
популярным. В процессе чтения главы вы поймете, почему он так называется.
(Некоторые другие методы написания анализаторов используют сложные
таблицы, которые должны генерироваться другими программами. Их иногда
называют анализаторами с управляющими таблицами.)
Синтаксический анализ выражения
Есть много способов разобрать и оценить выражение. В свете разработки
рекурсивного анализатора, представьте себе выражения как рекурсивные
структуры данных — другими словами, как выражения, определяемые в своих
собственных понятиях. Если на минуту предположить, что в выражения могут
входить только операции +, -, *, / и скобки, то все они могут быть определены
заданием следующих правил:
выражение -> член [4- член] [- член]
член -> сомножитель [* сомножитель] [/ сомножитель]
сомножитель -> переменная, число или (выражение)
Квадратные скобки обозначают необязательный элемент, а -> означает
порождение. На самом деле эти правила обычно называют правилами
порождения выражения. Таким образом, для определения члена можно было бы
сказать: «Член порождает сомножитель, возможно, умноженный на
сомножитель либо поделенный на сомножитель». Заметьте, что такой способ
определения выражений уже подразумевает старшинство операций.
8 выражение
10 + 5 *В
входят два члена: 10 и 5 * В. Во второй член входят два сомножителя: 5 и В.
Эти сомножители являются числом и переменной.
С другой стороны, выражение
14 * (7 - С)
имеет два сомножителя: 14 и (7 - С). Сомножители являются числом и
выражением, помещенным в скобки. Выражение в скобках содержит два члена:
число и переменную.
Этот процесс образует основу синтаксического анализатора с рекурсивным
спуском, который представляет собой набор взаимно-рекурсивных функций,
сцепленных подобно звеньям цепи и реализующих правила порождения. На
каждом шаге анализатор производит указанные операции в алгебраически
корректной последовательности. Чтобы понять, как правила порождения
применяются при разборе выражений, давайте проработаем пример со следующим
выражением:
9 / 3 - (100 + 56)
Вот правильная последовательность действий:
1. Взять первый член, 9/3.
2. Взять каждый из сомножителей и выполнить деление.
3. Взять второй член, (100 + 56). Поскольку он начинается с открывающей
скобки, начать рекурсивный анализ подвыражения.
Объектно-ориентированный анализатор выражений 487
4. Взять каждый из членов и выполнить сложение.
5. Вернуться из рекурсивной процедуры оценки и вычесть 156 из 3. Ответ
равен -153.
Если в настоящий момент вы в некотором замешательстве, не
расстраиваетесь. Это довольно сложная концепция, и к ней нужно привыкнуть. При
таком рекурсивном взгляде на выражения следует помнить о двух основных
моментах. Во-первых, старшинство операций подразумевается в самом способе
определения порождающих правил. Во-вторых, такой метод разбора и оценки
выражений очень похож на то, как математические выражения оцениваются
людьми.
Разбиение выражения на лексемы
Чаще всего, как это имеет место в примерах настоящей главы, оцениваемое
выражение представлено символьной строкой. Чтобы оценить такое
выражение, нужно уметь раскладыывать строку на ее компоненты. Эта операция
является в синтаксическом анализе фундаментальной, но не является частью
синтаксического анализа в собственном смысле. Поскольку для решения этой
задачи все три анализатора из этой главы используют одну и ту же функцию,
мы исследуем ее прямо сейчас.
Каждый компонент выражения называется лексемой. Например, выражение
А * В - (W + 10)
содержит лексемы А, *, В, -, (, W, 4-, 10 и ). Каждая лексема представляет
неделимый элемент выражения. Как правило, вам требуется функция, которая
последовательно возвращает каждую лексему выражения по отдельности.
Функция разбиения должна также пропускать пробелы и табуляции и
обнаруживать конец выражения. Функция, которой мы будем пользоваться,
называется get_token() и является элементом всех версий класса parser.
Помимо самой лексемы требуется знать, к какому типу она относится.
В разрабатываемых здесь анализаторах вам необходимы всего три типа:
VARIABLE, NUMBER и OPERATOR (последний используется и для операций, и
для скобок). Эти значения определяются перечислением typesT, входящим в
класс parser.
Функция get_token() показана в следующем листинге. Она извлекает
следующую лексему выражения, на которое указывает exp_ptr, и записывает ее в
переменную token. Тип лексемы помещается в поле tok_type. Переменные
exp_ptr, token и tok_type являются элементами класса parser. В листинге
показана также вспомогательная функция isdelim().
// Извлечь следующую лексему.
void parser::get_token()
{
register char *temp;
tok_type = UNDEFTOK;
temp = token;
*temp - *\0';
if(!*expjptr) return;
// конец выражения
488
Глава 13
while(isspace(*exp_ptr)) ++exp_ptr; // пропустить пробелы
if{strchr("+-*/%A=()", *exp_ptr)){
tok_type = OPERATOR;
// перейти к следующему символу
*temp++ = *exp_j>tr++;
}
else if (isalpha(*exp_j>tr)) {
while(!isdelim(*exp_j>tr)) *temp++
tok_type = VARIABLE;
}
else if (isdigit(*exp__ptr)) {
while (! isdelim(*exp_ptr)) *temp++
tok_type = NUMBER;
}
*temp ss • \0' ;
}
// Возвратить true, если с -- ограничитель.
bool parser::isdelim(char c)
I
if(strchr(" +-/*%*=()", c) ]| c==9 || c=='\r' || c==0)
return true;
return false;
>
Посмотрите внимательно на эти функции. После начальных
инициализаций get_token() проверяет, не найден ли ограничивающий выражение нуль.
Это сводится к проверке символа, на который указывает exp_ptr. Поскольку
exp_ptr является указателем на анализируемое выражение, то в случае, если
он указывает на нуль-символ, конец выражения достигнут. Если же в строке
еже имеются лексемы, get_token() первым делом пропускает все начальные
пробелы. После этого exp_ptr может указывать на число, переменную,
операцию или — если пропущенные пробелы стояли в конце строки — на нуль.
Если следующий символ — операция, он возвращается как строка в token, а в
tok_type помещается OPERATOR. Если же следующий символ — буква,
предполагается, что это одна из переменных. Буква возвращается в виде строки в token,
а в tok_type записывается VARIABLE. Если следующий символ — цифра,
читается все число, которое в строковой форме помещается в token; в tok_type за*
писывается NUMBER. Наконец, если следующий символ не является ни одним
их предыдущих, предполагается, что достигнут конец выражения. В этом
случае token содержит пустую строку, что и служит признаком конца выражения.
Функция isdelim() — вспомогательная; она упрощает определение того, что
символ является разделителем, т. е. символом, отделяющим одну лексему от
другой.
Как уже говорилось, в интересах ясности кода в этой функции опущены
некоторые проверки на ошибку, вместо которых делаются априорные
предположения. Например, нераспознанный символ может оканчивать выражение.
Переменные могут быть любой длины, но значимой будет только первая буква.
Вы можете ввести дополнительные проверки и другие детали, какие потребует
ваше конкретное приложение.
*exp_ptr++;
*exp_ptr++;
Объектно-ориентированный анализатор выражений 489
Чтобы лучше понять процесс разбиения на лексемы, посмотрите, что get_to-
ken() возвращает для каждой лексемы (и ее типа) следующего выражения:
А + 100 - (В * С) / 2
Лексема
А
+
100
-
(
В
*
с
)
/
2
нуль
Тип лексемы
VARIABLE
OPERATOR
NUMBER
OPERATOR
OPERATOR
VARIABLE
OPERATOR
VARIABLE
OPERATOR
OPERATOR
NUMBER
нуль
Помните, что в token всегда содержится ограниченная нулем строка, даже
если она состоит всего из одного символа.
В оставшейся части этой главы разрабатываются три синтаксических
анализатора. Первый реализует минимальный анализ и работает только с
константными значениями. Во втором вводится поддержка переменных. Третий
анализатор реализован как шаблон класса и может применяться для
синтаксического разбора выражений любого численного типа.
Простой анализатор выражений
В этом разделе разрабатывается простой синтаксический анализатор,
который может оценивать выражения, состоящие исключительно из констант,
операций и скобок. Он не обрабатывает выражения, в которые входят
переменные. Более того, предполагается, что все константы имеют тип double.
Хотя и простой, этот анализатор ясно демонстрирует ключевые моменты
метода рекурсивного спуска; и он вполне работоспособен в пределах своих
ограниченных возможностей.
Код
Ниже приводится код простого синтаксического анализатора из файла
parserl.h.
490
Глава 13
f* Данный модуль содержит анализатор выражений
с рекурсивным спуском, не использующий переменных.
Файл называется parserl.h
*/
^include <iostream>
tfinclude <cctype>
ftinclude <cstring>
using namespace std;
class parser {
enum typesT { UNDEFTOK, OPERATOR, NUMBER);
enum errorsT { SERROR, PARENS, NOEXP, DIVZERO };
char *exp_ptr; // указывает на выражение
char token[80]; // содержит текущую лексему
typesT tok_type; // содержит тип лексемы
void eval_exp2(double Sresult);
void eval_exp3(double Sresult);
void eval_exp4(double Sresult);
void eval_exp5(double Sresult);
void eval_exp6(double sresult);
void atom(double Sresult);
void get_token();
void serror(errorsT error);
bool isdelim(char c) ;
public:
parser();
double eval_exp(char *exp);
>;
// Конструктор
parser::parser ()
{
exp_ptr - 0;
}
// Входная точка анализатора,
double parser::eval_exp(char *exp)
i
double result;
exp_ptr = exp;
get_token() ;
if(!*token) {
serror(NOEXP); // выражение отсутствует
return 0.0;
}
eval_exp2(result);
if(*token) serror(SERROR) ; // последняя лексема должна быть нулевой
return result;
I)
Объектно-ориентированный анализатор выражений 491
• 'if/ Сложить или вычесть два члена.
* ' 'void parser::eval_exp2(double firesuit)
ч register char op;
■ 4 double temp;
i
■т eval_exp3(result);
while((op = *token) == * + ' || op — '-') {
get_token{);
eval_exp3(temp);
switch(op) {
case '-':
result = result - temp;
break;
i *»
\
ft
'j case ' + ' :
± { result - result + temp;
break;
*
J I// Перемножить или поделить два сомножителя.
■ ?void parser::eval_exp3(double £result)
{
register char op;
double temp;
eval_exp4(result);
while((op = *token) «■ '*' || op = •/' II op == '%') {
get_token();
eval_exp4(temp);
switch(op) {
case '*':
result = result * temp;
break;
case •/•:
\ltcl if (! temp) serror (DIVZERO) ; // попытка деления на ноль
else result = result / temp;
break;
case '%':
result = (int) result % (int) temp;
break;
}
}
i >
! 4
; J
i !
» j
* t
:u
• i
\ \lI Обработка степени.
\ jvoid parser::eval_exp4(double firesult)
* ,{
double temp, ex;
ч register int t;
is
' 4
■ \ eval_expS(result);
Uif(*token== ,A') {
get token();
eval_exp4(temp);
ex = result;
if (temp=0.0) {
result - 1.0;
return;
}
for(t=(int)temp-1; t>0; —t) result = result * (double)ex;
}
}
// Оценить унарный + или -.
,void parser::eval_exp5(double Sresult)
{
л register char op;
op = 0;
if((tok_type == OPERATOR) && *token=='+' || *token == '-•) (
op = *token;
get_token() ;
}
eval_exp6(result);
if(op=='~') result = -result;
}
// Обработка выражения в скобках,
void parser::eval_exp6(double firesult)
{
if((*token == ' (')) {
get_token();
eval_exp2(result);
if(*token != ')')
serror(PARENS);
get_token();
}
else atom(result);
}
// Получить значение числа.
void parser::atom(double firesult)
<
switch(tok_type) {
case NUMBER:
result = atof(token);
get_token();
return;
default:
serror(SERROR) ;
}
'}
// Сообщить о синтаксической ошибке.
{
,void parser::serror(errorsT error)
static char *e[]= (
"Syntax Error",
Объектно-ориентированный анализатор выражений
493
*
"Unbalanced Parentheses",
"No expression Present",
"Division by zero"
};
cout « e[error] « endl;
{// Извлечь следующую лексему,
void parser::get_token()
{
register char *terap;
j tok_type = UNDEFTOK;
i temp = token;
1 * *temp - •\0•;
• i
■ j if (! *exp__ptr) return; // конец выражения
*• i while(isspace(*expj>tr)) ++exp_ptr; // пропустить пробелы
I
, if (strchr("+-*/%A=0", *exp_ptr)){
tok__type = OPERATOR;
// перейти к следующему символу
* ' *temp++ — *exp_ptr++;
"/* в данном анализаторе не используется
\ else if(isalpha(*exp_ptr)) {
* ■ while(!isdelim(*exp_ptr)) *temp++ ~ *exp_ptr++;
'*'', tok type = VARIABLE;
• > "
U*/
J, "j else if (isdigit (*exp_ptr)) {
' j while(!isdelim(*exp_ptr)) *temp++ = *exp_ptr++;
, \ tok_type = NUMBER; -
. ! >
, i *temp = *\0';
'' i
*' // Возвратить true, если с -- ограничитель.
I jbool parser:;isdelim(char c)
. '.<
; if(strchr(" +-/*%A=()'\ c) || c==9 || c~'\r' || c==0)
| return true;
j , return false;
}
^шш^Л.
Вот программа, тестирующая анализатор:
#include <iostream>
#include "parserl.h"
using namespace std;
int main()
494
Глава 73
i
char expstr[80];
cout « "Enter a period to stop.\n";
parser ob; // создать объект анализатора
for<;;> {
cout « "Enter expression: ";
cin.getline(expstr, 79);
if<*expstr=='.') break;
cout « "Answer is: " « ob.eval_exp(expstr) « "\n\n";
}
return 0;
}
А вот ее примерный прогон:
Enter a period to stop.
Enter expression: 10-2*3
Answer is: 4
Enter expression: (10-2)*3
Answer is: 24
Enter expression: 10/3
Answer is: 3.33333
Enter expression: .
ПРИМЕЧАНИЯ
| ПРк
Первый синтаксический анализатор может обрабатывать следующие
операции: +, -, *, / и %. Кроме того, он воспринимает возведение в целую степень (*)
и унарный минус. Он правильно обрабатывает скобки. Давайте поближе
посмотрим на его работу.
Все три наших анализатора выражений строятся на основе класса parser.
Первый анализатор использует версию, показанную здесь. Последующие
версии являются расширениями данной.
class parser {
enum typesT { UNDEFTOK, OPERATOR, NUMBER);
enum errorsT { SERROR, PARENS, NOEXP, DIVZERO };
char *exp_ptr; // указывает на выражение
char token[80]; // содержит текущую лексему
typesT tok_type; // содержит тип лексемы
void eval_exp2(double firesult);
void eval_exp3(double firesult);
void eval_exp4(double firesult);
void eval_exp5(double firesult);
void eval ехрб(double firesult);
Объектно-ориентированный анализатор выражений 495
void atom (double Sresult) ;
void get_token{);
void serror(errorsT error);
bool isdelimfchar c);
public:
parser();
double eval__exp(char *exp) ;
};
Класс parser начинается с определения двух перечислений. Первое — typesT —
определяет константы, представляющие типы лексем, возвращаемых get_to-
ken(). Так как данная версия анализатора не воспринимает переменные, typesT
не определяет упомянутый ранее тип VARIABLE; в последующих версиях эта
константа будет определена. Перечисление errorsT определяет константы для
ошибок, о которых может сообщать анализатор.
Класс содержит три закрытых элемента данных. Оцениваемое выражение
содержится в ограниченной нулем строке, на которую указывает exp_j>tr.
Таким образом, анализатор оценивает выражения, представленные
стандартными строками ASCII. Например, следующие строки содержат выражения,
которые он может оценить:
"10 - 5"
"2 * 3.3/(3.1416 *3.3)
Когда анализатор начинает выполняться, exp_ptr должен указывать на
начало выражения. По ходу выполнения анализатор продвигается по строке,
пока не встретится ограничивающий нуль.
Элементы token и tok_type хранят текущую лексему и ее тип. Эти значения
присваиваются им при вызове функции get_token(). Как уже говорилось, token
содержит текущую лексему в виде строки, a tok_type содержит значение
перечислимого типа typesT.
Входной точкой анализатора является eval_exp(), которая должна
вызываться с указателем на анализируемое выражение. Действительная оценка
выражения происходит во взаимно рекурсивных функциях от eval_exp2() до
eval_exp6() и функции atom(), возвращающей значение числа. Эти функции
образуют синтаксический анализатор с рекурсивным спуском. Они реализуют
расширенный набор порождающих правил, обсуждавшихся ранее.
Комментарии в начале каждой функции объясняют ее роль в синтаксическом разборе
выражения. В последующих версиях анализатора к ним прибавится функция
с именем eval_expl.
Функция serror() сообщает о синтаксических ошибках; она принимает
аргументы типа errorsT. Функции get_token() и isdelim() осуществляют
разбиение выражения на его составные части и обсуждались выше. Заметьте,
однако, что в функции get_token() закомментирован код, читающий имена
переменных. Поскольку анализатор не обрабатывает переменные, для get_token()
нет причины их воспринимать. Попытка использования переменной приведет
просто к синтаксической ошибке. Способность обрабатывать переменные будет
введена в следующем разделе.
Чтобы в точности понять, как синтаксический анализатор производит
оценку, проработайте следующее выражение. (Предполагается, что exp_ptr
указывает на его начало.)
10 - 3 * 2
496
Глава 13
Когда вызывается входная точка анализатора, функция eval_exp(), она
получает первую лексему. Если лексема нулевая, функция выводит сообщение
No Expression Present и возвращает управление. Однако в нашем примере
лексема содержит число 10. Так как первая лексема — не нуль, вызывается
eval_exp2(). В результате eval_exp2() вызывает eval_exp3(), a eval_exp3()
вызывает eval_exp4(), которая, в свою очередь, вызывает eval_exp5(). Последняя
проверяет, не является ли лексема унарным плюсом или минусом — в нашем
случае не является, так что вызывается eval_exp6(). В этой точке eval_exp6()
либо рекурсивно вызывает eval_exp2() (в случае выражения в скобках), либо
вызывает atom(), чтобы найти значение числа. Так как в нашем случае
лексема не является левой скобкой, исполняется atom() и параметру result
присваивается значение 10. Затем извлекается следующая лексема, и начинается
цепочка возвратов. Так как теперь лексема является операцией -, возвраты из
функций оканчиваются в eval_exp2().
То, что происходит дальше, очень важно. Поскольку лексема является
знаком ~, она сохраняется в ор. Затем анализатор получает следующую лексему,
т. е. 3, и снова начинается спуск по цепочке вызовов. Как и прежде,
исполняется atom(); в result возвращается 3 и читается лексема *. Это приводит к
возврату по цепочке до eval_exp3(), где читается завершающая лексема 2. В этой
точке производится первое арифметическое действие — перемножение 2 и 3.
Результат возвращается eval_exp2() и выполняется вычитание. Вычитание
дает окончательный ответ — 4. Хотя поначалу описанный процесс может
показаться сложным, проработайте какие-нибудь другие примеры, чтобы
убедиться, что метод в каждом случае работает правильно.
Теперь давайте посмотрим, как анализатор контролирует корректность
синтаксиса. При разборе выражений синтаксическая ошибка является просто
ситуацией, когда входное выражение не согласуется со строгими правилами,
заложенными в анализаторе. По большей части такие ситуации возникают
из-за ошибок человека — обычно опечаток. Например, следующие выражения
не являются корректными с точки зрения анализаторов из этой главы:
10 ** 8
(10 - 5) * 9)
/8
Первое выражение содержит два оператора подряд, во втором имеется
непарная скобка, а третье начинается со знака деления.
Как уже упоминалось, синтаксическая проверка выполняется функцией
serror(), вызываемой при обнаружении ошибки. Метод рекурсивного спуска
делает проверку синтаксиса несложной, поскольку каждая функция точно
знает, какую лексему она должна получить в каждой конкретной точке.
Проверка синтаксиса реализована так, что ошибка не приводит к остановке всего
анализатора. Он просто сообщает об ошибке и продолжает разбор. Это может
привести к множественным сообщениям об ошибке. Конечно, можно изменить
такое поведение анализатора; некоторые предложения по этому вопросу
приведены в конце главы.
Данный анализатор вполне годится для применения в простом калькуляторе,
что демонстрирует предыдущая программа-тестер. Однако для того, чтобы его
можно было использовать в более сложных приложениях, анализатор должен
уметь обрабатывать переменные. Этому предмету посвящен следующий раздел.
Объектно-ориентированный анализатор выражений 497
Анализатор, воспринимающий переменные
Все языки программирования, научные калькуляторы и электронные
таблицы используют переменные, сохраняя в них данные для последующей
обработки. Перед тем, как наш синтаксический анализатор можно будет
применять в такого рода приложениях, его нужно расширить, введя в него
поддержку переменных. Для этого к предыдущему варианту анализатора нужно
кое-что добавить. Прежде всего, конечно, сами переменные. Потребуется
также функция, получающая значение переменной, и функция обработки
операции присваивания.
Код
В следующем листинге показан синтаксический анализатор parser2.h,
воспринимающий переменные.
[/* Данный модуль содержит анализатор с рекурсивным
спуском, воспринимающий переменные.
Файл называется parser2.h
!*/
#include <iostream>
#include <cctype>
#include <cstring>
using namespace std;
const int NUMVARS = 26;
class parser {
enum typesT { UNDEFTOK, OPERATOR, NUMBER, VARIABLE};
enum errorsT { SERROR, PARENS, NOEXP, DIVZERO };
char *exp_ptr; // указывает на выражение
char token[80]; // содержит текущую лексему
typesT tok_type; // содержит тип лексемы
double vars[NUMVARS]; // хранит значения переменных
void eval_expl(double firesuit);
void eval_exp2(double firesult);
void eval_exp3(double firesult);
void eval_exp4(double firesult);
void eval_exp5(double &result);
void eval__exp6 (double fire suit) ;
void atom(double (result);
void get_token();
void putback();
void serror(errorsT error);
double find_var(char *s);
bool isdelim(char c);
498
Глава 13
public:
1 parser() ;
* double eval_exp(char *exp);
■ .;><■
' >// Конструктор
sparser::parser()
:'/i< . t .
■ j int i;
'{
* J exp_ptr = 0;
I
% i
&» ! for(i=0; KNUMVARS; i++) vars[i] = 0.0;
.+ // Входная точка анализатора.
i «double parser::eval exp(char *exp)
a
('j double result;
[ - exp_ptr = exp;
' d
J"« get_token() ;
if(!*token) {
. serror(NOEXP); // выражение отсутствует
"4 return 0.0;
! }
I eval_expl (result) ;
j if(*token) serror (SERROR) ; // последняя лексема должна быть нулевой
£| return result;
"1>
1*
"// Обработать присваивание.
Jvoid parser::eval_expl(double Sresult)
I int slot;
J, typesT ttok__type;
j char temp__token[80] ;
£.« if (tok_type=VARIABLE) {
// сохранить старую лексему
strcpy(temp_token, token) ;
ttok_type = tok^type;
V 3
i:
П
i ■ // вычислить индекс переменной
. * slot = toupper(*token) - 'A';
get_token();
if(*token != '=') { // не присваивание
putback(); // возвратить текущую лексему
// восстановить старую лексему
strcpy(token, temp_token);
tok type = ttok__type;
Объектно-ориентированный анализатор выражений
499
>
else {
get_token(); // получить следующую часть выражения
eval_exp2(result);
vars[slot] =, result;
return;
}
". )
eval_exp2 (result) ;
// Сложить или вычесть два члена,
void parser::eval_exp2(double firesult)
{
*, t register char op;
double temp;
eval_exp3(result);
while((op = *token) == ' + ' || op == '-*) (
i get_token();
eval_exp3(temp);
switch(op) {
J1,. 1 case ' - ' :
1 result = result - temp;
t break;
, case *+':
.' ] result = result + temp;
I break;
}
}
1
i)
// Перемножить или поделить два сомножителя,
'void parser::eval_exp3(double firesult)
.{
register char op;
J double temp;
j eval_exp4(result) ;
' while((op = *token) — '*' || op = ■/' II op == '%') {
I get_token() ;
eval_exp4 (temp) ;
i switch (op) {
case '*':
result = result * temp;
, break;
case '/' :
' j if(!temp) serror(DIVZERO); // попытка деления на ноль
{ ' else result = result / temp;
I | break;
case '%':
result = (int) result % (int) temp;
break;
500
Глава 13
}
}
}
// Обработать степень.
void parser::eval_exp4(double Sresult)
{
double temp, ex;
L register int t;
■ eval exp5(result);
if(*token== ,A') {
I get_token();
eval_exp4(temp);
■ ex = result;
if (temp=0.0) {
result =1.0;
return;
}
for (t= (int) temp-1; t>0; ~t) result = result * (double) ex;
i.*
// Оценить унарный + или -.
%void parser::eval_exp5(double firesult)
■{
j register char op;
*\ op = 0;
^ if((toJc_type == OPERATOR) &£ *token=' + ' || *token == '-') {
j op = *token;
get token();
•■; >
eval_exp6(result);
if(op='-') result = -result;
!>
II Обработать выражение в скобках,
void parser::eval_exp6(double firesult)
■{
if((*token — • (')) {
get_token();
eval_exp2(result);
if(*token != ')')
serror(PARENS);
get_token();
" )
else atom(result);
•>
// Получить значение числа или переменной,
void parser::atom(double sresult)
{
switch(tok_type) {
Объектно-ориентированный анализатор выражений 501
ч*
case VARIABLE:
result = find var(token);
i ,l| get_token() ;
return;
case NUMBER:
result = atof(token);
get_token();
return;
default:
serror(SERROR);
*i"rl
1 1
fcT.,
L
* 1
!V
// Возвратить лексему во входной поток.
' "void parser::putback()
U
\ char *t;
t = token;
for(; *t; t++) exp_ptr--;
я J{ Сообщить о синтаксической ошибке.
- void parser::serror(errorsT error)
Лч
t ij static char *e[]= {
"Д! "Syntax Error",
\ j "Unbalanced Parentheses",
.J "No expression Present",
t i "Division by zero"
i ■ ,;
I ,• cout « e[error] « endl;
л. ■
[' ,iJ// Извлечь следующую лексему.
Vjvoid parser::get_token()
1 , register char *temp;
I
j I tok_type = UNDEFTOK;
temp = token;
Г J *temp = '\0•;
r -4
•: I
if(**exp_ptr) return; // конец выражения
while(isspace(*exp_ptr)) ++exp_ptr; // пропустить пробелы
if(strchr("+-*/%A=<)", *exp_ptr)){
!.«! tok_type = OPERATOR;
!'■* // перейти к следующему символу
■ *terap++ = *exp_ptr++;
\ }
\t* else if(isalpha(*exp_ptr)) {
JjJ while(!isdelim(*exp_ptr)) *temp++ = *exp_ptr++;
502
Глава 13
У-\
tok_type = VARIABLE;
}
, 1] else if (isdigit(*exp_j?tr)) {
1 while (! isdelim(*exp_jptr)) *temp++ = *exp_j>tr++;
- 1 tok_type = NUMBER;
?'i
': *temp = '\0';
:'\}
. ]// Возвратить true, если с - ограничитель.
' 'bool parser::isdelim(char c)
\ «<
* » if<strchr(" +-/*%*=<)", с) || c=9 || c='\r' || c-=0)
* 1 return true;
return false;
■}
t
л «I
1 ''// Возвратить значение переменной,
.double parser::find var(char *s)
; -J if(!isalpha(*s)){
' 'I serror (SERROR) ;
"'"£ return 0.0;
' 1 >
ь , return vars [toupper (*token)- 'A' ] ;
AJJ ™__
Для испытания усовершенствованного анализатора вы можете
воспользоваться той же функцией main(), что и раньше. Не забудьте только включить
заголовок parser2.h вместо parserl.h. Теперь анализатор может обрабатывать
выражения, подобные следующим:
А = 10 / 4
А + В
С = А * (F - 21)
Вот пробный прогон программы:
Enter a period to stop.
Enter expression: A = 10 + 5
Answer is: 15
Enter expression: В = A / 3
Answer is: 5
Enter expression: С = A * В
Answer is: 75
Enter expression: .
Объектно-ориентированный анализатор выражений 503
( ПРИМЕЧАНИЯ
Код второго синтаксического анализатора по большей части тот же самый,
что и в первом анализаторе. Изменения связаны только с различными
нововведениями» необходимыми для поддержки переменных.
Начнем с того, что в перечислимый тип typesT введен новый
идентификатор VARIABLE:
enum typesT { UNDEFTOK, OPERATOR, NUMBER, VARIABLE};
Эта константа присваивается tok_type, если прочитана переменная. И, как
видите, теперь в коде убраны знаки комментария, ранее окружавшие
обработку имен переменных в get_token().
Как уже говорилось, для переменных используются буквы от А до Z.
Переменные хранятся в показанном ниже массиве vars, являющемся элементом
класса parser:
double vars[NUMVARS]; // хранит значения переменных
Каждая переменная занимает одну ячейку 26-элементного массива типа double.
Конструктор parser модифицирован и инициализирует теперь массив
переменных:
// Конструктор
parser::parser()
< .
int i;
exp_ptr = 0;
for(i=0; KNUMVARS; i++) vars[iJ = 0.0;
}
Переменные инициализируются нулями; это своего рода любезность,
оказываемая пользователю.
Нужна еще функция для нахождения значения конкретной переменной.
Так как переменные именуются буквами от А до Z, это очень просто сделать,
вычислив индекс переменной в массиве vars путем вычитания из ее имени
ASCII-значения для А. Что и делает функция-элемент find_var():
// Возвратить значение переменной,
double parser::find_var(char *s)
{
if(!isalpha(*s)){
serror(SERROR);
return 0.0;
}
return vars[toupper(*token)-'A'];
}
Так написанная функция допускает длинные имена, однако значима
только первая буква имени. Вы можете изменить это поведение в зависимости от
своих нужд.
Функция atom() расширена и обрабатывает теперь как числа, так и
переменные. Ниже показан новый вариант:
504
Глава 13
// Получить значение числа или переменной,
void parser::atom(double firesult)
{
swi tch(tok_type) {
case VARIABLE:
result = find_var(token);
get_token() ;
return;
case NUMBER:
result - atof(token);
get_token();
return;
default:
serror(SERROR) ;
}
}
Обратите внимание, что в операторе switch имеется теперь метка для
VARIABLE. Когда встречается переменная, ее значение извлекается из массива и
помещается в result.
С технической точки зрения рассмотренные изменения достаточны для
корректной обработки переменных анализатором; однако пока нет никакого
способа присваивать им значения. Часто это действие реализуют вне
синтаксического анализатора, но можно рассматривать знак равенства в качестве
операции (как это делается в C++) и включить присваивание в анализатор. Это
можно сделать разными способами. Метод, применяемый во втором
анализаторе, состоит в определении новой функции eval_expl() класса parser. Теперь
цепочку рекурсивного спуска начинает она; таким образом, для запуска
анализа выражения eval_exp() вызывает функцию eval_expl(), а не eval_exp2().
Вот код eval_expl():
// Обработать присваивание.
void parser::eval_expl(double firesult)
{
int slot;
typesT ttok_type;
char temp_token[80];
i f(tok_type==VARIABLE) {
// сохранить старую лексему
strcpy(temp_token, token) ;
ttok_type = tok_type;
// вычислить индекс переменной
slot = toupper(*token) - 'A';
get__token() ;
if(*token != '=') { //не присваивание
putback(); // возвратить текутщгю лексему
// восстановить старую лексему
strcpy(token, temp_token);
tok_type = ttok_type;
}
else {
Объектно-ориентированный анализатор выражений
505
get_token(); // получить следующую часть выражения
eval_exp2(result);
vars[slot] = result;
return;
}
}
eval_exp2(result);
}
Как видите, eval_expl() должна заглянуть вперед, чтобы определить,
производится ли в выражении присваивание. Присваиванию всегда предшествует
имя переменной, но имя переменной само по себе не гарантирует, что за ним
следует выражение присваивания. Другими словами, анализатор воспримет
А = 100 как присваивание, но он достаточно сообразителен, чтобы не принять
за присваивание А / 10. Такое поведение реализуется в eval_expl()
посредством чтения следующей лексемы из входного потока. Если это не знак
равенства, лексема возвращается в поток для дальнейшей обработки вызовом putback(),
показанной ниже:
// Возвратить лексему во входной поток,
void parser::putback()
{
char *t;
t = token;
for(; *t; t++) exp_ptr--;
}
Обобщенный анализатор
Два предыдущих синтаксических анализатора работали с численными
выражениями, в которых все значения предполагались относящимися к типу double.
Это прекрасно подходит для приложений, обрабатывающих вещественные числа,
но оказывается избыточным для программ, имеющих дело, например, только с
целыми значениями. Жесткая кодировка типа оцениваемых значений без всякой
на то причины ограничивает область применения анализатора.
К счастью, написав шаблон класса, можно легко создать обобщенную
версию синтаксического анализатора, пригодного для работы с любыми данными,
которые допускают выражения в алгебраической форме. Как только это
сделано, можно использовать анализатор как для встроенных типов, так и для
численных типов, созданных вами.
Код
Вот- обобщенная версия анализатора выражений. Она находится в файле
g_parser.h.
506
Глава 13
/* Обобщенный анализатор синтаксиса.
Файл называется g_parser.h
*/
#include <iostream>
#include <cctype>
*, #include <cstring>
using namespace std;
"* const int NUMVARS = 26;
* template <class PType> class parser {
' enum typesT { UNDEFTOK, OPERATOR, NUMBER, VARIABLE };
** enum errorsT { SERROR, PARENS, NOEXP, DIVZERO };
char *exp_ptr; // указывает на выражение
char token[80]; // содержит текущую лексему
typesT tok_type; // содержит тип лексемы
PType vars[NUMVARS]; // хранит значения переменных
*!f void eval_expl (PType firesult) ;
-l void eval_exp2(PType firesuit);
void eval_exp3(PType bresult);
void eval_exp4(PType Sresult);
void eval_exp5(PType firesult);
4 void eval_exp6(PType firesult);
i void atom(PType firesult);
"'* void get_token(), putback();
j void serror(errorsT error);
■ PType find_yar(char *s) ;
- ' bool isdelim(char c);
ipublic:
* parser();
l PType eval_exp(char *exp);
I
■k '// Конструктор
'template <class PType> parser<PType>::parser()
1 int i;
■ ■*■
1 ' expjptr * 0;
for(i=0; KNUMVARS; i++) vars[i] = (PType) 0;
\ >
,// Входная точка анализатора,
template <class PType> PType parser<PType>::eval_exp(char *exp)
{
PType result;
exp_j>tr = exp;
Объектно-ориентированный анализатор выражений 507
'ДО?
Ч get_token() ;
' * if('*token) (
serror(NOEXP); // выражение отсутствует
return (PType) 0;
}
eval_expl(result);
if(*token) serror(SERROR); // последняя лексема должна быть нулевой
return result,-
■i
ь К
\l>
* '// Обрабатывает присваивания.
Ь 'template <class PType> void parser<PType>::eval_expl(PType &result)
int slot;
* , typesT ttok_type;
t,' char temp token[80];
f i
' 1 if (tok_type=VARIABLE) {
// сохранить старую лексему
strcpy (temp_token, token);
ttok_type = tok_type;
* // compute the index of the variable
, \ slot = toupper(*token) - 'A';
,'j get_token() ;
\'^ if(*token ! = '—') { // не присвваивание
putback(); // возвратить текущую лексему
// восстановить старую лексему
strcpy(token, temp_token);
tok_type = ttok_type;
}
else {
get__token(); // получить следующую часть выражения
• eval_exp2(result);
"1 vars[slot] = result;
return;
}
£*
■
eval_exp2(result);
v J// Сложить или вычесть два члена.
г* чtemplate <class PType> void parser<PType>::eval_exp2(PType (result)
ъ register char op;
PType temp;
eval_exp3(result);
while((op = *token) == ' + » || op == '-') {
get_token();
, "* eval_exp3 (temp) ;
ъ\А switch (op) (
508
Глава 13
case '-':
^ \ result — result - temp;
- " break;
""/ case ' +' :
, _„' result = result + temp;
t."- '■ break;
. i >
4'
*// Перемножить или поделить два сомножителя.
*t -template <class PType> void parser<PType>::eval ехрЗ(PType «.result)
'ff<
*\ register char op;
. ч\ PType temp;
• r 4
1
j eval_exp4 (result) ;
i while((op = *token) «= '*• || op == •/* II op — ■%') {
\\.£ get_token() ;
i eval__exp4 (temp) ;
1 switch (op) {
\^ case '*' :
F ■• result = result * temp;
■ break;
"■\ case '/' :
J if(!temp) serror(DIVZERO); // division by zero attempted
4 ■ else result = result / temp;
■ '. break;
'*( case '%' :
i result = (int) result % (int) temp;
break;
4k )
I >
%
•' \// Обработать степень,
■template <class PType> void parser<PType>::eval_exp4(PType firesult)
^{
\ PType temp, ex;
register int t;
4
".'*-
. . eval_exp5(result);
si 4 if (*token= ,A') {
; /i get_token() ;
<?\ eval_exp4 (temp);
*.* ! ex = result;
if(temp= (PType) 0) {
result - (PType) 1;
return;
}
for(t=(int)temp-1; t>0; —t) result = result * ex;
*я >
Объектно-ориентированный анализатор выражений
509
[// Оценить унарный + или -.
template <class PType> void parser<PType>::eval_exp5(PType «result)
{
register char op;
op = 0;
if((tok_type == OPERATOR) S& *token=='+' || *token = '-') {
op = *token;
get__token() ;
}
eval_exp6(result);
if(op='-') result - -result;
)
[// Обработать выражение в схобках.
template <class PType> void parser<PType>::eval_exp6(PType sresult)
{
if((*token == ' (')) {
get_token();
eval_exp2(result) ;
if(*token != ')')
serror(PARENS);
get_token();
}
else atom(result);
l)
[// Получить значение числа или переменной.
[template <class PType> void parser<PType>::atom(PType Sresult)
(
switch(tok_type) {
case VARIABLE:
result ~ find_var(token);
get_token();
return;
case NUMBER:
result = (PType) atof(token);
get_token() ;
return;
default:
serror(SERROR);
}
// Возвратить лексему во входной поток.
template <class PType> void parser<PType>::putback()
{
char *t;
t ~ token;
for(; *t; t++) exp_ptr—;
510
Глава 13
«// Сообщить о синтаксической ошибке.
■template <class PType> void parser<PType>::serror(errorsT error)
и
static char *e[] = {
"Syntax Error",
* "Unbalanced Parentheses",
"No expression Present",
"Division by zero"
i сout « e[error] « endl;
h
// Извлечь следующую лексему,
(template <class PType> void parser<PType>::get_token()
"■ register char *temp;
\ tok_type = UNDEFTOK;
i temp = token;
*temp = •\0•;
if (!*exp__ptr) return; // конец выражения
■ l
while(isspace(*exp_ptr)) ++exp_jptr; // пропустить пробелы
if(strchr("+-*/%A=(>'\ *exp_ptr)){
tok_type = OPERATOR;
// перейти к следующему символу
*temp++ - *exp_ptr++;
}
else if(isalpha(*exp_ptr)) {
while(!isdelim(*exp_ptr)) *temp++ = *exp_ptr++;
tok_type = VARIABLE;
}
else if(isdigit(*exp_ptr)) {
while(!isdelim(*expj?tr)) *temp++ - *exp_ptr++;
tok_type - NUMBER;
1
*temp = '\0';
}
// Возвратить true, если с - разделитель,
'template <class PType> bool parser<PType>::isdelim(char c)
{
if(strchr(" +-/*%A=()", c) || c=9 || с«='\С || c==0>
return truer-
return false;
}
// Возвратить значение переменной,
'■^template <class PType> PType parser<PType>: : find_var (char *s)
-J1
Z if(!isalpha(*s)){
Объектно-ориентированный анализатор выражений 511
Iserror(SERROR);
return (PType) 0;
return vars[toupper(*token)-'A'];
Как видите, тип данных, которыми оперирует анализатор,
специфицируется формальным типом РТуре.
Следующая программа демонстрирует работу анализатора:
#include <iostream>
#include "g_parser.h"
using namespace std;
int main()
<
char expstr[80];
// Демонстрация анализатора для вещественных чисел.
parser<double> ob;
cout « "Floating-point parser. ";
cout « "Enter a period to stop\n";
for(;;) {
cout « "Enter expression: ";
cin.getline(expstr, 79);
if(*expstr=='.') break;
cout « "Answer is: " « ob.eval_exp(expstr) « "\n\n";
}
cout « endl;
// Демонстрация анализатора для целых значений.
parser<int> lob;
cout « "Integer parser. ";
cout « "Enter a period to stop\n";
for{;;) (
cout « "Enter expression: *';
cin.getline(expstr, 79);
if (*expstr=' . ') break ;
cout « "Answer is: " « lob.eval_exp(expstr) « "\n\n";
)
return 0;
}
Вот примерный прогон программы:
Floating-point parser. Enter a period to stop
Enter expression: a=10.1
Answer is: 10.1
Enter expression: b=3.2
Answer is: 3.2
512
Глава 13
Enter expression: a/b
Answer is: 3.15625
Enter expression: .
Integer parser. Enter a period to stop
Enter expression: a=10
Answer is: 10
Enter expression: b=3
Answer is: 3
Enter expression: a/b
Answer is: 3
Enter expression: .
Как видите, анализатор для чисел с плавающей точкой работает с
вещественными значениями, а анализатор для целых использует целую арифметику.
[ ПРИМЕЧАНИЯ
Обобщенный анализатор обладает теми же функциональными
возможностями, что и parser2.h, но обобщенную версию можно применить для анализа
выражений с любыми численными типами. Это достигается благодаря
спецификации типа данных, с которыми работает анализатор, в качестве параметра
шаблона (обобщенного формального типа). В остальном работа анализатора не
отличается от того, что уже было описано.
Однако будьте осторожны в отношении следующего момента: анализатор
все еще должен оперировать стандартными численными типами. Например,
рассмотрите такой фрагмент функции atom():
case NUMBER:
result = (PType) atof(token);
get_token();
return;
Хотя типом значения является РТуре, для преобразования лексемы в ее
двоичный эквивалент здесь все равно используется функция atof(). Это
означает, что нельзя создать представитель шаблона parser для типа, который не
может быть автоматически создан из double, — такого, как complex,
например. Однако вы всегда можете усовершенствовать анализатор таким образом,
чтобы он распознавал тип complex и обрабатывал его отдельно.
Расширение и улучшение анализатора
Чтобы синтаксические анализаторы в этой главе были просты для
понимания и легко адаптируемы, они представлены минимальными реализациями.
Однако их достаточно несложно улучшить и усовершенствовать. Вот
некоторые соображения на этот счет.
Объектно-ориентированный анализатор выражений 513
В анализаторах предусмотрены лишь рудиментарные средства обработки
ошибок» но вам могут потребоваться более детализированные диагностические
сообщения. Например, можно было бы подсвечивать на экране ту точку
выражения, где была детектирована ошибка. Это позволило бы пользователю легко
найти и исправить ее.
Как отмечалось в примечаниях к первому анализатору, синтаксическая
ошибка не приводит к завершению работы анализатора, благодаря чему могут
выдаваться множественные сообщения об ошибках. В одних ситуациях это
будет просто вызывать раздражение, однако в других окажется весьма
полезным, поскольку можно будет локализовать сразу несколько ошибок. Если вы
хотите при появлении ошибки остановить анализатор, наилучшим способом
будет такая реализация serror(), которая производила бы того или иного рода
сброс. Это проще всего сделать, воспользовавшись механизмом обработки
исключений C++ (ключевыми словами try, catch и throw). В зависимости от
ситуации можно также использовать пару функций setjmp() и longjmp(). Их
вызовы позволяют программе перейти от текущей к другой функции. Таким
образом, serror() могла бы, исполнив Iongjmp(), перейти в некоторую
безопасную точку за пределами анализатора.
В настоящей реализации анализатор может оценивать только численные
выражения. Однако, сделав некоторые добавления, можно придать
анализатору способность оценивать и другие типы выражений, включая строки,
пространственные координаты или комплексные числа. Например, для оценки
строковых объектов нужно внести следующие изменения:
1. Использовать для строк стандартный класс string.
2. Определить новый тип лексем с именем STRING.
3. Расширить get_token() таким образом, чтобы она воспринимала строки в
кавычках.
4. Ввести в atom() новый вариант case для лексем типа STRING.
Если реализовать эти пункты, анализатор сможет обрабатывать строковые
выражения, подобные следующим:
а = "one"
Ъ = "two"
с = а + b
Результатом будет конкатенация а и Ь, т. е. "onetwo".
Вот еще хорошее применение для синтаксического анализатора: создать
простую контекстно-вызываемую программу, которая принимает введенное
пользователем выражение и выводит результат. Это будет ценным
дополнением к практически любому коммерческому приложению. Если вы
программируете для Windows, это будет сделать особенно просто.
17 За* 1208
#d ЬУ*
<s,
лО>
у
ГЛАВА
-«ч 1 '"'
I ■:•■•.
< t
i
i№
f> * I
м!*м1^
\ -1.!
sb parser.cpp
SBASIC.CPP
Херберт Шильдт
576
Глава 14
Не возникало ли у вас когда-нибудь мысли создать собственный
компьютерный язык? Если вы похожи на большинство программистов, то,
скорее всего, да. И правда, возможность создать, расширять и модифицировать
свой собственный язык чрезвычайно соблазнительна. Однако немногие
программисты знают, насколько это может быть легким и занятным делом. Вне
всяких сомнений, разработка полномасштабного компилятора является
нешуточным предприятием; с другой стороны, создание языкового
интерпретатора — гораздо более простая задача. В этой главе вы изучите секреты
интерпретации и рассмотрите работающий, реальный пример.
Важность интерпретаторов обусловлена четырьмя совершенно различными
причинами. Во-первых, интерпретаторы способны поддерживать истинно
интерактивную среду. Многие приложения, в робототехнике, например, очень
выигрывают от реализации в интерпретирующей, а не компилирующей среде.
Во-вторых, по самой своей природе они очень хорошо подходят для
интерактивной отладки. В-третьих, интерпретаторы превосходно работают с
«языками сценариев», таких, как языки запросов в системах управления базами
данных. И наконец, интерпретаторы позволяют без всяких изменений запускать
программу на самых разных платформах. Для каждого нового окружения
нужно только реализовать пакет времени выполнения. Именно по этой
причине Java была первоначально спроектирована как интерпретирующий язык.
Хотя на переднем крае программирования всегда будут стоять
компиляторы, в последнее время интерпретаторы снова вышли из тени как важная
составная часть программного окружения. Весьма вероятно, что за время своей
карьеры в качестве программиста C++ вам потребуется написать ту или иную
форму интерпретатора (возможно, для языка сценариев). К счастью, C++
является идеальным языком для создания интерпретаторов.
Чтобы проиллюстрировать процесс разработки интерпретатора,
необходимо на практике интерпретировать некоторый язык. Естественным было бы
выбрать для этого C++, но этот язык просто очень велик и сложен, чтобы для
него можно было легко создать интерпретатор. Исходный код интерпретатора
даже для небольшого подмножества C++ занимал бы слишком много места и
не поместился бы в одной главе этой книги. Вместо этого для демонстрации
интерпретирующих методик мы возьмем подмножество стандартного BASIC,
которое мы в дальнейшем будем называть Small BASIC.
BASIC выбран по трем причинам. Во-первых, BASIC с самого начала и
предназначался для интерпретации, что упрощает написание его
интерпретатора. Например, в BASIC отсутствуют локальные переменные, рекурсивные
функции, перегрузка, шаблоны и т. д., — все, что увеличило бы сложность
интерпретатора. (Именно поэтому C++ гораздо труднее интерпретировать, чем
BASIC.) Однако принципы, лежащие в основе интерпретатора BASIC, прило-
жимы к любому другому языку, и вы можете взять разработанные здесь
процедуры в качестве отправной точки.
Второй причиной выбора BASIC является то, что его разумное
подмножество может быть реализовано сравнительно небольшим объемом кода. Наконец,
BASIC был выбран как язык, о котором подавляющее большинство
программистов имеет хотя бы поверхностное представление. Но если вы не знаете
BASIC, не расстраивайтесь. Имеющиеся в Small BASIC команды совершенно
тривиальны и понять их не составит труда.
Реализация языковых интерпретаторов на C++ 517
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Если вы интересуетесь вопросами интерпретации, то для вас будет
особенно интересен интерпретатор С, описанный в книге С: The
Complete Reference, 3rd Edition, Herbert Schildt, Osborne/McGraw-Hill, 1994.
Если вы захотите попробовать свои силы в разработке
интерпретатора C++, приведенный там интерпретатор С послужит для этого
отличной отправной точкой.
Анализатор выражений Small BASIC
Одной из важнейших частей языкового интерпретатора является анализатор
выражений. Как вы знаете, анализатор выражений используется для
преобразования численных выражений вроде (10 - X) / 23 в форму, которую компьютер
понимает и может оценивать. Small BASIC адаптирует анализатор выражений,
разработанный в 13-й главе. (Если вы ее не прочитали, сделайте это сейчас. Там
дано детальное описание синтаксического анализатора.) Хотя в своих основах
работа его не изменилась, анализатор, применяемый в Small BASIC, достаточно
отличен, насколько это необходимо для специализированной версии.
Многие изменения в анализаторе связаны с синтаксисом языка BASIC.
Например, синтаксический анализатор должен распознавать ключевые слова
этого языка; он не должен рассматривать знак = в качестве операции; он
должен оценивать операции отношений. Функция get_token() подверглась
значительным модификациям, в соответствии с возросшими к ней требованиями.
Другие различия между интерпретаторами 13-й главы и описанным здесь
исходят из соображений эффективности. Например, поскольку сам
интерпретатор и анализатор выражений читают один и тот же исходный код,
эффективнее будет, если обе подсистемы используют один и тот же указатель на
интерпретируемую программу. В анализаторах, разработанных в предыдущей
главе, указатель на оцениваемое выражению передавался как параметр. Но в
показанной здесь версии указатель на программу сделан глобальным, и он
доступен и для интерпретатора, и для анализатора синтаксиса. Есть и другие, менее
значительные отличия, направленные на улучшение общей скорости работы
интерпретатора. Так как интерпретаторы по самой своей природе медленны,
любое увеличение эффективности кода становится важным.
Поскольку синтаксический анализатор Small BASIC оптимизирован и
интегрирован с интерпретатором, он больше не инкапсулирован в отдельном
классе. Вместо этого он реализуется автономными функциями. Побочным
выигрышем от такой организации является то, что интерпретатор и анализатор
совместно используют некоторые разделы кода. Например, и тот и другой
вызывают get_token() и serror(). Нужно подчеркнуть, что инкапсуляция
синтаксического анализатора (как в главе 13) вполне возможна. Но отказ от нее
позволяет в ряде мест организовать код интерпретатора более рационально
Так как анализатор выражений Small BASIC основан на применении тех же
методик, что описывались в 13-й главе, вам не составит труда разобраться в
его работе. Однако перед изучением кода анализатора следует сделать
несколько общих замечаний. Мы начнем с точного определения того, что такое
выражение в смысле языка Small BASIC.
518
Глава 14
Выражения Small BASIC
Если говорить о выражениях в плане разрабатываемого здесь
интерпретатора Small BASIC, то выражение состоит из следующих элементов:
♦ Целых чисел
♦ Операций 4- - / * Л <>>=<= о
♦ Переменных
В языке BASIC A означает возведение в степень. Знак = используется как
для присваиваний, так и для отношения равенства; однако в выражениях
BASIC он является операцией только в выражениях отношения. (В
стандартном BASIC присваивание не операция, а оператор.) Неравенство обозначается
как о. Эти элементы можно комбинировать по правилам алгебры, образуя
выражения. Пот несколько примеров:
7-8
(100 - 5) * 14 / 6
а + b - с
А < В
Старшинство этих операций следующее:
наивысшее:
|
наинизшее:
{)
унарные + -
-
*/
+ -
<><=>=<> =
Операции одинакового старшинства оцениваются слева направо.
В Small BASIC принимаются следующие предположения. Все переменные
являются одиночными буквами; это означает, что всего доступны 26
переменных от А до Z. Хотя в стандартном BASIC допустимы дополнительные имена,
состоящие из буквы с последующим числом (например, Х27), в
разрабатываемом интерпретаторе Small BASIC мы отказались от них в интересах ясности.
Переменные не различают регистр; а и А рассматриваются как одна и та же
переменная. Все числа являются целыми, хотя было бы несложно написать
процедуры для обработки значений других типов, например, вещественных.
Наконец, в выражениях не поддерживаются строки, хотя строки в кавычках
можно использовать для вывода на экран сообщений. Эти предположения
встроены в синтаксический анализатор.
Лексемы языка Small BASIC
В Small BASIC каждая лексема имеет две формы: внешнюю и внутреннюю.
Внешняя форма — текстовая, как она записывается в программе. Например,
внешней формой команды PRINT будет "PRINT". Хотя можно разработать
интерпретатор, в котором каждая лексема использовалась бы только во внешней
текстовой форме, это редко (если вообще) делается, потому что такой подход
страшно неэффективен. Вместо этого используется внутренний формат лексе-
Реализация языковых интерпретаторов на C++ 519
мы, который представляет собой просто целое число. Например, команда
PRINT представляется числом 1, команда INPUT — числом 2 и т. д.
Преимуществом такого внутреннего представления является то, что для него можно
написать значительно более быстрые процедуры, чем для строк.
Задача преобразования лексем из внешнего формата во внутреннее
представление возложена на функцию get_token(). Имейте в виду, что не у всех
лексем эти формы различаются. Например, преобразование большинства
операций не дало бы никаких преимуществ, поскольку они и так представлены в
текстовой форме одиночными символами.
Код
Ниже приводится весь анализатор выражений, модифицированный для
применения в интерпретаторе языка Small BASIC. Он находится в отдельном
файле (если объединить вместе код анализатора и интерпретатора, получился
бы слишком большой файл, поэтому целесообразно разместить их в отдельных
файлах). Смысл и использование внешних переменных мы поясним, когда
будем рассматривать интерпретатор.
/* Анализатор выражений языка Small BASIC.
Данный анализатор является адаптацией
анализатора из главы 13. Он спроектирован
для использования в простых языковых
интерпретаторов, таких, как Small BASIC.
Файл называется sbparser.cpp
*/
#include <iostream>
#include <cctype>
#include <cstdlib>
#include <cstring>
using namespace std;
// Типы лексем языка Small Basic.
enum typesT { UNDEFTOK, OPERATOR, NUMBER, VARIABLE, COMMAND,
STRING, QUOTE };
// Лексемы команд Small Basic.
enum SBtokensT { UNKNCOM, PRINT, INPUT, IF, THEN, FOR, NEXT, TO,
GOTO, GOSUB, RETURN, EOL, FINISHED, END };
/* Эти константы передаются при вызове serror(), когда
встречается синтаксическая ошибка. Если хотите, можете
определить дополнительные константы.
ЗАМЕЧАНИЕ: SERROR означает общее сообщение об ошибке,
когда ни одна из других констант не подходит. */
enum errorsT
{ SERROR, PARENS, NOEXP, DIVZERO, EQUAL_EXP,
NOT_VAR, LAB_TAB_FULL, DUP_LAB, UNDEF_LAB,
THEN_EXP, TO_EXP, TOO_MNY_FOR, NEXT_WO_FOR,
TOO_MNY_GOSUB, RET_WO__GOSUB, MISS_QUOTE };
520
Глава 14
ft'.Jenum double_ops { LE=1, GE, NE };
, /extern char *prog; // указатель на текущую точку программы
* ^extern char *p_buf; // указывает на начало программы
-extern int variables[26]; // переменные
"^extern struct commands {
* s, char command[20] ;
*. * SBtokensT tok;
'$. '■} tablet] ;
/''extern char token[80]; // содержит текстовое представление лексемы
' -1 extern typesT token_type; // содержит тип лексемы
^extern SBtokensT tok; // содержит внутреннее представление лексемы
jvoid eval_exp(int firesult);
,' .void eval_expl (int firesult) ;
jvoid eval_exp2(int firesult);
■ t. -void eval_exp3 (int firesult);
Void eval_exp4(int firesult);
*void eval_exp5(int firesult);
«■•.Jvoid eval_exp6(int firesult) ;
"', jvoid atom (int firesult);
Ivoid putbackO ;
,. 'void serror (errorsT error);
i.'" I typesT get_token () ;
|T.-jSBtokensT look_up(char *s) ;
i*"]bool isdelim(char c) ;
.-jbool is_sp_tab(char c);
■ .'int find_var (char *s) ;
i . 1
pi*.// Входная точка анализатора.
*.! void eval_exp(int firesult)
!"\# i
4 get_token();
. ■ if(»*token) {
r ■ - serror(NOEXP);
1 return;
;""'■ eval_expl (result) ;
s "i putback(); // вернуть последнюю прочитанную лексему в поток
\ .->
"*"■"// Обработка операций отношения.
1-sWoid eval expl (int firesult)
;'Ч<
. i // Операции отношений.
\ ' char relops[] = {
Й-Г GE, NE, LE, '<', '>' , ' = ',0
;9 };
\ int temp;
\-Л register char op;
eval__exp2 (result) ;
op = *token;
Реализация языковых интерпретаторов на C++ 521
if(strchr(relops, op)) {
get_token();
eval_expl(temp);
switch(op) { // выполнить операцию
case '<':
result = result < temp;
break;
case LE:
i result = result <= temp;
break;
case •>•:
result = result > temp;
break;
case GE:
result - result >- temp;
break;
case '«•:
result = result == temp;
break;
case NE:
result = result != temp;
break;
}
}
)
// Сложить или вычесть два члена,
void eval_exp2(int firesult)
{
register char op;
int temp;
eval_exp3(result);
while((op = *token) = '+' || op == '-') {
get_token();
eval_exp3(temp);
switch(op) {
case '-'
result = result - temp;
break;
case '+':
result = result + temp;
break;
)
}
}
// Перемножить или поделить два сомножителя.
void eval_exp3(int firesult)
{
register char op;
int temp;
eval__exp4 (result) ;
while((op = *token) «= •*' || op =- */') i
get_token();
eval_exp4(temp);
522
Глава 14
switch (op) {
case '*' :
result - result * temp;
break;
case '/':
if(!temp) serror(DIVZERO); // попытка деления на ноль
result = result / temp;
break;
}
■ }
'// Обработать целую степень,
"void eval_exp4(int firesult)
] int temp, ex;
register int t;
eval_exp5(result);
if(*token= ,AI) {
. -." get_token() ;
1 t eval_exp4(temp);
. if(!temp) {
, ' result = 1;
i - return;
:.. )
i-"- ex = result;
!\V for(t=temp-l; t>0; t--) result = result * ex;
'■ )
;■ >
// Унарный + или -.
:void eval_exp5(int bresult)
■■*.'<
: register char op;
- У ■
1
; op = 0 ;
\ if ( (token_type=OPERATOR) &S
* token—'+' || *token=='-')
°P — *token;
get_token() ;
f eval_exp6(result);
{'*, if(op=='-') result = -result;
■-- i)
;// Обработать выражение в скобках.
.■ "jvoid eval exp6(int firesult)
U
if(*token = •(') {
get_token();
eval_exp2(result);
if(*token ! = ')')
serror(PARENS);
get_token();
Реализация языковых интерпретаторов на C++ 523
)
else
atom(result) ;
)
// Найти значение числа или переменной,
void atom(int &result)
{
switch(token_type) {
case VARIABLE:
result = find_var(token);
get_token{) ;
return;
case NUMBER:
result = atoi(token);
get_token();
return;
default:
serror(SERROR);
}
>
// Найти значение переменной,
int find_yar(char *s)
{
if(!isalpha(*s)){
serror(NOT_VAR); // не переменная
return 0;
}
return variables[toupper(*token)-'A1];
>
// Вывести сообщение об ошибке,
void serror(errorsT error)
{
char *p, * temp;
int linecount = 0;
register int i;
static char *e[]= {
"Syntax error",
"Unbalanced parentheses",
"No expression present",
"Division by zero",
"Equal sign expected",
"Not a variable",
"Label table full",
"Duplicate label",
"Undefined label",
"THEN expected",
"TO expected",
"Too many nested FOR loops",
"NEXT without FOR",
"Too many nested GOSUBs",
"RETURN without GOSUB",
"Double qoutes needed"
524
Глава 14
);
cout « е[error];
р = p_buf;
while(p != prog) { // найти номер строки с ошибкой
р++;
if(*P == '\r') {
linecount++;
>
}
cout « " in line " « linecount « ".\n";
temp — p; // вывести ошибочную строку
for(i=0; i<20 && p>p_buf && *p»-'\n'; i++, p—);
for(; p<=temp; p++) cout « *p;
throw(1); // выбросить исключение
// Извлечь лексему.
typesT get_token()
{
register char *temp;
token_type — UNDEFTOK;
tok = UNKNCOM;
temp — token;
if (*prog=* \0 ') { // конец файла
*token = '\0';
tok = FINISHED;
return(token type=OPERATOR);
)
while(is_sp_tab{*prog)) ++prog; // пропустить пробелы
if(*prog='\r') ( // cr/lf
++prog; ++prog;
tok = EOL; *tokens'\r';
token[l]='\n'; token[2]=0;
return (token_type - OPERATOR);
}
if(strchr("<>", *prog)) { // проверка на операцию из двух знаков
switch(*prog) {
case '<':
if<*<prog+l)=*•>') {
prog++; prog++;
*temp = NE;
}
else if(*(prog+l)=='='J {
prog++; prog++;
*temp « LE;
}
else {
prog++;
Реализация языковых интерпретаторов на C++
525
*temp = '<';
}
tenrp++ ;
*tenip = '\0' ;
break;
case '>':
if(*(prog+l)=— ' = ') {
prog++; prog++;
*temp = GE;
}
else {
prog++;
*temp - *>' ;
}
temp++;
*temp = '\0' ;
break;
}
return(token_type = OPERATOR);
}
if (strchrC4--*A/=; () ," f *prog)){ // операция
*temp = *prog;
prog++; // перейти к следующей позиции
temp++;
*temp =
'Nonreturn (token_type=OPERATOR);
}
if(*prog=='M') { // строка в кавычках
prog++;
while (*prog'-""&& *prog!='\r*) *temp++ = *prog++;
if (*prog=' \r') serror (MISS_QUOTE) ;
prog++; *temp = '
Nonreturn (token_type=QUOTE) ;
}
if(isdigit(*prog)) { // число
while(!isdelim(*prog)) *temp++ - *prog++;
*temp = '\0';
return(token_type = NUMBER);
}
if(isalpha(*prog)) { // переменная или команда
while(!isdelim(*prog)) *temp++ = *prog++;
token_type = STRING;
}
*temp - '\0';
// проверить, что строка является командой или переменной
if(token_type==STRING) {
tok = look_up(token); // преобразовать во внутреннюю форму
if(!tok) token_type = VARIABLE;
else token_type = COMMAND; // команда
}
526
Глава 14
• ' return token_type;
: )
Iff Вернуть лексему во входной поток.
■ void putback.0
{
char *t;
ъ
t = token;
, for(; *t; t++) prog--;
;}
i
[f* Отыскать внутреннее представление лексемы
в таблице лексем.
SBtokensT look__up(char *s)
. {
, register int i;
char *p;
// преобразовать в нижний регистр
р = s;
while(*р){
*р = tolower(*p);
; р++;
■ ■: }
г
// проверить, имеется ли лексема в таблице
f for(i=0; *table[i].command; i++)
• if(!strcmp(table[i].command, s))
i return table[i].tok;
return UNKNCOM; // неизвестная команда
}
tff Возвратить true, если с - разделитель,
"bool isdelim(char с)
"■<
-. if(strchr(" ;,+-<>/*%*=*()", с) || c==9 || c=='\r' || c==0)
* return true;
.., return false;
? >
' // Возвратить true, если с - пробел или табуляция.
,booX is_sp_tab(char с)
if(c==' ' || c=='\f) return true;
* else return false;
-4iJ ___™
ПРИМЕЧАНИЯ
| ПРЕ
Показанный анализатор выражений может обрабатывать следующие
операции: +, ~, *, /, целую степень (Л), операции отношений и унарный минус. Он
Реализация языковых интерпретаторов на C++
527
также корректно обрабатывает скобки. Заметьте, что он имеет шесть уровней
и еще функцию atom(), возвращающую значение числа.
Анализатор построен вокруг функции get_token(). Это расширенный
вариант функции, показанной в главе 13. Внесенные изменения позволяют ей
выделять не только лексемы выражений, но и другие элементы BASIC-програм-
мы, такие, как ключевые слова и строки.
В Small BASIC программа хранится в виде одной длинной строки,
ограниченной нулем. Функция get_token() двигается по программе шагами по
одному символу. На символ, который будет читаться следующим, ссылается
глобальный указатель. В показанном здесь варианте get_token() указатель
называется prog. Он глобальный, поскольку должен сохранять свое значение
между вызовами get__token() и к нему должны обращаться другие функции.
Данный синтаксический анализатор работает с шестью типами лексем:
OPERATOR, VARIABLE, NUMBER, COMMAND, STRING и QUOTE.
OPERATOR используется и для операций, и для скобок. VARIABLE используется
при обнаружении переменной. NUMBER означает число. Тип COMMAND
присваивается, когда встречается команда BASIC. STRING является внутренним
типом для get_token(). Тип QUOTE предназначен для строк в кавычках. Тип
лексемы сохраняется в глобальной переменной token_type. Внутреннее
представление лексемы записывается в глобальную tok.
Посмотрите внимательно на get_token(). Так как люди любят для ясности
вставлять в выражение пробелы, начальные пробелы пропускаются; для этого
вызывается функция is_sp_tab(), возвращающая true, если ее аргумент —
пробел или табуляция. Когда пробелы пропущены, prog будет указывать на
число, переменную, команду, возврат каретки / перевод строки, операцию,
строку в кавычках или на нуль, если пробелы стояли в конце программы. Если
далее идет возврат каретки, tok устанавливается равной EOL, в token
записывается последовательность CR/LF, а в token_type помещается OPERATOR.
Затем функция ищет операцию из двух знаков, такую, как <=. Функция
get_token() переводит двойные операции в их внутреннее представление.
Значения NE, GE и LE определяются вне функции get_token(). Если следующий
символ является одиночным знаком операции, он помещается в виде строки в
глобальную переменную token, а в token_type записывается тип OPERATOR.
В противном случае функция ищет строку в кавычках. Если таковой не
найдено, get__token() определяет, не является ли следующая лексема числом,
проверяя, является ли первый символ цифрой. Если же следующий символ —
буква, то лексема должна быть переменной или командой (такой, как PRINT).
Функция look_jip() сравнивает лексему с командами в таблице и, если
находит совпадение, возвращает соответствующее внутреннее представление
команды. (Функция look_up() будет обсуждаться позже.) Если команда в таблице
не найдена, лексема полагается переменной. Наконец, если лексема не
является ни одной из предыдущих, предполагается, что достигнут конец выражения и
в token устанавливается нуль, сигнализирующий о конце выражения.
Чтобы лучше понять, как работает данный вариант get_token(),
посмотрите, что возвращает функция для каждой лексемы и типа при анализе такого
выражения:
PRINT А + 100 - (В * С) / 2
528
Глава 14
Лексема
PRINT
А
+
100
-
(
В
*
с
)
/
2
Тип лексемы
COMMAND
VARIABLE
OPERATOR
NUMBER
OPERATOR
OPERATOR
VARIABLE
OPERATOR
VARIABLE
OPERATOR
OPERATOR
NUMBER
Помните, что token всегда будет содержать ограниченную нулем строку,
даже если последняя состоит из единственного символа.
Чтобы оценить выражение, установите prog на начало строки, в которой
оно находится, а затем вызовите eval_exp() с переменной, куда нужно
записать результат. Заметьте, что eval_expl() отличается от соответствующей
функции из 13-й главы. Как вы помните, там эта функция обрабатывала
операцию присваивания. Однако в BASIC присваивание есть оператор, а не
операция. Поэтому при разборе выражений в BASIC-программах eval_expl() не
используется для обработки присваиваний, вместо этого она оценивает операции
отношений. Если вы будете экспериментировать с интерпретатором,
адаптируя его к другим языкам, вам может понадобиться дополнительная функция
eval_expO(), которая будет обрабатывать присваивание как операцию.
Еще одно важное различие между данным анализатором и анализаторами
из главы 13 состоит в том, что в предыдущей главе на конец выражения
указывал нуль, ограничивающий содержащую выражение строку. В настоящей
версии о конце выражения сигнализирует конец строки или нечто такое, что
не может являться частью выражения, например, команда BASIC.
Особое внимание следует уделить функции serror(), которая вызывается
для вывода сообщений об ошибке. Когда обнаруживается синтаксическая
ошибка, ее идентификатор передается serror(). Функция выводит
соответствующее сообщение, номер строки с ошибкой и ту ее часть, в которой ошибка
локализована. Как сказано в комментарии перед перечислением еггогТ, если
ничто другое*не подходит, выводится просто сообщение "syntax error". В
противном случае сообщается о конкретной ошибке. Обратите внимание, что в
конце s err or () выбрасывает исключение оператором throw. Это исключение
должно перехватываться оператором catch, который предпримет какие-то
разумные действия. В нашем интерпретаторе Small BASIC оператор catch
находится в main() и просто завершает выполнение программы.
Как уже говорилось, в качестве имен переменных интерпретатор Small BASIC
распознает только буквы от А до Z. Каждая переменная занимает одну ячейку
26-элементного целого массива integers. Этот массив определяется в коде
интерпретатора, как показано ниже; все переменные инициализируются нулями:
Реализация языковых интерпретаторов на C++ 529
int variables[26]- { // 26 пользовательских переменных A-Z
О, О, О, О, О, О, О, О, О,
О, О, О, О, О, О, О, О, О,
О, О, О, О, О, О, О, О
>;
Поскольку имена переменные являются буквами от А до Z, по ним очень
просто определить индекс переменной в массиве variables путем вычитание из
ее имени ASCII-значения для А. Функция find_var() находит значение
переменной. Эта функция допускает длинные имена переменных, но значимой
является только первая буква. Если хотите, вы можете изменить функцию так,
чтобы воспринимались только однобуквенные имена.
Интерпретатор языка Small BASIC
Интерпретаторы состоят из двух частей: синтаксического анализатора,
оценивающего выражения, и собственно интерпретатора, который реально
исполняет программу. В этом разделе рассматривается модуль интерпретатора.
Код
В следующем листинге показан весь код интерпретатора Small BASIC, за
исключением элементов, расположенных в модуле синтаксического
анализатора. Вы должны компилировать оба файла и затем скомпоновать их.
Назовите исполняемый файл SBASIC.
[ '. /* Интерпретатор Small BASIC.
h J:
Вы можете легко расширить этот интерпретатор
\ *' или взять его в качестве отправной точки для
ь ■_ ■! разработки собственного программного языка.
:-■* з Файл называется sbasic. срр
Г" I
&'5*£#i.nclude <iostream>
';■ f#include <fstream>
T'| , #include <cctype>
J.n#include <cstdlib>
;' ^'#include <cstring>
|..{Using namespace std;
5 * ;
' ^const int NUMJCAB « 100.;
„'Const int LAB_LEN = 10;
'*'const int FOR_NEST = 25;
>-i"const int SUB_NEST = 25;
JiV-const int PROG SIZE = 10000;
;- ;// Типы лексем Small Basic.
"-'enum typesT { UNDEFTOK, OPERATOR, NUMBER, VARIABLE, COMMAND,
STRING, QUOTE };
.-'
: *// Лексемы команд Small Basic.
£?jenum SBtokensT { UNKNCOM, PRINT, INPUT, IF, THEN, FOR, NEXT, TO,
530
Глава 14
Ч GOTO, GOSUB, RETURN, EOL, FINISHED, END };
'/* Эти константы передаются в вызовах serror(), когда
] происходит синтаксическая ошибка. Вы можете расширить их набор,
ЗАМЕЧАНИЕ: SERROR - общее сообщение об ошибке, передаваемое,
когда ничто другое не подходит. */
*enum errorsT
1 { SERROR, PARENS, NOEXP, DIV_ZERO, EQUAL_EXP,
K NOT_VAR, LAB_TAB_FULL, DUP_LAB, UNDEF_LAB,
THEN_EXP, TO_EXP, TOO_MNY_FOR, NEXT_WO_FOR,
■ , TOO_MNY_GOSUB, RET_WO_GOSUB, MISS_QUOTE } ;
fchar *prog; // указывает на текущую точку программы
1 jchar *p_buf; // указывает на начало программы
int variables[26]= { // 26 пользовательских переменных A-Z
О, О, О, О, О, О, О, О, О,
1 ; о, о, о, о, о, о, о, о, о,
■' О, О, О, О, О, О, О, О
;>;
1
,// таблица поиска ключевых слов
struct commands {
; , char command[20]; // текстовая форма
SBtokensT tok; // внутреннее представление
'} table[] = { // Команды в этой таблице должны быть
"print", PRINT, // в нижнем регистре,
"input", INPUT,
; "if", IF,
."' "then", THEN,
1 ■' "goto", GOTO,
I "for", FOR,
; "next", NEXT,
' "to", TO,
. "gosub", GOSUB,
"return", RETURN,
"end", END,
"", END // отмечает конец таблицы
"■ Л;
■ t»char token[80];
1 typesT token_type;
SBtokensT tok;
i
K\// таблица поиска меток
!struct label {
■ ; char name[LAB__LEN] ; // метка
char *p; // указывает на позицию метки в исходном файле
} label table[NUM LAB];
■ i — —
i' I
.»// поддержка циклов FOR
{struct for_stack {
int var; // счетчик
j int target; // конечное значение
! char *loc; // позиция начала повторения в исходном коде
. } fstack[FOR NEST]; // стек для вложенных циклов FOR/NEXT
Реализация языковых интерпретаторов на C++
531
char *gstack[SUB_NEST]; // стек для GOSUB
int ftos; // индекс вершины стека FOR
int gtos; // индекс вершины стека GOSUB
void print();
void scan_labels ();
void find_eol();
void exec_goto();
void exec_if();
void exec_f or();
. "void next();
void fpush(struct for_stack i);
void input();
void gosub() ;
void greturn();
void gpush(char *s) ;
void label_init();
void assignment();
char *find_label(char *s) ;
char *gpop();
struct for_stack fpop();
bool load_program(char *p, char *fname);
' int get_next_label(char *s);
// прототипы функций из файла синтаксического анализатора
void eval_exp(int &result);
typesT get_token();
void serror(errorsT error), putback();
int main(int argc, char *argv[])
t
i
if(argc!=2) {
cout « "Usage: sbasic <filename>\n";
return 1;
}
i // выделение памяти для программы
1 try {
.. i prog = new char [PROG_SIZE] ;
} catch(bad_alloc xa) {
cout « "Allocation Failure\n";
return 1;
}
p_buf = prog;
// загрузить программу для исполнения
i f(!load_program(prog, argv[1])) return 1;
»ч // начало главного блока try
'*- try {
scan_labels(); // отыскать метки
ftos =0; // инициализировать стек FOR
gtos =0; // инициализировать стек GOSUB
do {
532
Глава 14
■ \
ъ
token_type = get_token();
// проверить на оператор присваивания
i £ (token_type=VARIABLE) {
putback(); // вернуть переменную во входной поток
4 assignment(); // должен быть оператором присваивания
I }
j else // команда
I switch(tok) {
k ' case PRINT:
* I print ();
» i break;
case GOTO:
exec_goto();
j break;
case IF:
. exec_if();
J break;
case FOR:
4 exec_for();
* break;
case NEXT:
next();
break;
case INPUT:
input () ;
break;
case GOSUB:
'4 gosub();
j break;
I case RETURN:
I1 greturn() ;
break;
case END:
J return 0;
j } while (tok != FINISHED);
} // конец блока try
' /* Перехват исключений. В данной реализации только
serror() выбрасывает исключение. Однако при создании
своего собственного языка вы можете выбрасывать
исключения различных типов.
*/
i
'*1 catch (int) {
я return 1; // фатальная ошибка
i>
}
return 0;
\- // Загрузить программу.
К bool load_program(char *p, char *fname)
{
, ifstream in(fname, ios::in | ios::binary);
^д int i=0;
Реализация языковых интерпретаторов на C++ 533
\ i if(!in) <
*t \ cout « "File not found ";
? 1 cout « "— be sure to specify .BAS extension.\n";
return false;
}
M
i = 0;
do {
*p = in.get();
p++; i++;
} while (! in. eof{) &£ KPROG SIZE) ;
\ // закончить текст программы нулем
i rf ] if (* (p-2)=0xla) *(p-2) = '\0'; // отбросить маркер eof
■ ,\ else Mp-1) - *\0";
i '!
^ j in.close();
f ) return true;
*4
* •]// Найти все метки.
'. void scan labels ()
{
int addr;
char *temp;
К - label_init{); // обнулить все метки
,,, temp = prog;' // сохранить указатель программы
'ц. // если первая лексема файла - метка
V *• get_token() ;
* ч if (token_type==NUMBER) {
*t strcpy(label_table[0].name, token);
i label table[0]-p = prog;
"v >
* find_eol();
■"■ do {
■ -\ get_token() ;
if (token_type==NUMBER) {
i . addr = get_next_label(token);
I if(addr == -1 || addr — -2) {
U J (addr = -1) ? serror(LAB_TAB_FULL> : serror(DUP_LAB);
, *; >
, strcpy(label_table[addr].name, token);
i::1
>
// записать текущую позицию указателя
label_table[addr].p = prog;
т // если не на пустой строке, найти следующую строку
"' * if (tok!=EOL) find_eol();
' j } while(tok'=FINISHED);
* ) prog = temp; // восстановить исходную позицию
534
Глава 14
ч *
s // Найти начало следующей строки.
* * void find_eol()
' ' '{
-_ I while(*prog!='\n' && *prog<='\0') ++prog;
if(*prog) prog++;
</* Возвратить индекс первой свободной ячейки массива меток
4 -1 возвращается, если массив заполнен.
i -2 возвращается при дублировании меток.
'int get_next__label (char *s)
{
register int i;
for(i=0; i<NUM_LAB; ++i) {
•\ if(label_table[i].name[O]==0) return i;
if (!strcmp(label_table[i] .name, s)) return -2;
f )
return -1;
}
i */* Найти местоположение данной метки. Если не найдена,
иИ возвращается нуль; в противном случае возвращается
указатель на позицию метки.
char *find label(char *s)
"j register int i;
for(i=0; KNUMJLAB; ++i)
, if(!strcmp(label_table[i].name, s))
) return label_table[i] ,p;
return 0; // ошибка
[' )
!/* Инициализировать массив меток.
- * Предполагается, что нулевое имя метки означает,
; что ячейка массива не занята.
г */
» t void label_init()
{
register int i;
ft " for(i=0; i<NUM_IAB; ++i)
t label_table[i].name[0] = 0;
;<- :>
SII Присвоить значение переменной.
, "void assignment()
ь {
int var, value;
// получить имя переменной
Реализация языковых интерпретаторов на C++ 535
get_token();
if(!isalpha(*token)) {
serror(NOT_VAR);
return;
}
t
// преобразовать в индекс массива переменных
var = toupper(*token)-'A';
// извлечь знак равенства
get_token();
if(*token != '=') {
serror(EQUAL_EXP);
* return;
}
// получить присваиваемое значение
eval_exp(value);
// присвоить значение
variables[var] = value;
}
// Исполнить простой вариант оператора PRINT языка BASIC.
void print()
* {
int result;
int len=0, spaces;
char last_delim, str[80];
do {
* get_token(); // получить следукаций элемент списка
if(tok==EOL || tok=FINISHED) break;
i f(token_type==QUOTE) { // строка
cout « token;
len += strlen(token);
/" get_token () ;
}
else { // выражение
putback();
eval_exp(result) ;
get_token();
■ cout « result;
itoa(result, str, 10);
len += strlen(str); // сохранить длину
last_delim = *token;
// если запятая, перейти к следующей позиции табуляции
if(*token == ',') {
// вычислить число пробелов до следующей табуляции
spaces = 8 - (len % 8) ;
len += spaces; // добавить полученное число пробелов
while(spaces) {
cout « " "; ,
space s--;
: }
536
Глава 14
}
else if (*token=';') {
cout « " ";
I len++;
i else if(tok!=EOL && tok!=FINISHED) serror(SERROR);
: } while (*token=='; • | I *token~ *, *) ;
if(tok—EOL || tok—FINISHED) {
if(last_delim != ';' && last_delim != ',')
cout « endl;
I )
1 else serror(SERROR);
.}
,// Исполнить оператор GOTO,
.void exec_goto()
j{
char *loc;
I
get_token(); // извлечь метку перехода
// найти местоположение метки
'l loc = find_label(token);
, if(loc==NULL)
я serror(DNDEF_LAB); // метка не определена
" else prog = loc; // начать исполнение программы с loc
I)
*// Исполнить оператор IF.
.void exec_if()
■{
int result;
I
* eval_exp(result); // получить значение выражения
■ *
■ if(result) { // если true, обработать целевой оператор IF
" get_token() ;
if(tok!=THEN) (
serror(THEN_EXP);
■ return;
| ) // если нет ошибки, целевой оператор исполняется
}
else find_eol(); // найти начало следукщей строки
J
■
J)
•// Исполнить цикл FOR.
."void exec_for()
и
struct for_stack stckvar;
int value;
get_token(); // прочитать управляющую переменную
if(!isalpha(*token)) {
serror(NOT_VAR);
return;
Реализация языковых интерпретаторов на C++ 537
: >
// сохранить индекс управляющей переменной
,* л stckvar.var =* toupper(*token)-'А' ;
i
j get_token(); // прочитать знак равенства
"j if(*token != ' = ') {
■ J serror(EQUAL_EXP);
return;
-! }
*"\ eval_exp(value); // получить начальное значение
, j
,fcj variables[stckvar.var] = value;
,-J get_token();
5 if(tok!=TO) serror(TO_EXP); // прочитать и отбросить ТО
~\
i
i ■ '
i
eval_exp(stckvar.target); // получить конечное значение
/* Если цикл может исполниться хотя бы раз,
затолкнуть информацию в стек */
if(value >= variables[stckvar.var]) {
stckvar.loc = prog;
fpush(stckvar);
}
else //в противном случае пропустить весь цикл
while(tok!=NEXT) get_token();
Vr'// Исполнить оператор NEXT.
- Ivoid next()
i struct for_stack stckvar;
stckvar - fpop(); // прочитать информацию цикла
К
variables[stckvar.var]++; // приращение управляющей переменной
// если закончено, возврат
if(variables[stckvar.var] > stckvar.target) return;
] fpush(stckvar); // иначе вернуть информацию в стек
.(. j prog = stckvar.loc; // повторить
1 ')
• i
\
V // Затолкнуть в стек FOR.
Jvoid fpush(struct for stack stckvar)
; •<
£ Л if (ftOS—FOR_NEST)
ц . serror(TOO_MNY_FOR);
* | fstack[ftos] = stckvar;
■ ftos++;
• ;>
i
а--// Вытолкнуть из стека FOR.
538
Глава U
j f i
struct for_stack £pop()
4
w] if(ftos==0)
1 serror (NEXT__WO_FOR) ;
^ ftos—;
t return(fstack[ftos]);
,. i)
l
V/ Исполнить простой вариант команды INPUT.
Void input (>
- Ч
, J char var;
'| int i;
i
9
get_token(); // есть ли строка подсказки?
r. J if(token_type==QUOTE) {
y. cout « token; // если да, вывести и найти запятую
V I get_token();
: Л if(*token != ',') serror(SERROR);
t, '• get token () ;
i >
I else cout « "? "; // в противном случае вывести "?"
I var = toupper(*token)-'A*; // получить переменную
1
cin > i; // прочитать ввод
variables[var] = i; // сохранить
\fI Исполнить команду GOSUB.
void gosub()
Л
char *loc;
■ get_token();
-, // найти вызываемую метку
| loc = find_label(token);
j if(loc==NULL)
i serror(UNDEF_IAB); // метка не определена
., else {
gpush(prog); // сохранить точку возврата
. J prog - loc; // начать исполнение с loc
)
'}
i"
■л
'- 1// Возврат из GOSUB.
4-4void greturn()
' J prog m gpop() ;
4) ■
j
i
\if Затолкнуть в стек GOSUB.
4void gpush(char *s)
, Л if(gtos==SUB_NEST)
Реализация языковых интерпретаторов на C++
539
i serror(TOO_MNY_GOSUB);
,Гл gstack[gtos] - s;
- 's gtos++;
л >
*'«// Вытолкнуть из стека GOSUB.
J_ char *gpop()
г !<
*■'' if (gtos==0)
'. * j serror (RET_W0_GOSUB) ;
iч gtos--;
^4 return (gstack [gtos]) ;
i ■!*>_ _ __^^_____ ___
| ПРИМЕЧАНИЯ
Поскольку код интерпретатора Small BASIC довольно длинен, мы
рассмотрим каждый из его разделов по отдельности.
Ключевые слова
Как говорилось в начале главы, Small BASIC интерпретирует небольшое
подмножество языка BASIC. Вот допустимые ключевые слова:
PRINT
INPUT
IF
THEN
FOR
NEXT
TO
GOTO
GOSUB
RETURN
END
Внутреннее представление этих команд, плюс EOL для конца строки и
FINISHED для конца программы, определяются в показанном ниже перечислении:
enum SBtokensT { UNKNCOM, PRINT, INPUT, IF, THEN, FOR, NEXT, TO,
GOTO, GOSUB, RETURN, EOL, FINISHED, END );
Обратите внимание, что SBtokensT начинается с UNKNCOM. Это значение
возвращается функцией look__up() (описанной чуть ниже) для индикации
неопознанной команды.
Для преобразования внешнего представления лексем во внутреннюю форму
в массиве структур с именем table хранится как внешний, так и внутренний
их формат:
// таблица поиска ключевых слов
struct commands {
char command[20]; // текстовая форма
540
Глава 14
SBtokensT tok; // внутреннее представление
} table ЕЗ = { // Команды в этой таблице должны быть
"print", PRINT, //в нижнем регистре,
"input", INPUT,
"if", IF,
"then", THEN,
"goto", GOTO,
"for", FOR,
"next", NEXT,
"to", TO,
"gosub", GOSUB,
"return", RETURN,
"end", END,
"", END // отмечает конец таблицы
};
Функция look__up(), показанная далее, производит поиск в этой таблице и
возвращает либо внутреннее представление лексемы» либо UNKNCOM, если
соответствия не найдено. (Эта функция находится в файле синтаксического
анализатора.)
/* Отыскать внутреннее представление лексемы
в таблице лексем.
*/
SBtokensT look_up(char *s)
i
register int i;
char *p;
// преобразовать в нижний регистр
р = s;
while(*p){
*р = tolower(*p);
Р++;
}
// проверить, имеется ли лексема в таблице
for(i=0; *table[i].command; i++)
if(!strcmp(table[i].command, s))
return table[i].tok;
return UNKNCOM; // неизвестная команда
Загрузка программы
В интерпретаторе Small BASIC нет встроенного редактора. Вы должны
создать программу с помощью стандартного текстового редактора. Когда Small
BASIC начинает свою работу, программа считывается из файла, а затем
начинается ее исполнение интерпретатором. Загружает программу функция с
именем load_program().
// Загрузить программу.
bool load_program(char *р, char *fname)
{
ifstream in(fname, ios::in | ios:rbinary);
int i=0;
Реализация языковых интерпретаторов на C++ 541
if(!in) {
cout « "File not found
cout « "— be sure to specify .HAS extension.\n";
return false;
}
i = 0;
do {
*p = in.get();
p++; i++;
} while(!in.eof() && i<PROG_SIZE);
// закончить текст программы нулем
if(*(p-2)==0xla) *(p-2) = ' \0'; // отбросить маркер eof
else *(p-l) * '\0';
in.close();
return true;
}
Как следует из комментариев, функция отбрасывает маркер EOF, если он
имеется в файле. Как вы, возможно, знаете, некоторые редакторы записывают
в файл такой маркер; другие — не записывают. Функция load_program()
учитывает обе возможности.
Главный цикл
Во всех интерпретаторах имеется цикл верхнего уровня, который читает
последовательные лексемы программы и предпринимает соответствующие
действия для их обработки. Интерпретатор Small BASIC не является
исключением. Главный цикл интерпретатора выглядит так:
do {
token__type - get_token () ;
// проверить на оператор присваивания
i f (token_type=VARIABLE) {
putback(); // вернуть переменную во входной поток
assignment(); // должен быть оператором присваивания
}
else // команда
switch(tok) {
case PRINT:
print();
break;
case GOTO:
exec_goto{);
break;
case IF:
exec_if () ;
break;
case FOR:
exec_for{);
break;
case NEXT:
next (J ;
break;
542
Глава Ц
case INPUT:
input();
break;
case GOSUB:
gosub();
break;
case RETURN:
greturn() ;
break;
case END:
return 0;
}
} while (tok ' = FINISHED);
Сначала из программы считывается лексема. В предположении, что нет
никаких синтаксических ошибок, прочитанная переменная означает, что
данный оператор — присваивание. В противном случае лексема должна быть
командой, и для нее выбирается подходящий вариант case в зависимости от
значения tok. Давайте посмотрим, как работает каждая команда.
Функция присваивания
В BASIC присваивание не операция, а оператор. Общая форма оператора
присваивания в BASIC следующая:
имя-переменной = выражение
Оператор присваивания интерпретируется функцией assignment):
// Присвоить значение переменной.
void assignment()
{
int var, value;
// получить имя переменной
get_token();
if(!isalpha{*token)) {
serror(NOT_VAR);
return;
}
// преобразовать в индекс массива переменных
var = toupper(*token)-rAr;
// извлечь знак равенства
get_token();
if(*token •= *=') {
serror(EQUAL_EXP);
return;
}
// получить присваиваемое значение
eval_exp(value);
// присвоить значение
variables[var] = value;
}
Реализация языковых интерпретаторов на C++ 543
Первым делом assignment^) считывает из программы лексему. Это будет
переменная, которой присваивается значение. Если лексема не является
действительной переменной, выдается сообщение об ошибке. Затем читается знак
равенства и вызывается eval_exp() для вычисления присваиваемого значения.
Наконец, полученное значение присваивается переменной. Функция
assignment) на удивление проста и прозрачна, поскольку основной объем «грязной»
работы берут на себя синтаксический анализатор и функция get_token().
Команда PRINT
В языке BASIC команда PRINT является на самом деле довольно мощной и
гибкой. Хотя создание функции, полностью реализующей возможности
команды PRINT, выходит за рамки этой главы, разработанная здесь функция
воплощает наиболее существенные черты этой команды. Общей формой
команды PRINT в BASIC является
PRINT список-аргументов
где список-аргументов представлен списком выражений и заключенных в
кавычки строк, отделенных друг от друга запятыми или точками с запятой.
Показанная ниже функция print() интерпретирует команду PRINT.
// Исполнить простой вариант оператора PRINT языка BASIC,
void print()
i
int result;
int len=0, spaces;
char last_delim, str[80];
do {
get_token(); // получить следующий элемент списка
if(tok-=EOL || tok—FINISHED) break;
if (token_type=QUOTE) { // строка
cout « token;
len +- strlen(token);
get_token();
}
else { // выражение
putbacM) ;
eval_exp(result);
get_token();
cout « result;
itoa(result, str, 10);
len += strlen(str); // сохранить длину
}
last_delim = *token;
// если запятая, перейти к следующей позиции табуляции
if(*token == •,') {
// вычислить число пробелов до следующей табуляции
spaces = 8 - (len % 8);
len += spaces; // добавить полученное число пробелов
while(spaces) {
cout « " ";
spaces--;
544
Глава 14
}
У
else if(*token==';') {
cout « " ";
len++;
}
else if{tok!=EOL && tok!=FINISHED) serror(SERROR);
} while (* token—* ;' | | *token==' , ■) ;
if(tok=EOL || tok—FINISHED) {
if(last_delim != •;' && last_delim !=',')
cout « endl;
}
else serror(SERROR);
}
Команда PRINT может использоваться для вывода на экран списка
переменных и заключенных кавычки строк. Если один элемент списка отделяется
от следующего точкой с запятой, то между ними выводится один пробел. Если
же два элемента отделяет друг от друга запятая, второй элемент будет выведен
в следующей позиции табуляции. Если список оканчивается запятой или
точкой с запятой, перевод строки не производится. Вот примеры корректных
операторов PRINT:
PRINT X; Y; "THIS IS A STRING"
PRINT 10/4
PRINT
Последний оператор просто переводит строку.
Обратите внимание, что print() вызывает функцию putback(), чтобы
вернуть лексему во входной поток, потому что print() должна заглянуть вперед и
определить, является ли следующий печатаемый элемент строкой или же
численным выражением. Если верно последнее, то первый член выражения
необходимо вернуть в поток, чтобы анализатор выражений смог корректно
оценить его значение.
Команда INPUT
В языке BASIC для чтения информации с клавиатуры в переменную
используется команда INPUT. Она имеет две общих формы. Первая из них,
INPUT имя-переменной
выводит вопросительый знак и ожидает ввода. Вторая форма,
INPUT "строка-подсказки" имя-переменной
выводит подсказку-запрос и также ожидает ввода значения. Следующая
функция, input(), реализует команду INPUT.
// Исполнить простой вариант команды INPOT.
void input()
<
char var;
int i;
get_token(); // есть ли строка подсказки?
Реализация языковых интерпретаторов на C++ 545
if(token_type==QUOTE) {
cout « token; // если да, вывести и найти запятую
get_token();
if(*token •= ' ,') serror(SERROR);
get_token();
}
else cout « "? "; // в противном случае вывести "?"
var = toupper(*token)-'A'; // получить переменную
cin > i; // прочитать ввод
variables[var] = i; // сохранить
}
Действия этой функции достаточно прямолинейны и понятны из
комментариев.
Команда GOTO
В стандартном BASIC наиболее важной формой программного управления
является непритязательная команда GOTO. Целевым объектом GOTO должен
быть номер строки; этот традиционный подход сохраняется в Small BASIC.
Однако в Small BASIC не требуется, чтобы каждая строка имела свой номер;
номер нужен только в случае, если данная строка будет целевой строкой GOTO.
Общая форма GOTO:
GOTO номер-строки
Главная трудность в реализации GOTO связана с тем, что допускаются
переходы как вперед, так и назад. Такое условие по сути требует, чтобы еще до
исполнения вся программа была просканирована и местоположение каждой
метки было зафиксировано в таблице. После этого всякий раз, когда исполняется
GOTO, положение целевой строки можно отыскать по таблице и передать
управление в эту точку. Таблица, в которой хранятся метки, определяется так:
// таблица поиска меток
struct label {
char name[LAB_LEN]; // метка
char *p; // указывает на позицию метки в исходном файле
} label_table[NUM_LAB];
Процедура, сканирующая программу и записывающая в таблицу каждую
найденную метку, называется scan_labels() и приводится здесь вместе с
несколькими вспомогательными функциями:
// Найти все метки,
void scan_labels()
i
int addr;
char *temp;
label_init(); // обнулить все метки
temp = prog; // сохранить указатель программы
// если первая лексема файла - метка
get_token{);
18 Зек. 1208
if (token_type==NUMBER) {
strcpy (label_table[0] .name, token) ;
label_table[0].p = prog;
}
find_eol();
do (
get_token{) ;
if(token_type==NUMBER) {
addr = get_next_label(token);
if(addr = -1 || addr =- -2) {
(addr — -1) ? serror(IAB_TAB_FULL) : serror(DUP_LAB);
)
strcpy(label_table[addr].name, token);
// записать текущую позицию указателя
label_table[addr].p = prog;
// если не на пустой строке, найти следующую строку
if(tok!=E0L) find_eol();
} while(tok!=FINISHED);
prog - temp; // восстановить исходную позицию
}
// Найти начало следующей строки.
void find_eol()
{
while(*ргод!='\гГ && *prog!=*\0') ++prog;
if(*prog) prog++;
}
/* Возвратить индекс первой свободной ячейки массива меток.
-1 возвращается, если массив заполнен.
-2 возвращается при дублировании меток.
*/
int get_next_label(char *s)
{
register int i;
for(i«0; i<N0M_LAB; ++i) {
if(label_table[i].name[0]==0) return i;
if(!strcmp(label_table[i].name, s)) return -2;
)
return -1;
}
/* Найти местоположение данной метки. Если не найдена,
возвращается нуль; в противном случае возвращается
указатель на позицию метки.
*/
char *find_label(char *s)
{
Реализация языковых интерпретаторов на C++ 547
register int i;
for(i=0; i<NUM_LAB; ++i)
if('strcmp(label__table[i].name, s))
return label_table[i].p;
return 0; // ошибка
}
/* Инициализировать массив меток.
Предполагается, что нулевое имя метки означаетf
что ячейка массива не занята.
*/
void label_init()
{
register int i;
for(i=0; i<NUM_LAB; ++i)
label_table[i].name[0] = 0;
}
Функция scan_labels() может сообщать о двух типах ошибок. Первый —
дублирование метки. В программе BASIC (как и в большинстве других
языков) не может быть двух одинаковых меток. Второй ошибкой является
переполнение таблицы меток. Размер ее задается константой NUM_LAB, для
которой вы можете установить любое желаемое значение.
Когда таблица меток построена, исполнить команду GOTO становится
совсем просто, как показывает функция exec_goto():
// Исполнить оператор GOTO.
void exec_goto()
{
char *loc;
get_token(); // извлечь метку перехода
// найти местоположение метки
loc = find_label(token);
if(loc==NULL)
serror(UNDEF_LAB); // метка не определена
else prog = loc; // качать исполнение программы с loc
}
Вспомогательная функция find__label() ищет метку в таблице и возвращает
указатель не нее. Если метка не найдена, возвращается нуль — который
никогда не может быть действительным указателем, если указатель не нуль, он
присваивается глобальной prog, в результате чего исполнение программы
продолжается с позиции метки. (Как вы помните, prog является указателем на
точку, в которой в данный момент происходит исполнение программы.) Если
метка не найдена, выдается сообщение о неопределенной метке.
Оператор IF
Интерпретатор Small BASIC реализует подмножество оператора IF
стандартного BASIC. В Small BASIC не допускается применение ELSE. (Однако
ввести в интерпретатор это ключевое слово будет несложно, если вы разберетесь в
том, как работает IF.) Общая форма оператора IF имеет такой вид:
18*
548
Глава 14
IF выражение операция-отношения выражение THEN оператор
Оператор, следующий за THEN, исполняется только в случае, если выражение
отношения истинно. Следующая функция, exec_if(), исполняет оператор IF:
// Исполнить оператор IF.
void exec_i f()
{
int result;
eval_exp(result) ; // получить значение выражения
if(result) { // если true, обработать целевой оператор IF
get_token ();
if(tok!=THEN) {
serror(THEN_EXP);
return;
} // если нет ошибки, целевой оператор исполняется
}
else find_eol(); // найти начало следующей строки
}
Функция exec_if() работает так. Прежде всего вычисляется значение
относительного выражения. Если выражение истинно, исполняется целевой
оператор; в противном случае find_eol() ищет начало следующей строки. Заметьте,
что при истинном выражении exec_if() просто возвращает управление. Это
приводит к следующей итерации главного цикла и чтению следующей
лексемы. Поскольку целевым объектом IF является оператор, возврат к главному
циклу означает выполнение этого оператора, как если бы он стоял на
отдельной строчке. Если же выражение ложно, то перед возвратом в главный цикл
происходит переход к следующей строке.
Цикл FOR
Реализация цикла FOR в BASIC представляет собой трудную задачу,
которая, однако, поддается довольно элегантному решению. Общей формой цикла
FOR является
FOR управляющая-переменная = начальное-значение ТО конечное-значение
NEXT
Версия цикла в Small BASIC реализует только циклы, в которых при
каждом проходе управляющая переменная увеличивается на единицу; команда
STEP не поддерживается.
В языке BASIC, как и в C++, возможны многократно вложенные циклы.
Главная трудность при этом заключается в том, что необходимо корректным
образом хранить информацию каждого из циклов (другими словами, каждый
NEXT должен ставиться в соответствие правильному FOR). Решением этой
проблемы является реализация механизма FOR на основе стека.
В верхней части цикла информация о состоянии управляющей переменной,
конечном значении и положении вершины цикла заталкивается в стек. Вся-
Реализация языковых интерпретаторов на C++ 549
кий раз, когда встречается NEXT, эта информация выталкивается из стека,
управляющая переменная модифицируется и сравнивается с конечным
значением. Если управляющее значение превосходит конечное, цикл заканчивается
и исполнение продолжается со строки, следующей за оператором NEXT.
В противном случае модифицированная информация снова заталкивается в
стек и исполнение возобновляется с вершины цикла.
Такая реализация цикла FOR работает не только с одиночными, но и с
вложенными циклами, так как самый внутренний оператор NEXT всегда будет
ассоциирован с самым внутренним FOR. (Информация, заталкиваемая в стек
последней, будет вытолкнута первой.) Когда же внутренний цикл
завершается, его информация выталкивается из стека и на верху стека оказывается
информация внешнего цикла (если он есть). Таким образом, следующий NEXT
будет также ассоциирован с нужным FOR.
Для поддержки цикла FOR нужно создать стек, хранящий информацию
циклов, как показано ниже.
// поддержка циклов FOR
struct for_stack {
int var; // счетчик
int target; // конечное значение
char *loc; // позиция начала повторения в исходном коде
) fstack[FOR_NEST]; // стек для вложенных циклов FOR/NEXT
Значение FOR_NEST определяет, насколько глубоко могут быть вложены
циклы FOR (25 уровней обычно более чем достаточно).
Стеком FOR управляют две функции fpush() и fpop(), показанные ниже.
// Затолкнуть в стек FOR.
void fpush(struct for_stack stckvar)
{
if (f tos—FOR_NEST)
serror(TOO_MNY_FOR);
£stack[£tos] = stckvar;
ftos++;
}
// Вытолкнуть из стека FOR.
struct for_stack fpop()
i
if(ftos==0)
serror(NEXT_WO_FOR);
ftos--;
return(fstack[ftos]);
}
Теперь, когда необходимая поддержка обеспечена, функции исполнения
операторов FOR и NEXT могут быть реализованы следующим образом:
// Исполнить цикл FOR.
void exec_for()
t
struct for__stack stckvar;
int value;
get_token(); // прочитать управляющую переменную
550
Глава 14
if(!isalpha(*token)) {
serror(NOT_VAR);
return;
)
// сохранить индекс управляющей переменной
stckvar.var = toupper(*token)-'A';
get_token(); // прочитать знак равенства
if(*token != ' = ') {
serror(EQUAL_EXP);
return;
}
eval_exp(value); // получить начальное значение
variables[stckvar.var] = value;
get_token();
if(tok!=TO) serror(TO_EXP); // прочитать и отбросить ТО
eval_exp(stckvar.target); // получить конечное значение
/* Если цикл может исполниться хотя бы раз,
затолкнуть информацию в стек */
if(value >= variables[stckvar.var]) {
stckvar.loc = prog;
fpush(stckvar);
}
else // в противном случае пропустить весь цикл
while(tok!=NEXT) get_token();
}
// Исполнить оператор NEXT.
void next()
i
struct for_stack stckvar;
stckvar = fpop(); // прочитать информацию цикла
variables[stckvar.var]++; // приращение управляющей переменной
// если закончено, возврат
if (variables[stckvar.var] > stckvar.target) return;
fpush(stckvar); // иначе вернуть информацию в стек
prog = stckvar.loc; // повторить
>
Вы сумеете сами проследить действия этих процедур, прочитав
комментарии. Данная реализация не запрещает выходить из цикла FOR с помощью
GOTO. Однако такой переход нарушит структуру стека, и его следует избегать.
Решение проблемы цикла FOR с применением стека может быть
распространено на все остальные циклы. Хотя Small BASIC никаких иных
операторов цикла не реализует, вы можете применить подобные процедуры для
любого типа циклов, включая WHILE и DO / WHILE. К тому же, как вы увидите в
следующем параграфе, решение на основе стека годится для любого элемента
языка, допускающего вложение, в том числе для вызова подпрограмм.
Реализация языковых интерпретаторов на C++
551
GOSUB
Хотя Small BASIC не поддерживает истинно автономные подпрограммы, в
нем можно вызывать отдельные части программы с возвратом в точку вызова,
используя операторы GOSUB и RETURN. Общей формой GOSUB / RETURN
является
GOSUB номер-строки
номер-строки
код подпрограммы
RETURN
Вы зов подпрограммы, даже в такой редуцированной форме, какая
реализована в BASIC, требует наличия стека. Причина этого та же, что и в случае
циклов FOR: возможность вложенных вызовов подпрограмм. Поскольку одна
подпрограмма может вызвать другую, стек необходим для гарантии того, что
каждый оператор RETURN будет ассоциирован с правильным GOSUB. Стек
GOSUB определяется следующим образом:
char *gstack[SUB_NEST]; // стек для GOSUB
Как видите, gstack представляет собой просто массив указателей на
символ. Он хранит положение в программе той точки, куда нужно возвратить
управление по завершении подпрограммы.
Вот функция gosub() и ее вспомогательные процедуры:
// Исполнить команду GOSUB.
void gosub()
{
char *loc;
get_token();
// найти вызываемую метку
loc — find_label(token);
if (loc—NULL)
serror(UNDEF_LAB); // метка не определена
else {
gpush(prog); // сохранить точку возврата
prog = loc; // качать исполнение с loc
}
}
// Возврат из GOSUB.
void greturn()
{
prog = gpop() ;
)
// Затолкнуть в стек GOSUB.
void gpush(char *s)
{
552
Глава 14
if(gtos==SUB_KEST)
serror(TOO_MNY_GOSUB);
gstack[gtos] = s;
gtos++;
}
// Вытолкнуть иэ стека GOSUB.
char *gpop()
{
if(gtos==0)
serror(RET_WO_GOSUB);
gtos—;
return(gstack[gtos]);
}
Команда GOSUB работает так: если встречается GOSUB, текущее значение
prog заталкивается в стек. (Это та точка программы, в которую
подпрограмма вернет управление по своем завершении.) Ищется целевой номер строки,
и связанный с ним адрес присваивается prog. Это приводит к тому, что
исполнение продолжается с начала подпрограммы. Когда встречается оператор
RETURN, prog присваивается значение, вытолкнутое из стека GOSUB, и
исполнение программы возобновляется со строки, следующей за оператором
GOSUB. Поскольку адрес возврата хранится в стеке, допустимы вложенные
вызовы подпрограмм. В каждом случае подпрограмма, вызванная последней,
будет той, что возвратит управление при обнаружении оператора RETURN.
(Т. е. адрес возврата последней вызванной подпрограммы находится на
вершине стека.) Такой процесс допускает сколь угодно глубоко вложенные вызовы
GOSUB.
Работа со Small BASIC
Здесь приводятся образцы программ, которые может исполнять Small
BASIC. Обратите внимание, что ключевые слова могут записываться как в
верхнем, так и в нижнем регистре. Вы, наверно, захотите написать какие-то
свои программы. Попробуйте также исполнять программы с синтаксическими
ошибками и посмотрите, как Small BASIC будет о них сообщать.
Следующая программа исполняет все команды, имеющиеся в Small BASIC.
Файл программы называется TEST1.BAS.
PRINT "This program demonstrates all commands."
FOR X = 1 TO 100
PRINT X; X/2, X; X*X
NEXT
GOSUB 300
PRINT "hello"
INPUT H
IF H<11 THEN GOTO 200
PRINT 12-4/2
PRINT 100
200 A = 100/2
IF A>10 THEN PRINT "this is ok"
PRINT A
Реализация языковых интерпретаторов на C++
553
PRINT A+34
INPUT H
PRINT H
INPUT "this is a test ",y
PRINT H+Y
END
300 PRINT "this is a subroutine"
RETURN
В предположении, что интерпретатор называется SBASIC, для запуска
программы вам нужно выполнить такую командную строку:
SBASIC TEST1.BAS
Small BASIC автоматически загрузит программу и приступит к ее
исполнению.
Следующая программа демонстрирует вложенные подпрограммы:
PRINT "This program demonstrates nested GOSUBs."
INPUT "enter a number: ", I
GOSUB 100
END
100 FOR T = 1 TO I
X = X + I
GOSUB 150
NEXT
RETURN
150 PRINT X;
RETURN
Эта программа иллюстрирует работу команды INPUT:
print "This program computes the volume of a cube."
input "Enter length of first side ", 1
input "Enter length of second side ", w
input "Enter length of third side ", d
t = 1 * w * d
print "Volume is ", t
Следующая демонстрирует вложенные циклы FOR:
PRINT "This program demonstrates nested FOR loops."
FOR X = 1 TO 100
FOR Y = 1 TO 10
PRINT X; Y; X*Y
NEXT
NEXT
Наконец, последняя программа проверяет работу всех операций отношения:
PRINT "This demonstrates all of the relational operators."
A = 10
В ~ 20
IF A = В THEN PRINT "A - B"
IF А О В THEN PRINT "А О В"
IF A < В THEN PRINT "A < B"
554
Глава 14
IF A > В THEN PRINT "A > В"
IF А >= В THEN PRINT "A >= В"
IF А <= В THEN PRINT "A <= В"
Усовершенствование и расширение
интерпретатора
Вводить в интерпретатор Small BASIC новые команды очень легко.
Следуйте просто общему формату команд, представленных в этой главе. Чтобы ввести
новые типы переменных, нужно организовать массив структур для хранения
переменных; одно поле структуры будет специфицировать тип, а другое
хранить значение переменной. Чтобы реализовать строки, нужно организовать
таблицу строк. Для поддержки строк можно использовать класс C++ string.
Это позволит буквально переводить операции над строками в BASIC в
соответствующие операции класса string.
И еще одно замечание. В том виде, как они здесь написаны, различные
перечисления и константы просто дублируются в обоих файлах интерпретатора.
Это удобно для представления в книге. Но если вы будете усовершенствовать и
расширять этот интерпретатор, то целесообразно поместить такого рода
объявления в заголовочный файл и включить последний во все модули программы.
Это не только упростит работу с проектом возросшего объема, но и позволит
избежать рассогласования исходных файлов.
Создание своего собственного
компьютерного языка
Модификация или расширение Small BASIC является хорошим способом
получше познакомиться с его работой и с тем, как вообще функционируют
языковые интерпретаторы, но ничто не ограничивает ваши возможности
одним только языком BASIC. Вы можете воспользоваться теми же методиками,
что описывались в этой главе, и написать интерпретатор практически для
любого языка программирования. Вы можете даже изобрести свой собственный
язык, отражающий ваш стиль программирования и вашу индивидуальность.
На самом деле, скелет интерпретатора Small BASIC представляет собой
отличный «испытательный стенд» для любого рода элементов специального языка,
которые вы захотите реализовать.
Например, чтобы ввести в интерпретатор цикл REPEAT / UNTIL, нужно
выполнить следующие четыре операции:
1. Добавьте к перечислению SBtokensT REPEAT и UNTIL.
2. Добавьте REPEAT и UNTIL в таблицу commands.
3. Введите варианты REPEAT и UNTIL в оператор switch главного цикла.
4. Определите функции repeat() и until(), которые будут обрабатывать эти
команды. (В качестве отправной точки возьмите exec_for() и ncxt().)
И последнее. Разнообразие операторов, которые можно интерпретировать,
ограничивается только вашим воображением. Не бойтесь экспериментировать.
t/£f
. \.: f .
■к--Ж
ГЛАВА
«w-
JTtawra
-* -■.-* _ • * * *
■4fl '
Эндрю Гейтер
sort.cpp
partial_.sort.cpp
sortjieap.cpp
user_sort1.cpp
user.sort2.cpp
findkcpp
binaryjearch.cpp
functors.cpp
io.cpp
iofile.cpp
string_compare.cpp
setjntersection.cpp
priority.queue.cpp
binaryJree.cpp
у &
Si>.
\ 4 ? •?
# ^ *■
A. *"*
"u.t| .- ,
--Л
556
Глава 15
В этой главе мы будем исследовать одну из важнейших подсистем языка
C++ — Библиотеку стандартных шаблонов, или STL (Standard Template
Library). STL состоит из стандартизованных контейнеров, итераторов,
алгоритмов и функциональных объектов, образующих в совокупности мощный
инструментарий из обобщенных блоков, на основе которых можно строить
конкретные приложения.
Главным достоинством STL является то, что она предоставляет программисту
в готовом виде самые распространенные алгоритмы и структуры данных, обеспе-
чивая к тому же автоматическое управление памятью динамических структур.
Ему больше не приходится каждый раз заново «изобретать велосипед».
Объектно-ориентированная модель поощряет повторное использование
кода посредством развертывания существующих классов и предопределенных
функциональных свойств. STL — идеальный кандидат для создания
приложений, в которых «утилизация» кода ставится превыше всего. Она поможет
снизить стоимость разработок и сократить их временной цикл — немаловажное
преимущество в условиях давления рынка, требующего частых обновлений
программных продуктов.
Образцы программ этой главы были разработаны и написаны для
демонстрации возможных применений STL для решения некоторых стандартных, а
равно и сложных, программных задач. Примеры охватывают такие области,
как сортировка, поиск, действия над множествами и потоковый ввод/вывод.
Вы также увидите, как реализуется контейнер специального типа.
[ ЗАМЕЧАНИЕ ПРОГРАММИСТА
Примеры из этой главы были разработаны на основе
Microsoft-реализации STL. Когда это возможно, следует пользоваться самой последней
версией компилятора Microsoft, поскольку в него все время вносятся
улучшения и исправления.
В некоторых примерах используется директива #pragma для
отключения ряда предупреждений, генерируемых компилятором. Если
хотите, эти директивы можно спокойно убрать без каких-либо вредных
последствий.
Обзор библиотеки стандартных шаблонов
Материал этой главы предполагает, что вы имеете по крайней мере
элементарное представление об STL и ее стандартном наборе обобщенных
контейнеров, алгоритмов и функций. Учитывая размер библиотеки, детальное
описание STL выходит далеко за рамки отдельной главы. Далее мы приводим обзор
главных элементов STL в качестве краткого «повторения пройденного»,
которое будет полезно всем разработчикам.
Контейнеры
Контейнеры STL — это основные строительные блоки библиотеки
шаблонов. Это не означает, однако, что для любого применения STL вы обязаны оп-
Исследование библиотеки стандартных шаблонов 557
ределять свои собственные контейнеры. Это далеко не так. В STL
предусмотрен богатый набор встроенных контейнеров с существенной функциональной
поддержкой, которые можно вводить в новые или уже существующие
приложения. Кроме того, во многих случаях STL предусматривает
высокоэффективные алгоритмы, избавляя вас от необходимости писать свои собственные.
Для хранения данных в STL имеется ряд контейнеров, реализующих
многие традиционные структуры вроде массивов и списков. Вот краткая сводка
контейнеров, поддерживаемых библиотекой:
Контейнер STL
vector
deque
list
map
multimap
set
multiset
queue
stack
priority_queue
Описание
Динамический массив
Двусторонняя очередь
Двунаправленный линейный список
Ассоциативный контейнер с уникальными ключами
Ассоциативный контейнер, допускающий дублирование ключей
Ассоциативный контейнер для наборов уникальных элементов
Ассоциативный контейнер для наборов с дублированием элементов
Стандартная очередь
Стандартный стек
Приоритетная очередь
Каждый контейнер предназначен для решения специфических задач, и
разработчик должен внимательно проанализировать разные контейнеры, чтобы
быть уверенным в правильности сделанного выбора. Наиболее часто
используются вектора, списки, deque и карты.
Одним из простейших контейнеров STL является vector. Его можно
сравнить с традиционным массивом. Однако вектор имеет немало преимуществ,
делающих его идеальным средством для хранения последовательностей
данных. Вектор является динамическим — по мере добавления новых элементов
его размер автоматически возрастает. Хранящиеся в векторе данные можно
сортировать, копировать, перемещать и модифицировать либо посредством
алгоритмов STL, либо применяя специальные алгоритмы пользователя.
Контейнер list обладает большой гибкостью и поддерживает такие
операции, как splice(), merge() и unique().
Контейнер deque (double-ended queue) реализует двустороннюю очередь.
Ассоциативные контейнеры, такие, как тар, идеально подходят для
хранения данных, с которыми ассоциированы ключевые значения.
Итераторы
К контейнерам можно применять самые разные алгоритмы STL, так как ее
итераторы обеспечивают единообразный интерфейс для доступа к данным
различных контейнеров. Итераторы напоминают указатели C++ и могут
использоваться для перебора и доступа к данным контейнера. Итераторы делят-
558
Глава 15
ся на несколько категорий в зависимости от операций, которые они позволяют
выполнять:
♦ Выходные
♦ Входные
♦ Поступательные
♦ Двунаправленные
♦ Произвольного доступа
Например, поступательный итератор допускает применение только
операции ++, в то время как к двунаправленному итератору можно применять как ++,
так и --. Итераторы произвольного доступа наиболее полезны, так как они
предусматривают полный набор операций итерации и сравнения. Различные
контейнеры STL поддерживают разные типы итераторов. Например,
итераторы векторов и deque — произвольного доступа, а контейнеры списков и карт
поддерживают только двунаправленные итераторы.
В этой главе мы будем пользоваться следующими условными именами для
обозначения различных типов итераторов:
Имя
Ranlt
Inputlterator
Fwd!t
Outlt
Bidlt
Тип итератора
Итератор произвольного доступа
Входной итератор
Поступательный итератор
Выходной итератор
Двунаправленный итератор
Алгоритмы
В STL реализовано около 60-ти алгоритмов. Они предоставляют
программисту быстрые и удобные способы управления, сортировки и поиска в наборах
данных.
Многие алгоритмы способны производить с данными весьма сложные
манипуляции. Например, алгоритм разбиения может «рассечь» данные контейнера
в соответствии с критерием «расположить все элементы, большие х, правее
всех элементов, которые меньше хъ.
Функциональные объекты, определяемые
пользователем
STL позволяет разработчику определять, в дополнение к стандартным,
пользовательские функциональные объекты. Функциональный объект может
быть полезен при переборе элементов контейнера. Обычные алгоритмы не
сохраняют информацию между последовательными вызовами (если только не
использовать статические переменные). Функциональный объект, однако,
может хранить состояние между вызовами в своих элем; нтах данных. Это
полезно, когда для некоторой последовательности нужно вычислить общий
результат (как при нахождении общей суммы).
Исследование библиотеки стандартных шаблонов
559
Хотя теория STL интересна и сама по себе, ее применение на практике для
решения распространенных программных задач гораздо полезнее. Многие из
показанных далее образцов программ можно комбинировать и
модифицировать в зависимости от текущих потребностей. Не удивляйтесь, если простое
применение некоторого алгоритма или функционального объекта
обеспечивает вашей программе такие возможности, на разработку которых вы сами
потратили несколько недель. STL предлагает вам обобщенные решения самых
разнообразных программных задач.
Сортировки STL
Для сортировки данных контейнеров в STL имеются различные алгоритмы.
Три из них — sort(), partial_sort() и sort_heap() — демонстрируются в
следующих разделах.
Алгоритм сортировки
sort()
partial_sort()
sort_heap()
Описание
Сортировка общего назначения
Сортирует только часть массива
Сортирует накопитель |
Алгоритм sortQ
Первый алгоритм сортировки, который мы рассмотрим — это sort(). Это
самая обычная сортировка, которая выполняется очень просто, единственным
ограничением является то, что ее можно применять только к контейнерам,
поддерживающим итераторы произвольного доступа (например, к векторам).
Вот примерные прототипы алгоритма sort():
template<class Ranlt>
void sort(Ranlt begin, RanIt end);
template<class Ranlt, class Pred>
void sort (Ranlt begin, Ranlt end, Pred pr) ; t
Действия первого варианта sort() достаточно прямолинейны; он использует
operator<() для сортировки данных в восходящем порядке. Второй вариант
заменяет операцию сравнения функцией рг(х, у); тем самым возможно
сортировать данные в порядке, задаваемом вашей собственной функцией сравнения.
Код
Вот пример, демонстрирующий sort() в применении к вектору. Файл
программы называется sort.cpp.
560
Глава 15
/*___
/*
/* STL sort( )
/*
/*
!/*
#include <vector>
#include <iostream>
[ #include <algorithm>
fusing namespace std;
jvoid Print(int x)
{
cout « x « endl;
}
jmt main()
{
vector<int> v(5);
v[0] = 5,
v[l] = 6,
v[2] = 3,
v[3] = 9
v[4] = 7
sort(v.begin(), v.end() );
for_each(v.begin(), v.end(), Print)
return
Сопрограмма выводит:
3
5
6
7
9
[ ПРЕ
ПРИМЕЧАНИЯ
В этом примере в векторе хранится пять целых чисел. Алгоритм sort()
вызывается для их сортировке в восходящем порядке. Для вывода результатов в
cout применяется определенная пользователем функция Print(), которая
передается алгоритму for_each().
Алгоритм for_each() полезен, когда требуется произвести перебор всех
элементов контейнера и выполнить для каждого из них некоторые действия. Вот
прототип for_each():
template<class Inputlterator, class Func>
Func for_each(Inputlterator begin, Inputlterator end, Func fn);
Исследование библиотеки стандартных шаблонов
561
Здесь функция, указываемая fnt применяется ко всем элементам
диапазона, заданного итераторами begin и end. Наш пример вполне можно было бы
написать без for_each(), но алгоритм часто упрощает код.
Для использования предикатного варианта sort() нужно просто
специфицировать в качестве третьего аргумента функциональный объект или указатель
на функцию. Например, чтобы сортировать вектор в обратном порядке,
попробуйте написать (нужно только включить заголовок <functional>):
sort(v.begin(), v.end(), greater<int>());
Здесь мы применили функциональный объект STL greater(), чтобы
продемонстрировать, как просто можно передавать алгоритмам различные
функциональные объекты.
Алгоритм partial_sort()
Другой полезный алгоритм сортировки — это partial_sort(), который
можно применить для «частичной» сортировки последовательности данных. Он
имеет следующий прототип:
template<class Ranlt>
partial_sort(Ranlt begin, Ranlt middle, RanIt end);
Код
Следующий пример демонстрирует частичную сортировку. Исходный файл
называется partial_sort.cpp.
I/* */
К*
/* STL partial_sort( )
/*
I/*
'* ■ */
|#include <vector>
I#include <iostream>
|#include <algorithm>
using namespace std;
jvoid Print(int x)
{
cout « x « endl;
|)
[int main()
{
vector<int> v(10);
v[0] = 5
v£l] = 6
v[2] = 3
v[3] = 9
562
Глава 15
v[4J - 7
v[5] = О
v[6] - 1
v[7] = 12;
v[8] - 2;
v[9] = 4;
partial_sort(v.begin(), v.begin()+4, v.end() );
for_each(v.begin(), v.end(), Print);
return
Сопрограмма выводит;
О
1
2
3
9
7
6
12
5
4
ПРИМЕЧАНИЯ
[ ПРк
В этом примере функция partial_sort() сортирует только первые четыре
элемента вектора: 0, 1, 2, 3.
Функция sortjieap
Функция sort_heap() может вызываться для сортировки существующего
контейнера, преобразованного предварительно в накопитель с помощью
алгоритма make_heap(). Вот прототип sort_heap():
template<class Ranlt>
sort_heap(RanIt begin, Ranlt and);
Накопитель (heap) похож на двоичное дерево; элементы накопителя можно
представить себе в виде узлов дерева. Наибольший элемент накопителя
находится в его начале; распределение остальных элементов, вообще говоря, не
определено. Результат преобразования вектора в накопитель можно увидеть,
распечатав значения вектора после того, как он будет преобразован.
Допустим, например, что мы конструируем целый вектор со значениями 5, 6, 3, 9, 7,
О, 1, 12, 2 и 4, а затем преобразуем его в накопитель. Элементы вектора будут
расположены в последовательности 12, 9, 3, 6, 7, 0, 1, 5, 2, 4.
Исследование библиотеки стандартных шаблонов
563
Код
Вот программа sort_.heap.cpp, которая конструирует накопитель и затем
сортирует его:
|/*
!/*
/* STL sort_heap( )
/*
/*
|/*
#include <vector>
j#include <iostream>
I #include <algorithm>
using namespace std;
[void Print(int x)
{
cout « x « endl;
}
| int main{)
{
vector<int> v(10);
v[0] = 5,
v[l] = 6
v[2] = 3,
v[3] - 9,
v[4] = 7
v[5] - 0
v[6] = 1
v[7] = 12;
v[8] « 2
v[9] = 4
cout « "contents of vector before make_heap "« endl;
for_each(v.begin(), v.end(), Print);
make_heap(v.begin(), v.end());
cout « "contents of vector after make_heap" « endl;
for_each(v.begin(), v.end(), Print);
sort_heap(v.begin(), v.end());
cout « "contents of vector after heap_sort" « endl;
for_each(v.begin(), v.end(), Print);
return 0;
■*/
J ПРИ
ПРИМЕЧАНИЯ
Алгоритм make_heap() преобразует вектор в накопитель. После
преобразования контейнера в накопитель для вставки и удаления его элементов нужно
применять алгоритмы push_heap() и pop_heap().
564
Глава 15
После преобразования вектора для его сортировки вызывается алгоритм
sort_heap(). Сортированный накопитель затем выводится в cout алгоритмом
for_each() с функцией Print().
Благодаря передаче Print() упомянутому алгоритму функция выполняется
«для каждого» элемента вектора.
Сортировка STL-контейнеров с элементами,
определенными пользователем
Операции сортировки в предыдущих примерах довольно просты, поскольку
используют встроенные типы. А что, если контейнер содержит элементы,
определенные пользователем? Эти элементы могут быть объектами класса и
сортироваться в соответствии с любым критерием.
Код
Следующий образец программы показывает, как применить алгоритм sort()
для сортировки контейнера, в котором хранятся элементы определенного
пользователем типа. В примере определяется структура employee,
содержащая различные сведения о служащих: имя, зарплата, отдел и
продолжительность работы.
Файл примера называется user_sortl.cpp.
/* */
/*
/* STL sort( ) для определяемого пользователем типа элементов
/*
/*
.Л
I.
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
Г *
struct employee {
tring name;
salary;
]' '. ! string department;
rr, i int years;
- ] stri
" J int
■bool operator=(const employee &x, const employee &y)
- "* return x.name == y.name;
■ ^bool operator<(const employee fix, const employee &y)
Исследование библиотеки стандартных шаблонов
565
!!'
return x.name < у.name;
void printName(const employee &x)
(
cout « x.name « endl;
)
л
int main()
{
vector<employee> v(5);
- 4
J
. v[0
1 v[0
| v[0
v[0
v[l
v[l
v[l
v[l
v[2
v[2
v[2
v[2
v[3
v[3
v[3
v[3
.name = "Stan";
.salary = 20000;
.department = "admin";
.years = 1;
.name = "Beril";
.salary = 24000;
.department = "support";
.years = 1;
.name = "Tanya";
.salary - 12000;
.department = "typing Pool";
.years = 5;
.name = "Bill";
.salary = 100000;
.department = "director";
.years = 5;
v [4].name = "Monica";
v[4].salary = 50000;
v[4].department = "typing pool";
v[4].years = 1;
sort(v.begin(), v.end());
for_each(v.begin(), v.end(), printName);
return 0;
Вот вывод программы:
Beril
Bill
Monica
Stan
Tanya
566
Глава 15
| ПРУ
ПРИМЕЧАНИЯ
Сначала определяется структура, которая будет хранить информацию о
работниках. Чтобы можно было поместить некоторый элемент в контейнер, он
должен соответствовать определенным требованиям C++. Нужно
предусмотреть операции «равно» и «меньше». Операции для класса employee показаны
ниже; они сравнивают имена работников.
bool operator=={const employee &x, const employee &y)
{
return x.name -- y.name;
}
bool operator<(const employee fix, const employee &y)
{
return x.name < y.name;
}
Программа объявляет вектор структур и записывает в него пять элементов.
Вектор затем сортируется алгоритмом sort(). Операция < определена так, что
сравнивает имена из структуры employee, что и приводит к показанному
результату:
Beril
Bill
Monica
Stan
Tanya
Определяемый пользователем критерий
сортировки
Давайте посмотрим теперь, как выполняется сортировка по критерию,
отличному от «меньше». Вы можете либо определить эту операцию по-другому,
либо написать свою собственную функцию сравнения, передаваемую
алгоритму. Следующий пример показывает реализацию второго способа.
Код
Программа демонстрирует алгоритм sort(), но специфицирует
пользовательскую функцию для сравнения элементов. В данном случае работники
сортируются по зарплате.
Файл называется user_sort2()cpp.
*/
STL sort( ) для определяемого пользователем типа элементов
с пользовательской функцией сравнения.
Исследование библиотеки стандартных шаблонов
567
#include <vector>
j#include <string>
j#include <iostream>
j#include <algorithm>
(usxng namespace std;
struct employee {
string name;
int salary;
string department;
int years;
bool operator=(const employee &x, const employee by)
return x.name == y.name;
bool operator<(const employee &x, const employee &y)
return x.name < y.name;
bool BySalary(const employee &x, const employee &y)
if (x.salary < y.salary) return true-
return false;
void printName(const employee &x)
cout « x.name « endl;
int main()
vector<employee> v(5);
v[0].name = "Stan";
v[0].salary = 20000;
v[0].department = "admin";
v[0].years = 1;
v[l].name = "Beril";
v[l].salary = 24000;
v[l].department = "support";
v[l].years = 1;
v[2].name = "Tanya";
v[2].salary = 12000;
v[2].department = "typing Pool";
v[2].years = 5;
568
Глава 15
i
j
v[3].name = "Bill";
v[3].salary = 100000;
v[3].department = "director";
VE3].years — 5;
v[4].name = "Monica";
v[4].salary = 50000;
v[4].department = "typing pool";
v[4].years = 1;
sort(v.begin(), v.end(), BySalary);
£or_each(v.begin(), v.end(), printName);
return 0;
}
Программа выводит:
Tanya
Stan
Beril
Monica
Bill
ПРИМЕЧАНИЯ
( ПРУ
Вместо алгоритма sort() со сравнением по умолчанию, принимающего два
аргумента, мы используем в примере предикатный его вариант с тремя
аргументами. Третьим аргументом указывается функция-предикат, используемая
для сравнения сохраняемых в контейнере объектов.
Чтобы обеспечить сортировку по зарплате, мы передаем в третьем
параметре алгоритма sort() предикат BySalary(). Он возвращает булево значение,
используемое алгоритмом для упорядочения последовательности элементов:
bool BySalary(const employee fix, const employee fiy)
{
if (x.salary < y.salary) return true;
return false;
}
Пример иллюстрирует простоту определения функций для совместной
работы с алгоритмами STL. Гибкость такого подхода является важным
преимуществом библиотеки стандартных шаблонов.
Поиск в контейнерах STL
Данные, сохраняемые в контейнерах, остаются совершенно бесполезными,
если нужный элемент нельзя быстро и просто отыскать. Ассоциативные
контейнеры, подобные картам, используют ассоциированные с данными
ключевые значения для быстрого извлечения данных. Если же контейнер
организован по-другому и не имеет таких ассоциативных связей, тогда, чтобы извлечь
конкретное значение, в нем придется произвести поиск.
Исследование библиотеки стандартных шаблонов 569
В STL реализованы различные алгоритмы, позволяющие выполнять
простые или сложные виды поиска. Вот некоторые прототипы алгоритмов:
template<class Fwdltl, class Fwdlt2>
Fwdltl search(Fwdltl beginl, Fwdltl endl, Fwdlt2 begin2,
Fwdlt2 end2);
template<class Fwdlt, class Dist, class T>
Fwdlt search_n(Fwdlt begin, Fwdlt end, Dist л, const Tfi val);
Эти два алгоритма могут применяться для поиска в контейнере указанной
подпоследовательности. Алгоритм search() возвращает поступательный
итератор, который «указывает» на найденную в контейнере (определяемом первой
парой итераторов) последовательность значений, заданную второй парой
итераторов. Алгоритм search_n() ищет в контейнере последовательность,
состоящую из идущих подряд п значений val. Если последовательность не найдена,
эти алгоритмы возвращают соответственно endl и end.
template<class InputXterator, class T>
Inputlterator find(Inputlterator begin, InputXterator end,
const T& value);
template<class Inputlterator, class T, class Predicate>
Inputlterator find_if(Inputlterator begin, Inputlterator end,
Predicate pred);
Алгоритмы find() и find_if() возвращают итератор найденного в контейнере
одиночного элемента. В первом алгоритме указывается значение, которое
нужно отыскать; во втором указывается критерий поиска, заданный
предикатом pred.
Большинству приложений приходится производить поиск специфических
значений в контейнерах. Часто элементы данных расположены в случайном
порядке; это означает, что алгоритм должен проверять каждый элемент
контейнера, пока нужное значение не будет найдено. Другие алгоритмы более
эффективны, но работают только с сортированными данными (см. раздел,
посвященный алгоритму binary_search()).
Алгоритм find()
Алгоритм find() довольно прост. Он ищет в контейнере первое вхождение
указанного значения. Прототип его приводился выше:
tempIate<class Inputlterator, class T>
Inputlterator find(Inputlterator begin, InputXterator end,
const T& value);
Алгоритм возвращает итератор, «указывающий» на найденный элемент.
Значение итератора может затем использоваться для вставки или удаления
элемента из контейнера.
570
Глава 15
Код
Следующий пример (find.cpp) демонстрирует вызов алгоритма find().
и
i.
г •
h
I л
/* _.*/
/*
/* STL find( )
/*
/*
/* */
#include <vector>
#include <iostream>
#include <algoritnm>
using namespace std;
'J ■ lint main()
\i -
'A
У
4
vector<int> v(1000);
vector<int>::iterator Ranlt;
int i = 0;
for (i m 0; i < 1000; i++) {
v[i] = i;
}
random_shuffle(v.begin(), v.end());
Ranlt m find(v.begin(), v.end(), 4627 )
if (Ranlt = v.end()) {
сout « "Item Not found" « endl;
}
else
(
cout « "Item found" « endl;
}
return 0;
}
Программа выводит:
Item Not Found
ПРИМЕЧАНИЯ
| ПРИ
Чтобы сделать пример более реалистическим, инициализируется 1000
элементов вектора. Элементы затем перемешиваются случайным образом с
помощью алгоритма random_shuffle{). Прототип его следующий:
template<class Ranlt>
void random_shuffie(Ranlt begin, Ranlt end);
В показанной программе элемент не находится, так как его значение 4627 в
контейнере отсутствует. Алгоритм find(), следовательно, возвращает итератор
контейнера end. Если бы элемент был обнаружен в контейнере, то алгоритм
Исследование библиотеки стандартных шаблонов 571
возвратил бы итератор, «указывающий* на первое вхождение значения в
последовательность элементов. Например, если бы мы искали значение 678,
итератор Ranit соответствовал бы позиции, в которой находится элемент со
значением 678; разыменовав итератор (*RanIt), мы получили бы искомое
значение 678.
Если контейнер содержит повторяющиеся значения элементов,
возвращаемый первым вызовом find() итератор мог бы быть использован для
продолжения поиска. Например:
Ranit = v.begin();
while (notAHFound) {
Ranit = find(RanIt, v.end(), value);
// Здесь что-то делается с Ranit или его содержимым...
}
Алгоритм binary_search()
Существуют алгоритмы другого рода, которые работают только с
сортированными последовательностями. Один из таких алгоритмов — binary_search().
Взяв предыдущий пример, мы можем сортировать последовательность и затем
вызвать binary_search() для поиска некоторого элемента.
Вот прототип алгоритма двоичного поиска:
template<class Fwdlt, class T>
bool binary_search(Fwdlt begin, Fwdlt end, const TS val);
Алгоритм возвращает булев результат, показывающий успех или неудачу
поиска.
Код
Пример демонстрирует применение алгоритмов random_shuffle(), sort() и
binary_search(). Код его находится в файле binary_search.cpp.
/*
/*
/* STL binary_search( )
/*
/*
/*
#include <vector>
#include <iostream>
#include <algoritnm>
using namespace std;
int main()
{
vector<int> v(1000);
int i = 0;
for (i =0; i < 1000; i++)
572
Глава 15
г *
г .
■ tl
{
v[i] = i;
}
f j random_shuffle(v.begin(), v.end());
.- j sort (v.begin () , v.end());
\ .] if (binary_search(v.begin(), v.end() / 678) «= true)
cout « "Found" « endl;
else
» cout « "Not found" « endl;
II return 0;
1 4
A
Программа выводит:
Found
ПРИМЕЧАНИЯ
| ПРИ
Чтобы вызов binary_search() имел смысл, последовательность элементов
должна быть сортирована, что легко достигается при помощи алгоритма
soj§(). В данном примере возвращаемое binaryjsearch() значение равно true,
та5£ как 678 присутствует в последовательности,
.^Лоиск в сортированном контейнере с помощью t>inary_j5earch() выполняет-
ся-значительно быстрее, чем при последовательном алгоритме поиска. Если
поиск производится на очень большом объеме данных, binary_search()
обеспечит очень высокую эффективность.
Использование функциональных объектов
Функциональные объекты являются, представителями классов,
определяющих operator(). В вызовах алгоритмов STL их можно использовать вместо
указателей на функцию; часто это оказывается более эффективным.
Код
Следующий пример демонстрирует использование функционального
объекта с алгоритмом for_each(). Код находится в файле functors.cpp.
яр
/* */
/*
/* STL. Использование функциональных объектов
/*
/*
/* */
#include <vector>
#include <string>
#include <iostream>
Исследование библиотеки стандартных шаблонов
573
]#include <algorithm>
fusing namespace std;
| struct employee {
string name;
int salary;
string department;
int years;
>;
(bool operator--(const employee fix, const employee fiy)
{
return x.name == y.name;
jbool operator<(const employee fix, const employee fiy)
(
return x.name < y.name;
|template<class _T> class Sum
{
int total;
(public:
Sum() : total(0) (}
void operator(){const _T fix) {
total += x.salary;
>
int Total() { return total; }
;b-
[int main()
t
vector<employee> v(5);
Sum<employee> s;
v[0
v[0
v[0
v[0
v[l
v[l
v[l
v[l
v[2
v[2
v[2
v[2
v[3
v[3
v[3
.name = "Stan";
.salary ■ 20000;
.department = "admin";
.years - 1;
.name = "Beril";
.salary - 24000;
.department = "support";
.years = 1;
.name = "Tanya";
.salary = 12000;
.department - "typing Pool"»;
.years - 5;
.name = "Bill";
.salary = 100000;
.department = "director";
574
Глава 15
у '
v[3].years = 5;
v[4].name = "Monica";
v[4].salary - 50000;
v[4].department = "typing pool";
v[4].years = 1;
s - for_each(v.begin(), v.end(), s) ;
cout « "Sum total of salary " « s.Total() « endl
return 0;
Вот что выводит программа:
Sum total of salary 206000
| ПРк
ПРИМЕЧАНИЯ
Здесь показан шаблон функционального объекта Sum, который складывает
значения зарплаты работников в своей внутренней переменной total.
template<class _T> class Sum
<
int total;
public:
Sum() : total(0) (}
void operator()(const _T £x) {
total += x.salary;
}
int Total() { return total; }
>;
В классе предусмотрен конструктор, инициализирующий поле total.
Алгоритм for_each() перебирает содержимое вектора; представитель класса Sum —
s — передается алгоритму в качестве третьего аргумента.
Для каждого элемента контейнера вызывается operator(X) функционального
объекта, и значение зарплаты в элементе прибавляется к значению поля total
объекта. После перебора всех элементов общий результат выводится в cout.
Контейнеры и потоки
Во многих случаях желательно было бы заполнять контейнеры
непосредственно из потоков. Однако контейнеры STL не поддерживают прямое чтение
или запись элементов в поток. Чтобы стало возможным читать и записывать
содержимое контейнера в cin и cout, нужно предварительно преобразовать
потоки с помощью предусмотренных в STL алгоритмов и итераторов.
Приведенный ниже пример показывает, как можно читать и писать
элементы вектора через cin и cout. Пример использует итераторы потоков STL и
функторы (функциональные объекты).
Исследование библиотеки стандартных шаблонов 575
Код
Программа io.cpp демонстрирует чтение ряда целых чисел в вектор.
/*
/*
/* STL. чтение и запись потоков cin и cout.
/*
/*
/*
,J" d#include <iterator>
l #include <vector>
- ";# include <algorithm>
#include <iostream>
using namespace std ;
! fint main {) {
vector<int> V;
cout « "Enter a sequence of integers (eof to quit}: " ;
copy(istream_iterator<int>(cin), istream_iterator<int>(),
back_inserter(V));
cout « endl;
copy(V.begin(), V.end() , ostream_iterator<int>(cout, " "));
i« cout « endl;
■i
Л
return 0;
}
Вот пример работы программы:
Enter a sequence of integers (eof to quit):
1234567 \0
12 3 4 5 6 7
( ПРУ
ПРИМЕЧАНИЯ
Для копирования целых чисел в вектор здесь применяется алгоритм сору()
в сочетании со стандартным типом итератора istream_iterator. Пробелы
между вводимыми с клавиатуры числами воспринимаются как разделители, и
каждое число помещается в свой собственный элемент вектора. В процессе
перебора элементов вектора при вставке элементов используется стандартный
итератор back_inserter.
Ниже приводятся различные прототипы, имеющие отношение к данному
примеру.
template<class Inputlterator, class OutIt>
Outlt copy (Inputlterator begin, Inputlterator ело!, Outlt x) ;
576
Глава 15
Алгоритм сору() может использоваться для копирования последовательно^
сти элементов в другой, но совместимый контейнер. Требуется только, чтобы
третий параметр был выходным итератором, которому была присвоена
некоторая позиция в новом контейнере.
template <class Container>
back_insert_iterator<Container> back_inserter (Containers);
Функция back_inserter() возвращает итератор типа back_insert_iterator,
который вставляет элементы в конец существующего контейнера. Контейнер
должен поддерживать метод push_back().
template <class T, class charT, class traits = ios_traits<charT>,
class Distance = ptrdiff_t>
class istream_iterator
: public iterator<input_iterator_tag, T,Distance>;
Класс istream_iterator позволяет извлекать из входного потока объекты
указанного типа. Он конструирует итератор входного потока из переданного
ему объекта типа basic__istream, в данном случае cin.
Чтение элементов контейнера из файла
Следующая программа развивает предыдущий пример несколько далее.
Вместо чтения из стандартного ввода она читает данные из файлового потока.
Для работы с файловыми потоками необходимо подключить заголовок
<fstream>.
Код
Программа iofile.cpp демонстрирует чтение текстовых строк из файла в
вектор,
I/* */
/*
!/* STL. Чтение строк из файла.
/*
/*
/* */
|#include <iterator>
#include <vector>
I#inelude <algorithm>
I#include <£stream>
j#include <string>
j#include <iostream>
j#pragma warning(disable:4786)
jusing namespace std;
jint main (int argc, char *argv[]) {
vector<string> V;
if {argc \~2) {
cout « "Wrong number of arguments" « endl;
Исследование библиотеки стандартных шаблонов 577
exit(l);
:--Я
''3 1
г-'
h4
}
std::ifstream from(argv[l]) ;
if ('from) {
cout « "Error opening file" « endl;
exit(l);
>
copy(istream_iterator<string>(from),istream_iterator<string>(),
back__inserter(V) ) ;
cout « "File Contents:" « endl;
copy (V.begin(), V.end(), ostream_iterator<string>(cout, "\n"));
return 0;
( ПРУ
ПРИМЕЧАНИЯ
Код очень похож на предыдущий пример, так как каждая строка файла
читается в отдельный элемент вектора.
Вместо копирования данных из cin алгоритм сору() копирует теперь
данные файлового потока from:
copy(istream_iterator<string>(from),istream_iterator<string>(),
back_inserter(V) );
Когда все данные файла прочитаны, над ними можно производить самые
различные операции. Например, поиск в памяти, который выполняется
значительно быстрее, чем поиск на диске.
Обратите внимание, что здесь нигде не устанавливается размер вектора.
Динамические свойства вектора позволяют читать в память довольно большие
файлы, поскольку все распределение памяти выполняется аллокатором
(классом, ответственным за выделение/освобождение динамической памяти) по
умолчанию. Вы можете посмотреть на определение шаблона вектора:
template <class T, class Allocator = allocator<T> >
class vector {...};
Нужны ли вообще аллокаторы, отличные от аллокатора по умолчанию, это
спорный вопрос. Но может случиться так, что для некоторого типа элементов
контейнера потребуется какая-то специальная схема управления памятью.
В этом случае можно довольно просто определить для нового класса
распределения памяти методы и операции, которые должен предусматривать любой ал-
локатор.
Сравнение строк
Хотя класс string, строго говоря, не входит в библиотеку стандартных
шаблонов, он все же обладает характерными чертами контейнера и часто
используется в сочетании с другими элементами STL. По этой причине мы включили
в эту главу простой пример, иллюстрирующий применение класса string к
19 Зак. 1208
578
Глава 15
распространенной программной задаче сравнения строк без различения
регистра. Обычная операция класса строк — производит сравнение, различающее
регистр.
Код
Программа string_compare.cpp показывает, как написать функцию,
сравнивающую строки без различения регистра. Она хорошо иллюстрирует
операции, которые можно выполнять над итераторами.
(£{/* __*/
/* STL. Сравнение строк без различения регистра.
/*
/*
to
#include <string>
"^ #include <algorithm>
#include <iostreara>
J" #include <cctype>
using namespace std;
У
i
sfil
4*
*■■*";
- ?
•■ i
* * *
H':
4
L -
r
и
■ tt
'i
int compareJCfoCase ( const string &sl, const string £s2)
{
string: :const__iterator itl - sl.begin();
string::const_iterator it2 = s2.begin ();
while (itl != sl.endO &fi it2 != s2.end()) {
if (toupper(*itl) != toupper(*it2)) {
return (toupper(*itl) < toupper(*it2)) ? -1 : 1;
++itl;
++it2;
}
return s2.size() - sl.size();
)
int main()
<
string si;
string s2;
cout « "Input the first string" « endl;
cin » si;
cout « "Input the second string" « endl;
cin » s2;
if (compare_NoCase( si, s2) == 0)
cout « "True: " « s2 « " the same as " « si « endl;
else
cout « "False: " « s2 « " is not the same as " « si « endl;
return 0;
Исследование библиотеки стандартных шаблонов 579
Вот примерный результат работы программы:
Input the first string
Hello
Input the second string
HELLO
True: HELLO the same as Hello
( ПРИМЕЧАНИЯ
Для прохода по символам строк в функции compare_NoCase() объявляются
два итератора. При каждом проходе цикла while они получают приращение и,
таким образом, двигаются вдоль строк. Интересен фрагмент цикла,
производящий собственно сравнение:
if (toupper(*itl) != toupper (*it2)) {
return (toupper(*itl) < toupper(*it2)) ? -1 : 1;
}
Условная операция сравнивает два символа, на которые ссылаются
итераторы. Если
*itl < *it2
возвращается -1, как это делает функция C++ strcmp().
Функция toupper() входит в стандартную библиотеку функций C++. Она
объявляется в <cctype> и преобразует переданный символ в верхний регистр.
Если все прочитанные символы строк совпадают, проверяется, совпадает ли
их длина:
return s2.size() - sl.size();
Если длина одинакова, возвращается 0. Если длина различна,
возвращаемое значение будет ненулевым.
Алгоритмы теории множеств
К данным контейнеров могут применяться некоторые интересные
алгоритмы, основанные на теории множеств. Ниже дается их краткое описание.
template <class InputIteratori, class InputIterator2, class OutIt>
Outlt set_union (InputIteratori boginl, InputIteratori endl,
InputIterator2 begin2, InputIterator2 end2, Outlt x) ;
Алгоритм set_union() производит объединение двух контейнеров и может
быть полезен, если вам нужно соединить два контейнера, но вы не хотите
хранить дубликаты. Например, если у нас есть два контейнера, содержащих
соответственно элементы 1,2, 3,4и4, 5, 6, то объединением их будет 1, 2, 3, 4, 5,
6 — четверка не будет включена в контейнер дважды.
template <class InputIteratorl, class InputIterator2, class OutIt>
Outlt set_intersection(InputIteratorl beginl, InputIteratorl endl,
InputIterator2 beg±n2, Inputlterator end2, Outlt x) ;
Алгоритм set_intersection() возвращает контейнер только с теми элементы-
ми, которые присутствуют в обоих входных контейнерах.
19*
580
Глава 15
template <class Inputlteratorl, class InputIterator2, class OutIt>
Outlt set_difference (Inputlteratorl b&ginl, Inputlteratorl endl,
InputIterator2 begin2, InputIterator2 end2, Outlt x);
Алгоритм set_difference() возвращает набор элементов, которые имеются в
первом контейнере, но отсутствуют во втором. Например, два контейнера с
элементами 1, 2, 3, 4, 5 и 3, 4, 5 дадут результат 1, 2, 3.
template <class Inputlteratorl, class InputIterator2, class OutIt>
Outlt set_symmetric_difference (
Inputlteratorl beginl, Inputlteratorl endl,
InputIterator2 begin2, Inputiterator2 end2, Outlt x) ;
Алгоритм set_symmetric_difference() возвращает контейнер с элементами,
которые входят только в один из двух входных контейнеров.
Алгоритмы теории множеств не изменяют входные последовательности,
передаваемые им в качестве аргументов. Результаты возвращаются в третьем
контейнере, который должен быть создан заранее и передан в последнем
параметре алгоритма.
Поиск подмножества строк
Если применять традиционные методики, поиск в тексте нескольких слов
окажется весьма трудоемкой задачей. Алгоритмы теории множеств позволяют
очень просто и, вероятно, более эффективно выполнить такой поиск.
Код
Программа set_intersection.cpp использует алгоритм set_intersection(),
чтобы определить, все ли строки из некоторого набора входят в другой набор
строк.
I/* */
/*
/* STL. set_intersection( ).
/*
/*
/* */
|#include <vector>
I #include <iostream>
I#include <algorithm>
I#include <string>
#pragma warning(disable:4786)
[using namespace std;
[void Print(string x)
<
cout « x « endl;
>
|template<class _T> class Count
Исследование библиотеки стандартных шаблонов
581
int с;
public:
Count() : с(0) (}
void operator()(const _T& х) (
if (х != "") с += 1;
)
int count() { return с; }
)
[int main ()
{
vector<string> first(6);
vector<string> second(4);
vector<string> res(4);
Count<string> cnt;
first[0] = "this";
first[1] = "is";
first[2] = "a";
first[3] = "set";
first[41 = "theory";
first[51 = "test";
second[0]
second[l]
second[2]
second[3]
"is";
"aa" ;
"set";
"test";
sort(first.begin(), first.end());
sort(second.begin(), second.end());
set_intersection(first.begin(),first.end(),second.begin(),second.en
|d(>,
res.begin() );
cout « "first" « endl;
for_each(first.begin ()', first.end() , Print) ;
cout « "second" « endl;
for_each(second.begin(), second.end(), Print);
cout « "Result" « endl;
for_each(res.begin(), res.end(), Print);
if (for_each(res.begin(), res.end(), cnt ).count() ==
[second.size())
cout « "All stings occur in first" « endl;
else
cout « "All stings do not occur in first" « endl;
return 0;
Программа выводит:
first
a
is
set
582
Глава 15
test
theory
this
second
a
is
set
test
Result
a
is
set
test
All stings occur in first
| ПРИМЕЧАНИЯ
Этот пример алгоритма set_interscction() показывает, как с его помощью
можно убедиться в том, что все строки одного контейнера имеются в другом.
Создаются два вектора строк. Вектор first инициализируется словами "this
is a set theory test", а вектор second — "is a set test". Каждый элемент обоих
векторов содержит отдельное слово. После этого оба вектора сортируются
алгоритмом sort().
Начальные и конечные итераторы контейнеров first и second передаются
алгоритму set__intersection() вместе с начальным итератором пустого контейнера
res, в котором возвращается результат пересечения. Все три вектора first,
second и res выводятся в cout с помощью алгоритма for_each() и функции Print().
Проверка того, что все элементы second входят в first, осуществляется
путем подсчета элементов в res. Это делает алгоритм for_each() с
функциональным объектом cnt (определяемого программой класса Count), который
возвращает полученное число в качестве значения своего метода count().
Функциональный объект производит подсчет, проверяя, не является ли переданная ему
строка пустой; если строка не пуста, счетчик с увеличивается на единицу.
Если число непустых строк в res совпадает с числом элементов в контейнере
second, то, очевидно, все элементы second присутствуют в first.
Очень просто расширить программу таким образом, чтобы она читала
строки из файлового потока; тогда тот же самый алгоритм использовался бы для
поиска набора строк в файле. Подобной процедуре можно найти множество
применений. Например, агентство по найму могло бы иметь большое число
потенциальных работников, которым нужно подобрать рабочие места из
имеющихся вакансий. Программа поиска могла бы проверять учетные записи
работников на соответствие некоторому набору критериев. Тем самым были бы
удалены все записи, не удовлетворяющие всем критериям и, таким образом,
подбор кандидатов на имеющиеся вакансии ускорился бы.
Исследование библиотеки стандартных шаблонов 583
Обслуживание приоритетных сообщений
В сегодняшнем компьютерном мире компоненты и приложения могут быть
рассеяны по сетевым инфраструктурам. Во многих случаях для передачи
информации и синхронизации узлов необходима коммуникация между
распределенными по сети приложениями. Для передачи данных и сообщений в среде
взаимодействующих программ применяются серверы для обслуживания
сообщений. В некоторых случаях этим сообщениям должны приписываться
определенные приоритеты в соответствии с их важностью.
STL предлагает удобный контейнер для хранения данных с приоритетами.
Такие контейнеры порождаются шаблоном класса priority_queue, который
может упорядочивать данные в соответствии с заданным критерием приоритета.
Вот шаблон прототипа одного из конструкторов класса приоритетной очереди:
template <class lnputIterator>
priori ty__queue (Inputlterator begin, Znputlterator end,
const Predfi x = Pred(),
const allocator_type& all = allocator__type ()) ;
Код
Программа priority_queue.cpp показывает, как создать сервер сообщений,
посылающий первыми сообщения с более высоким приоритетом. Если
сообщения имеют одинаковый приоритет, они извлекаются и очереди в том же
порядке, в каком они поступали.
I/* */~
/*
/* STL priority_queue< >
К*
/*
/* */
|#include <queue>
j#include <functional>
I#include <iostream>
(using namespace std;
struct Message {
int priority;
Message() : priority(0) {}
Message(int p) : priority(p) {}
void Service() {
cout « "Message Sent with priority " « priority « endl;
}
);
[bool operator<(const Message fix, const Message &y)
{
return x.priority < y.priority;
)
584
Глава 15
' jbool operator==(const Message fix, const Message &y)
■'J{
:*-*4 return x.priority — y.priority;
JfJJ
*;■ 'Jvoid SendMessage(priority_queue<Message> &q, const Message &m )
jL ;l q.push(m) ;
H>
?■ i
i *
void ServeMessages (priority_queue<Message>&q)
i
while (!q.empty ()) {
Message m = q.top();
q-pop() ;
m.Service () ;
}
}
ASS
int main()
{
priority_queue<Message> MessageQueue;
SendMessage(MessageQueue, Message(5));
'£, SendMessage (MessageQueue, Message (2)) ;
'' SendMessage (MessageQueue, Message (6)) ;
ServeMessages( MessageQueue );
return 0;
Программа выводит:
Message Sent with priority 6
Message Sent with priority 5
Message Sent with priority 2
| ПРИМЕЧАНИЯ
Для элементов приоритетной очереди определяется структура сообщений
Message. Для нее имеется два конструктора: один присваивает полю
приоритета значение по умолчанию, второй перегруженный конструктор присваивает
сообщению указанный пользователем уровень приоритета. Два конструктора
определяются для простоты, но можно было бы объявить всего один, который
присваивал бы приоритету значение по умолчанию, если последний не указан.
Структура Message реализует один метод с именем Service(), вызываемый
из Serve Mess a gcs() всякий раз, когда сообщение удаляется из очереди.
В структуре предусмотрены операции сравнения, чтобы можно было
помещать ее объекты в контейнер приоритетной очереди. В показанном ниже коде
операция «меньше» возвращает true, если приоритет х меньше приоритета у.
Операция равенства просто сравнивает приоритеты х и у и возвращает true,
л они одинаковы.
Исследование библиотеки стандартных шаблонов 585
bool operator<(const Message &x, const Message &y)
<
return x.priority < y.priority;
}
bool operator^-(const Message fix, const Message &y)
{
return x.priority == y.priority;
}
Функция main() объявляет приоритетную очередь с элементами типа
Message. Создается несколько сообщений, которые ставятся в очередь функцией
SendMessage(). Последняя использует для этого метод очереди push(). Вот ее код:
void SendMessage(priority_queue<Message> £q, const Message fim )
{
q.push(m);
}
Когда все сообщения поставлены очередь, вызывается ServeMessages():
void ServeMessages (priority_queue<Message>£q)
{
while (!q.empty()) (
Message m = q.top();
q.popt) ;
m.Service();
)
}
Функция ServeMessages() сначала проверяет, не является ли очередь
пустой, а затем получает значение верхнего элемента методом top() и удаляет его
из очереди вызовом рор() — как это ни странно, данный метод только удаляет
элемент; можно было бы ожидать, что метод при этом возвратит
«вытолкнутое» значение.
Когда значение элемента получено, ServeMessages() вызывает его метод
Service(), который просто выводит текстовое сообщение в cout.
Если хотите, вы можете реорганизовать очередь таким образом, чтобы
меньшие значения означали более высокий приоритет. Это делается простым
изменением операции < либо передачей альтернативной функции сравнения
конструктору очереди. Первое решение показано ниже:
bool opeгаtor<(const Message fix, const Message fiy)
{
return y.priority < x.priority;
)
Этот пример сервера сообщений можно адаптировать для самых разных
приложений. Например, в ситуации, где каждое сообщение обрабатывается на
приостановленной «нити» процесса, сервер сообщений мог бы «пробуждать»
каждую нить в приоритетном порядке. Между сохраняемыми в очереди
элементами можно было бы организовать взаимозависимости, чтобы
синхронизовать вызовы методов или извлечение данных.
Реализация подобных возможностей без STL была бы долгой и трудоемкой
задачей, и вылилась бы в значительно большее количество строк кода, чем вы
увидели здесь.
586
Глава 15
Контейнер двоичного дерева
Последний пример этой главы показывает, как создать двоичное дерево
поиска, которое может хранить элементы любого типа, располагая их в
соответствии с ключевыми значениями. Реализация алгоритмов обхода позволяет
перебирать узлы дерева различными способами. Например, алгоритмы
обхода, применяемые в нашем примере, производят обход с «порядковой
выборкой» (inorder). Это означает, что содержащиеся в узлах дерева значения
всегда извлекаются в восходящем порядке. Это удобно и экономно, так не требует
никаких дополнительных структур данных или алгоритмов — сортировку вы
получаете «даром».
Показанный здесь пример не является законченным и представляет собой
оболочку, которой можно придать необходимые функциональные свойства. Он
реализует самые существенные действия над деревом, включая функции
Insert(), Search() и Remove(). Реализован также метод Traverse(),
выполняющий рекурсивный обход дерева с порядковой выборкой.
Совместимость с STL поддерживается благодаря реализации итератора. Он
также может быть использован для порядкового обхода дерева, — другими
словами, извлекаемые от begin() до end() значения следуют в восходящем
порядке.
ЗАМЕЧАНИЕ ПРОГРАММИСТА
( ЗАГ,
Вам, может быть, интересно будет сравнить код двоичного дерева,
приведенный в этой главе, и дерево Арта Фридмана из главы 3. Это две
очень разных вариации на одну и ту же тему.
Код
Код программы binary_tree.cpp показывает создание контейнера,
имеющего структуру- двоичного дерева.
//
[// Шаблон контейнерного класса двоичного дерева.
//
/У*****************************************************************
j#include <iostream>
!#include <functional>
Sftinclude <iterator>
#include <algorithm>
[ftpragma warning(disable:4786)
#pragma warning(disable:4550)
using namespace std;
[template <class _K, class _Ty, class _Pr = less<_K>,
Исследование библиотеки стандартных шаблонов
587
, 4
class _А ~ allocator<_Ty> > class BinaryTree
typedef pair<_K, _Ty> value_type;
_Pr key_conipare;
typedef _K key_type;
typedef _Ty referent_type;
typedef _A::difference_type difference_type;
typedef _REFERENCE_X(_Ty, _A) reference;
typedef _POINTER_X(void, _A) jGenPtr;
typedef _REFERENCE_X(_Ty, _A) _Vref;
typedef _REFERENCE_X(_K, _A) _Kref;
typedef _POINTER_X (_Ty, _A) _Tptr;
struct _Node;
typedef _POINTER_X(_Node, _A) _NodePtr;
typedef _REFERENCE_X(_NodePtr, _A) _NodePref;
static _Vref _Value( _NodePtr _P) { return
И ( Vref > (*_P)._Value (>; )
static _Kref _Key( _NodePtr _P ) { return (_Kref)(*_P)._Key();
static _NodePtr _Z( _NodePtr _P ) { return (*_P).z; }
static _NodePtr _Parent{ _NodePtr _P) { return (*_P).p; }
static _NodePtr _Min( _NodePtr _P )
{ while <_Left(_P) != _Z (_P)) _P = _Left(_P); return _P; }
static _NodePtr _Max( ^NodePtr _P )
{ while (JUght(_P) != _Z (_P)) _P = _Right(_P); return _P;
}
static _NodePref _Left(_NodePtr _P) { return
(_NodePref)(*_P).1; }
static _NodePref _Right(_NodePtr _P) { return
(_NodePref)(*_P).r; }
struct _Node (
value_type item;
_NodePtr 1;
_NodePtr r;
_NodePtr z;
_NodePtr p;
_Node( key_type k, referent_type v,
_NodePtr 11, _NodePtr rr, _NodePtr _X, _NodePtr _Y ) {
item.first = k;
.'\ r item, second = v;
1 = 11,
r = rr
p « _X
z - _t
}
i/3
|* ^ key__type &__Key() { return item.first; }
\ -J referent__type &_Value() { return item.second; }
588
Глава 15
' '•■* _NodePtr head;
*i\ _NodePtr z;
■" -."i
public:
J
i- j
BinaryTree(int max =10) {
z = new _Node( 0, ' Z' , 0, 0, 0, 0 );
head = new _Node( 0, 'a*, 0, z, z, z );
}
.' -BinaryTree () {
: f // Деструктор дерева.
* ' Clear ();
' £ delete z ;
! delete head;
}
void Clear(_NodePtr p = 0) (
// Уничтожает содержимое дерева.
if (p = 0) p ■ _Right( head );
if (p = z) return; // Дерево пусто.
DeleteSubTree( p );
*j head->l = head->r = head->p = z;
referent_type Search(key_type v)
{
*1
; _NodePtr x = „Right( head );
-' £ z->item. first = v;
■ £ while ( v != x->_Key() )
' ; x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x ) ;
; return Value(x);
j void Insert(key_type v, referent_type info)
* ■ i
_NodePtr p;
_NodePtr x;
, p = head;
•; x = __Right( head );
;■ .
■?* while ( x != z ) {
p = x;
"j x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x ) ;
• ,; )
F"7" x = new _Node(v, info, z, z, p, z );
.' if (key_coinpare(v, _Key (p)))
', 1 _Left( P ) = x;
' else
_Right( p ) - x;
Исследование библиотеки стандартных шаблонов 589
L> <
г -
■г*
'I
£1
I
■ *
■s *
!i
void Remove (key_type v)
_NodePtr c, p, x, t;
if (_Right(head) == z) return; // Дерево пусто.
к";^ z->item.first - v;
JJ] p = head;
x = _Right( head );
while (v \~ _Key(x) ) {
p = x;
x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x );
}
x;
if (_Right( t ) == z) {
x = _Left( x );
}
else {
if (_Left(_Right( t )) == z ) {
x = _Right{ x );
_Left( x ) = _Left( t );
}
else
{
с = _Right( x ) ;
while {_Left(_Left( с )) != z) {
с - _Left( с );
'1 )
x = _Left( с );
_Left( с ) = _Right( x );
j- ^ _Lef t ( x ) = _Lef t ( t ) ;
_Right( x ) = _Right( t );
)
}
delete t;
if (key_compare(v, _Key(p)))
_Left( p ) = x;
else
_Right( p ) = x;
if (x->p)
x->p = p;
}
void Traverse(_NodePtr t ■ 0)
{
// Обход с порядковой выборкой.
// ЗАМЕЧАНИЕ. Это рекурсивная функция, вызов которой
// для очень большого дерева может вызвать
// переполнение стека.
if (t == 0)
590
Глава 15
М t = Right{ head );
\Щ
if (t != z)
"l (
', Л Traverse (_Lef t ( t )) ;
\i Visit ( t );
Traverse(_Right( t ));
}
)
void Visit(_NodePtr t)
{
if (t != 0)
<
cout « _Value(t)« endl;
}
}
protected:
void DeleteSubTree(_NodePtr p)
{
_NodePtr t;
if (Р !« z) {
DeleteSubTree( _Left( p ) ) ;
t = _Right( p );
delete p;
DeleteSubTree( t );
}
}
public:
class iterator;
friend class iterator;
class iterator : public _Bidit< _Ty, difference_type> {
protected:
_NodePtr _Ptr;
public:
iterator() : _Ptr(0) {
}
iterator( _NodePtr _P ) : _Ptr(_P) {}
reference operator*() const {
return (_Value(_Ptr));
}
_Tptr operator->{) const (
return(S**this);
}
iterators operator++() {
f,J _lnc();
Исследование библиотеки стандартных шаблонов __ 591
' \ return (*this);
; | >
' A iterator Soperator++(int) {
I I iterator __Tmp = *this;
*' ++*this;
return (_Tmp) ;
}
i-\
t I iterators operator—{) {
_Dec();
return (*this);
}
\ 1
i
* K'
•.7
I ■
iterator operator—(int) {
iterator _Tmp = *this;
--*this;
return (_Tmp);
}
bool operator= (const iterators _X) const {
return (_Ptr -= _X._Ptr);
)
\*\ bool operator! —(const iterators _X) const (
f1 return (» (*this == _X)) ;
*lfi }
void _Inc() {
if (_Right(_Ptr) !=_Z(_Ptr))
_Ptr = _Min(_Right(_Ptr)) ;
else {
_NodePtr _P;
while (_Parent(_Ptr) != _Z (_Ptr) fiS
_Ptr = _Right(_P = _Parent(_Ptr))) {
_Ptr = _P;
}
_Ptr = _P;
)
}
void _Dec() {
if (_Parent(_Ptr) ~__Z(_Ptr))
__Ptr = _Max(_Right(_Ptr)) ;
else
if ( _Parent(_Parent(_Ptr)) = _Ptr)
J _Ptr = _Right(__Ptr) ;
J1 else
if (_Left(_Ptr) !=_Z(_Ptr))
3 _Ptr = _Max(_Left(_Ptr)) ;
i else
J <
592
Глава 15
г
it
» A
-$
\
- *,
j
_NodePtr _P;
while ( _Ptr = _Lef t (_P = _Parent (_Ptr)))
_Ptr = _P;
_Ptr = _P;
}
)?
iterator begin() {
if (_Right( head ) == z)
return iterator{ head );
_NodePtr _P = head;
return iterator ( _Min(_Right( _P } ) );
}
iterator end() {
_NodePtr _P = head;
return iterator( P );
\ void Print( char r )
<
cout « r « endl;
]int main()
■♦!<
1 j BinaryTree<char, char> bTree;
, bTree.Insert{ 't', 't' )
4 bTree. Insert ( • r', ' r' )
bTree.Insert( 'e', 'e' )
bTree.Insert( •e■, •e' )
^IM
" 1.1
V
»?
As
I
bTree.Traverse(O);
char ans =1;
while( ans ) {
cout « "Insert a letter : 1" « endl;
cout « "Search for a letter : 2" « endl;
cout « "Delete a letter : 3" « endl;
cout « "Finish : 4" « endl;
cin » ans;
char c;
switch( ans ) {
Исследование библиотеки стандартных шаблонов 593
case '1':
cout « "Enter a letter to insert" « endl;
cin » c;
bTree.Insert( с, с );
break;
case •2■:
cout « "Enter a letter to find" « endl;
cin » c;
if (bTree.Search( с ) != * Z')
cout « "Found " « с « endl;
else
cout « с « " Not Found" « endl;
break;
case '3':
cout « "Enter a letter to delete" « endl;
cin » c;
bTree.Remove( с );
break;
case '4':
ans = 0;
break;
default:
break;
}
cout « "Tree Contents" « endl;
for_each(bTree.begin(), bTree.end(), Print );
}
return 0;
}
( flPf
ПРИМЕЧАНИЯ
Реализация этого двоичного дерева весьма существенно отличается от того,
что описывалось в главе 3. Данная реализация обеспечивает базовые операции,
такие, как вставка, удаление и поиск, но предусматривает также итератор, что
позволяет дереву взаимодействовать с алгоритмами и контейнерами STL.
594
Глава 15
I СТРУКТУРА УЗЛА
Дерево состоит из некоторого числа узлов, которые могут быть связаны
между собой через свои указатели на левую и правую ветви, образуя, таким
образом, древовидную структуру. Вот определение структуры узла:
struct _Node {
value_type item;
_NodePtr 1; // Левый
_NodePtr r; // Правый
__NodePtr z; // Нулевой узел
_NodePtr p; // Родительский узел
_Node( key_type k, referent_type v,
_NodePtr 11, _NodePtr rr, __NodePtr _X, _NodePtr _Y ) {
item.first = k;
item.second — v;
1 = 11;
r — rr;
P * _x;
z = _Y;
)
key_type S__Key() { return item.first; }
referent__type & Value() { return item.second; }
>;
Здесь тип _NodePtr определен как указатель на структуру узла. Для
определения типа указателя на узел в стандартной модели памяти используется
макрос _POINTER_X(), определенный в <xmemory> (специфика Microsoft).
Макрос _REFERENCE_X(), также из <xmemory>, применяется для определения
типа ссылки на узел, который может использоваться для передачи ссылок.
Методы, включая _Min(), объявляются как статические элементы и
используются при реализации всего двоичного дерева и класса итератора для
упрощения и уменьшения объема кода.
Типы key__type и referent_type — также определения typedef,
упрощающие разработку и делающие код более удобочитаемым.
Каждый узел дерева содержит указатели на своего родителя и на
«фальшивый» узел, 2. Указатель на родительский узел введен потому, что доступ к
нему необходим итератору.
Хотя на первый взгляд это неочевидно, но на самом деле нет разницы
между использованием в качестве терминатора ветви * фальшивого» узла и
значения NULL. Однако узел-пустышка имеет одно важное достоинство: он дает
удобный способ завершения поиска, что можно видеть в реализации метода
Search(). Каждое дерево должно иметь корневой, или головной, узел, от
которого оно начинается. Узел head нашего дерева создается при конструировании
контейнера. Правый, левый, родительский и z-узел инициализируются
узлом-терминатором, образуя в результате пустое дерево. Следующая проверка:
if (_Right(head) -= z)
Исследование библиотеки стандартных шаблонов 595
позволяет легко убедиться, что дерево действительно пусто. Такая проверка
избавляет от необходимости отслеживать текущий счетчик узлов или размер
дерева.
Узел-терминатор z инициализируется одновременно с узлом head, но все
его указатели устанавливаются равными нулю. Каждый узел содержит также
структуру STL pair, в которой хранится ключ элемента и его данные. Эта
структура имеет два элемента и с именами first и second, к которым можно
получить доступ посредством двух методов Node.
| INSERTQ
Мы начнем с изучения метода Insert() двоичного дерева:
void Insert(key_type v, referent_type info)
_NodePtr p;
_NodePtr x;
p = head/
x = _Right( head );
// Найти точку вставки,
while ( x !- z ) {
p = x;
x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x );
}
// Создать новый узел и присоединить узлы ветвей.
х = new _Node(v, info, z, z, p, z );
// Вставить узел, установив связку родительского узла,
if (key_compare(v, _Key(p)))
_Left( p ) я х;
else
_Right{ p ) = х;
}
Чтобы вставить в дерево новый узел, нужно указать его ключ и данные.
Метод InsertQ прежде всего инициализирует переменную х указателем на узел
справа от головного. Тем самым предполагается, что ключи всех размещенных
в дереве элементов больше ключевого значения узла head. Это действительно
так, поскольку head инициализируется ключевым значением 0, наименьшим
из возможных в дереве, — так что все последующие ключевые значения
должны быть больше.
Чтобы найти точку вставки нового узла, применяются следующие правила:
1. Если ключевое значение нового узла больше, чем у текущего, пойти
направо.
2. Если ключевое значение нового узла меньше, чем у текущего, пойти
налево.
Например, в методе Insert() нашего двоичного дерева ту ветвь, в которую
нужно вставить новый узел, находит следующий код:
596
Глава 15
while ( х != z ) {
p = x;
x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x ) ;
}
Здесь key_compare() является представителем предиката _Рг, который
можно передать в качестве аргумента шаблона дерева и может быть любой
подходящей функцией или функтором. В данном примере используется класс
по умолчанию lesso. Статический метод _Кеу() определяется классом дерева
для извлечения ключа своего параметра типа _NodePtr. Цикл while
перебирает связанные узлы двоичного дерева, пока х не окажется равен z. По
завершении цикла переменная р указывает на узел, который станет родительским для
нового узла. Правая или левая ветвь родительского узла свяжет его с новым
узлом. В данный момент пока неизвестно, какая именно.
Теперь создается новый узел, и его правая, левая и нулевая ветви
инициализируются терминатором z. Его указателю родительского узла, р,
присваивается узел, найденный в предыдущем цикле while. В данный момент
родительский и новый узлы уже связаны, правда, не в том направлении (от нового к
родительскому). Последний фрагмент кода устанавливает связь от
родительского к новому узлу, устанавливая последний в качестве правой или левой ветви
родительского узла. Здесь опять вызывается key_compare() для определения
нужной ветви.
В примере в дерево вставляются символы s, t и 1. Символ s помещается в
правую ветвь узла head; t становится правой ветвью s и 1 вставляется в левую
ветвь узла s.
(search о
Рассмотрим теперь метод Search(). Он вызывается для поиска в дереве
элементов, соответствующих указанному ключевому значению.
referent_type Search(key_type v)
t
_NodePtr x - _Right( head );
z->item.first = v;
while ( v != x->_Key{) )
x = (key_compare(v, _Key(x))) ? _Left( x ) : _Right( x );
return _Value(x);
Интересной особенностью метода Search() является то, что он присваивает
искомое ключевое значение узлу-терминатору z. Тем самым условию
завершения цикла while будет удовлетворять и неудачный поиск, в результате
которого метод возвратит значение, записанное в z. Для сравнения ключевых
значений вызывается key_compare(), а условное выражение присваивает
переменной х правильный — левый либо правый — узел. Цикл while исполняется до
тех пор, пока текущее значение ключа не окажется равным искомому. При
равенстве значений цикл завершается и возвращаются данные, содержащиеся в
х, поскольку это и есть искомый узел. Как уже говорилось, при неудачном
исходе поиска возвращается значение данных z.
Исследование библиотеки стандартных шаблонов 597
В нашем примере узел-терминатор содержит в качестве данных 'Z'. Это
сделано для демонстрации работы узла-терминатора в методе Search().
Давайте рассмотрим процесс поиска, чтобы лучше понять преимущества
использования двоичных деревьев. В примере дерево содержит символы s, t
и 1. чтобы найти символ t, потребовалось бы всего два сравнения ключей. Если
бы для хранения тех же символов использовался массив, могло бы
потребоваться три сравнения. В нашем случае, при размещении в дереве символов s, t
и 1, максимальная глубина дерева равна 2. Усложненные двоичные деревья
поиска, называемые иногда красно-черными деревьями, реализуют оптимизацию
своей структуры посредством вращений, минимизирующих глубину дерева.
В таких деревьях, однако, процедуры вставки и удаления узлов могут быть
значительно медленнее, чем в традиционной реализации двоичных деревьев.
Метод двоичного дерева Remove() отвечает за удаление элементов из
дерева. Указанное при вызове ключевое значение используется для поиска узла,
который требуется удалить; это выполняет код, практически идентичный
коду в Search(). Давайте рассмотрим по порядку все фрагменты метода.
// Найти нужный узел,
while (v != _Кеу(х) > {
р = х;
х = (key__compare(v, _Кеу(х))) ? __Left( х ) : _Right( x };
}
t = х;
Различие между этим циклом и циклом while в методе Search() состоит в
том, что здесь переменная р типа _NodePtr отслеживает текущий
родительский узел.
Следующий фрагмент кода находит узлы, которые нужно будет заново
присоединить к дереву после удаления найденного узла. Если этого не сделать,
удаление разрушит структуру дерева и все узлы ниже удаляемого не будут
корректным образом включены в дерево.
// Найти присоединенные узлы,
if (_Right( t ) — z) {
x = Left( x >;
}
else {
if (_Lef t (_Right ( t >> == z ) {
x = _Right( x );
_Left( x ) = _Left( t );
}
else
<
с - _Right( x );
while (_Left(_Left( с )) '= z) {
с = _Left{ с );
)
x = Left( с );
598
Глава 15
_Left( с ) = _Right( x );
_Left( x ) » Left( t );
_Right( x ) = _Right{ t );
}
}
Переменной t присвоен узел, найденный циклом while. Первый условный
оператор проверяет, не равна ли правая ветвь t узлу-терминатору z. Если
равна, то х присваивается узел слева от х.
Например, если бы дерево состояло из единственного символа t, то на
содержащий его узел указывала бы правая ветвь head. После отыскания узла со
значением t родительский указатель р указывал бы на head, а переменная t —
на узел, подлежащий удалению. Поэтому первое условие в приведенном
фрагменте было бы истинным, поскольку правая ветвь удаляемого узла
равняется z. Переменной х был бы присвоен ъ и процедура перешла бы к
заключительным действиям:
// Удалить узел,
delete t;
// Установить новые связи.
if (key_compare(v, _Key(p)))
_Left( p ) ~ х;
else
_Right( p ) -ж;
if (x->p)
х->р = р;
}
Узел удаляется, а «осиротевшие» узлы, находящиеся ниже,
присоединяются к его родителю. С помощью key_compare() выясняется, к какой из ветвей
родителя они будут присоединены. Последний оператор связывает вновь
присоединенный узел с новым родителем.
ЕСЛИ УЗЛОВ МНОГО
| ЕС/
В обсуждении предыдущего параграфа затрагивался только случай
удаления, когда в дереве совсем мало узлов. Процесс усложняется, если дерево
содержит много узлов. Чтобы показать производимые при этом действия, нужно
вернуться снова к уже показанному коду:
// Найти присоединенные узлы.
if (_Right( t ) == z) {
x = _Left( x );
}
else {
if (_Left(_Right( t )) == z ) {
x = _Right( x );
_Left( x ) = _Left( t );
}
else
{
Исследование библиотеки стандартных шаблонов
599
)
}
с = _Right( х );
while {_Lef t (_Lef t ( с )) != z) {
с = _Left( с );
J
x =* _Lef t ( с ) ;
„Left{ с ) m _Right{ x );
_Left( x ) = _Lef t( t );
_Right{ x ) = _Right( t );
Если левый узел правой ветви t равен z, переменной х присваивается
правая ветвь х, а левой ветви последнего присваивается левая ветвь t. Эти
операторы выполняются, если узел в правой ветви узла, подлежащего удалению, не
имеет потомков в своей левой ветви.
Следующий фрагмент кода довольно сложен; он соответствует случаю,
когда узел справа от удаляемого имеет непустую левую ветвь.
else
{
с = _Right( х );
while (_Left(_Left( с )) != z) {
с = _Left< с ) ;
)
x = _Left{ с );
_Left( с ) = JRight( x );
_Left( x ) ■ _Left( t );
_Right{ x ) = _Right( t );
}
}
Сначала мы выясним текущее состояние указателей узлов с, х и t.
Переменная с — это просто временный указатель. Две другие переменные указывают
на узел, подлежащий удалению. (Ранее t было присвоено значение х.)
Первая часть этого фрагмента кода присваивает с узел из правой ветви х, и
цикл while ищет в этой ветви наименьшее ключевое значение, поскольку левые
узлы всегда содержат значение, меньшее родительского. Когда цикл
завершается, с указывает на узел перед узлом с наименьшим ключевым значением.
Следующий оператор присваивает х узел слева от с, который и является узлом с
наименьшим значением. Последние три оператора реорганизуют соединения
узлов вокруг удаляемого. После этого узел можно действительно удалить.
Заключительный фрагмент метода Remove() удаляет узел и завершает
установку новых связей, присваивая левой либо правой ветви родителя заново
скомпонованные нижние узлы.
// Удалить узел,
delete t;
// Установить новые связи,
if (key_compare (v, __Key (p)))
_Left( p ) - х;
else
_Right( p ) = х;
600
Глава 15
if <x->p)
х->р = р;
}
Вызов key_compare() определяет, какой из двух ветвей родителя должно
производиться присваивание. Поскольку каждый узел содержит указатель на
своего родителя, последний оператор выполняет присваивание родительского
узла соответствующему указателю структуры.
| TRAVERSEO
Перед рассмотрением итераторов двоичного дерева мы исследуем метод
Traverse(). Есть несколько алгоритмов для «обхода» дерева в различной
последовательности :
Метод обхода
с порядковой выборкой
(inorder)
с отложенной выборкой
(postorder)
с предварительной
выборкой (preorder)
с поуровневой выборкой
(level order)
Рекурсивное правило
Обойти левое поддерево, выбрать корень, затем обойти
правое поддерево.
Обойти левое поддерево, обойти правое поддерево, затем
выбрать корень.
Выбрать корень, обойти левое поддерево, обойти правое
поддерево.
Отсутствует. Дерево читается сверху вниз и слева направо.
Алгоритм обхода, реализованный в нашем двоичном дереве — это обход с
порядковой выборкой; это означает, что ключевые значения будут выбираться в
восходящем порядке. Например, если бы дерево содержало символы х, s, m и b,
метод Traverse() выбрал бы их в последовательности b, m, s, x. Вот код обхода:
void Traverse(_NodePtr t = 0)
{
// Обход с порядковой выборкой.
// ЗАМЕЧАНИЕ. Это рекурсивная функция, вызов которой
// для очень большого дерева может вызвать
// переполнение стека.
if (t == 0)
t = _Right( head );
if (t != z)
{
Traverse(_Left( t ));
Visit( t );
Traverse(_Right( t ));
)
}
Исследование библиотеки стандартных шаблонов
601
[ ЗАЛ
ЗАМЕЧАНИЕ ПРОГРАММИСТА
Комментарий в начале этой процедуры предупреждает, что функция
Traverse() рекурсивная. Это весьма важный момент, если функция
применяется к большим деревьям. При каждом вызове она
заталкивает свои переменные в стек программы. Чем больше число рекурсивных
вызовов, тем большее пространство требуется для стека. Поскольку
размер стека конечен, ин может исчерпаться. Если вы не уверены в
размере своего дерева, возможно, стоит заменить метод Traverse() на
приведенный ниже. Вместо рекурсии он использует операторы goto и
пользовательский стек.
void Traverse(_NodePtr t) (
у: while (t != z) {
Visit(t);
stck.push(_Right(t));
t = _Left(t);
>
if (stck.empty()) goto x;
t = stck.top();
stck.pop();
goto y;
x:
J
Метод двоичного дерева Traverse() проверяет переданный параметр t, и
если он 0, присваивает ему правую ветвь головного узла. (Как вы помните, это
первый действительный узел дерева.) Условный оператор проверяет текущее
значение t; если оно не равно z, Traverse() вызывает себя с узлом слева от t в
качестве параметра. В результате будет выполняться обход левого поддерева,
пока не встретится узел-терминатор ъ. Как только встретится t == z, стек
вызовов начинает разматываться и выбирается узел перед z. Снова Traverse()
вызывает себя, но на этот раз с правой ветвью текущего узла в качестве
параметра. При этом снова будеи исследована левая ветвь узла, и так далее, пока не
будет обойдено все дерево.
| CLEARQ
Показанная здесь реализация двоичного дерева конструируется из
динамически создаваемых узлов. Метод ClearQ, показанный ниже, может вызываться
для очистки и повторной инициализации дерева.
void Clear(_NodePtr p = 0) {
// Уничтожает содержимое дерева.
if (р — 0) р = _Right( head );
if (p == z) return; // Дерево пусто.
DeleteSubTree( р );
head->l = head->r = head->p = z;
602
Глава 15
Обеспечив действительность переданного указателя _NodePtr, метод
проверяет, не является ли дерево пустым. После этого вызывается защищенный
метод DeleteSubTree(). Это рекурсивный метод, очень похожий на Traverse().
вот его код:
void DeleteSubTree( NodePtr p)
{
_NodePtr t;
if (p !=> z) <
DeleteSubTree( _Left( p ) );
t = _Right( p >;
delete p;
DeleteSubTree( t );
}
>
Как только все узлы ниже head уничтожены, метод С1еаг() повторно
инициализирует правую ветвь head значением z. Это важно, так как правая ветвь
head в этот момент «указывает» на неинициализированную память.
Деструктор двоичного дерева вызывает С1еаг() для удаления содержимого
дерева. Он завершает уничтожение, удаляя узлы head и z. Вот код деструктора:
~BinaryTree() {
// Деструктор дерева.
Clear();
delete z;
delete head;
}
ИТЕРАТОРЫ
[ИТЕ
Чтобы сделать двоичное дерево совместимым с алгоритмами STL,
необходимо реализовать для него какую-то форму итератора. В нашем случае
контейнера двоичного дерева определяется не-константный двунаправленный
итератор. Класс итератора является вложенным в класс двоичного дерева. Итератор
может вызывать статические методы дерева. Кроме того, класс итератора
объявляется дружественным классу двоичного дерева. Две функции, begin() и
end(), определяются как методы дерева. Вот их определения:
iterator begin() {
if (_Right( head ) = z)
return iterator( head );
// Возвращает итератор, "указывающий"
//на наименьший элемент в дереве.
JHodePtr _P = head;
return iterator( _Min(_Right{ _P ) ) );
>
iterator end() {
Исследование библиотеки стандартных шаблонов 603
II Возвращает итератор, "указывающий"
// на узел-терминатор.
_NodePtr _P = head;
return iterator ( __Р );
)
Мы здесь разберем здесь только то, что происходит при вызове алгоритма
for_each() для итераторов двоичного дерева в функции main() примера.
Метод begin() проверяет, что дерево содержит действительные элементы.
Если это так, создается итератор, содержащий указатель узла с наименьшим
ключевым значением, которое определяется с помощью одной из статических
функций двоичного дерева, _Min(). Метод end() возвращает итератор,
содержащий указатель головного узла.
for_each(bTree.begin(), bTree.end(), Print );
Алгоритм for_each(), вызываемый в примере для печати содержимого
дерева, принимает его итераторы begin() и end(). Функция Print() является
пользовательской и производит распечатку значения узла. Когда for_each()
вызывается в первый раз, начальный и конечный итераторы установлены так, что
начальный итератор указывает на узел с наименьшим значением, а конечный —
на головной узел. Например, если дерево содержит символы s, t и 1, начальный
итератор будет указывать на узел с ключом 1, а конечный на головной узел.
Для извлечения следующего значения дерева алгоритм for_each()
использует операцию префиксного инкремента. Вот ее код:
iterators operator-H-() {
_Inc();
return (*this);
}
Итератор определяет метод _Inc(), который смещает на единицу позицию
указателя узла итератора, „Ptr. Вот метод _1пс():
void _lnc() {
if (_Right (__Ptr) !=_Z(_Ptr))
_Ptr = _Min(_Right(_Ptr));
else {
_NodePtr _P;
while (_Parent(_Ptr) !- _Z (_Ptr) &&
_Ptr — _Right(_P = _Parent(_Ptr))) {
_Ptr = _P;
)
_Ptr - _P;
}
)
Класс итератора двоичного дерева определяет поле указателя на узел, в
котором хранится текущее положение итератора. Этот указатель с именем _Ptr
необходим для _1пс() и соответствующей функции декремента _Dec(). Когда
создаются начальный и конечный итераторы, их указатель _Ptr
инициализируются соответственно указателями на наименьшее ключевое значение и
head. При вызове _1пс() итератор, к которому он применяется — это итератор,
полученный от метода begin() двоичного дерева. _Int начинается с проверки
того, что _Ptr не находится в конце правого поддерева, которая производится
604
Глава 15 '
путем сравнения правого узла _Ptr и z. Если проверка успешна, _Ptr
присваивается узел с наименьшим ключевым значением в левом поддереве текущего
узла. Это делается вызовом статической функции дерева _Min().
Показанный ниже цикл while находит следующий узел последовательности,
двигаясь вверх по дереву через указатель родительского узла. Метод _1пс()
следует рекурсивному правилу обхода с порядковой выборкой.
_NodePtr _Р;
while (_Parent(_Ptr) != _Z (_Ptr) &&
_Ptr = „Right <_P = __Parent (_Ptr))) {
_Ptr = _P;
}
Первая проверка в условии оператора while удостоверяет, что родителем
_Ptr не является головной узел. Временный указатель _Р нужен для хранения
позиции родителя и используется в условии оператора while для определения
правого узла родителя текущего узла. Завершающий оператор присваивает
_Ptr значение временного указателя:
_Ptr = _Р;
Рассмотрите внимательно метод _1пс(), и вы ясно увидите рекурсивное
правило порядковой выборки «обойти левое поддерево, остановиться в корневом
узле, обойти правое поддерево». Разработанный в этом примере контейнер
двоичного дерева можно использовать в самых разных приложениях.
Различные реализации алгоритмов обхода применяются, например, в деревьях
синтаксического разбора. Однако использование алгоритмов порядкового обхода,
реализованных в этом примере, предполагает, что дерево всегда сортировано.
Реализация класса итератора позволяет обходить дерево как в восходящем,
так и в нисходящем порядке и обеспечивает взаимодействие с разнообразными
алгоритмами STL.
ГЛАВА
Тм1 .'-
'■*>
.. *
«7«QftrTr
1 р: ] р»|]ртйЬяшк'^]Я:'
Lf;
f;
i€,e,.
'Чйщ!*.
clock.cpp
post.h (из post.exe)
post.cpp (из post.exe)
A?5-*
m -v>s +
Ларе Кландер
^й» h™« ^
606
Глава 16
Если вы занимаетесь разработкой программ хоть сколько-нибудь
длительное время, то почти неизбежно рано или поздно вам потребуется написать
приложение, используемое в Wide World Web или Internet. В главе 9 вы уже
познакомились с методиками разработки программ-клиентов, обеспечиваю-
щих доступ к сети. Однако в дополнение к клиенту, обращающемуся к
Internet, модель клиент/сервер требует, чтобы где-то имелся также сервер,
посылающий в ответ информацию клиенту. Хотя существует много протоколов,
используемых при обмене данными через Internet, наиболее популярными из
них являются сегодня, вероятно, HyperText Transport Protocol (HTTP) и
Transport Control Protocol / Internet Protocol (TCP/IP). Эти два протокола
необходимы для передачи страниц Web клиентам-обозревателям — что является
самой частой функцией Internet кроме, возможно, электронной почты.
В этой главе вы познакомитесь с моделью клиент/сервер, принятой в
Internet. Мы рассмотрим две программы, применяющие принципы обмена
данными Internet для генерации страниц или объектов Web, доступных
пользователям сети.
Архитектура CGI
Исторически приложения, способные генерировать специальные отклики
Web, создавались на основе архитектуры CGI, что значит Common Gateway
Interface (интерфейс стандартного канала). Такие программы по сути являлись
простыми ехе-расширениями. Если клиент просит сервер запустить программу,
последний порождает совершенно отдельный процесс и «похищает» результаты
программы, читая данные, которые она пишет в устройство стандартного
вывода. Реализуется это переадресацией стандартного вывода на конвейер, с точки
зрения порожденной программы напоминающий обычный файл.
В большинстве подобных приложений сервер затем читает этот файл и
некоторое время хранит его, перед тем как отослать на машину клиента. Как
только порожденный процесс закончит свои действия и операционная система
сервера завершит его, сервер посылает результаты клиенту. Такой подход к
разработке расширений сервера понять очень просто. Поскольку такое
приложение является самой обычной программой на C/C++, разработчик может
создать ее с помощью стандартных инструментов.
Обратной стороной этой простоты разработок CGI является огромный объем
работы, которую приходится выполнять серверу для запуска и управления
порожденным процессом. В конце концов, порождение процесса требует от
системы выделения значительного количества памяти, чтения исполняемого
образа программы из файла и его последующего запуска. При работе программы
операционная система должна обеспечить коммуникацию между
приложением и сервером. Более того, в большинстве систем переброска данных от одного
процесса к другому в такого рода архитектуре никак не является простой и
быстрой операцией. Наконец, по завершении программы нужно привести все в
исходное состояние. И не следует забывать, что сервер должен запустить
отдельную копию процесса для каждого из тех 50000, или около того,
пользователей, которые в данный момент могут посылать тот же самый CGI-запрос.
C/C++ в разработках CGI
607
Однако благодаря своей мощности и гибкости подход CGI все еще
применяется большинством программистов, создающих приложения для среды
Web, — хотя в последние годы появилось много модификаций этой модели.
Например, серверы на платформе Windows NT поддерживают не только
CGI-приложения, но и гораздо более эффективный метод реализации
расширений, называемый ISAPI (он выходит за рамки тематики этой главы).
Важно понять, что приложения CGI можно писать на любом языке.
Большинство разработчиков используют либо PERL, язык, предназначенный
специально для написания CGI-программ, либо C/C++. Однако если ваш сервер
работает в Windows NT (или совместимой системе), то можно, при желании,
писать такие программы и на Visual Basic. Вдобавок большинство
производителей баз данных создали в последнее время инструменты, помогающие
строить серверы, посылающие файлы клиентам. Многие инструменты разработки,
например, Cold Fusion, также предусматривают простые средства раширения
функциональных возможностей серверов. Но ничто не сравнится с C/C++ в
плане эффективности и гибкости.
[ ЗАМЕЧАНИЕ ПРОГРАММИСТА
Большинство работающих в этой области программистов называют
все CGI-приложения — написаны ли они на C/C++, PERL или ином
языке — «сценариями CGI» (CGI scripts). В этой главе термины «CGI-сце-
нарий» и «CGI-приложение» будут употребляться как синонимы.
FTP и HTTP: сохранение статуса
Как вы, вероятно, знаете, Internet поддерживает несколько протоколов, в
том числе FTP и HTTP. FTP обеспечивает непрерывное соединение с Internet,
пока не произойдет ошибки или вы не разорвете соединение сами. Поскольку
соединение FTP непрерывно, это соединение, сохраняющее статус. С другой
стороны, протокол HTTP не имеет постоянного статуса соединения.
Соединение без статуса означает, что взаимодействующие обозреватель и сервер
устанавливают сетевое соединение, но впоследствии разрывают его. Например,
при соединении с узлом Web ваш обозреватель и сервер создают соединение,
позволяющее серверу загрузить из узла в обозреватель файл HTML (HyperText
Markup Language).
После того, как обозреватель получит файл, сервер разрывает соединение.
В процессе синтаксического разбора файла (т. е. разбиения его на различные
компоненты) обозревателю могут встретиться HTML-ссылки на изображения,
программы Java или на другие объекты, которые также должны загружаться
с сервера. Всякий раз, когда обозревателю требуется загрузить файл, он
должен устанавливать новое соединение с сервером.
Главной причиной разработки некоторых новых стандартов HTML (таких,
как Dynamic HTML) является то, что обмен данными без сохранения статуса
по самой своей природе происходит медленно. С одной стороны, из-за
необходимости установки соединения для каждого передаваемого файла (и,
соответственно, задержек в доставке его содержимого пользователю) потенциал Web
608
Глава 16
остается в значительной степени нереализованным. С другой стороны, HTML
без постоянного статуса соединения гораздо более эффективен (с точки зрения
сервера), чем предлагаемые новые стандарты. Рис. 16.1 иллюстрирует
различие между обработкой сервером HTML без статуса и обработкой HTML,
поддерживающего статус соединения.
Клиент
Без статуса
Запрос
Соединение
Отклик
Нет
соединения
Сервер
С сохранением статуса
Запрос .
Клиент
Соединение
Отклик
Соединение
Ожидание
Сервер
Клиент
Сервер
Клиент
следующего
запроса
Сервер
Рис. 16.1. Обмен данными HTML без сохранения статуса соединения и предлагаемая схема
HTML, сохраняющего статус
Эффективность коммуникации без сохранения статуса
Отдельная пара запрос/отклик HTTP называется транзакцией. HTTP
использует соединение TCP/IP, поддерживаемое только на протяжении одной
транзакции. TCP/IP является набором протоколов, определяющих форму и
транспортировку сообщений в Internet. Ни клиент (обычно запускающий
обозреватель Web), ни сервер не помнят последнего состояния соединения.
Представьте себе, как вы перемещаетесь по узлам Web, и вы поймете смысл
такой стратегии HTTP-транзакций. Как вы знаете, щелчок кнопки мыши на
гипертекстовой ссылке (гиперссылке) перемещает вас в другой узел. Так как
это может произойти в любой момент, для сервера вполне разумным будет
предположить, что вы собираетесь совершить переход к другому узлу, поэтому
он заранее обрывает соединение. Если вы никуда не переходите, сервер просто
создает новое соединение. Но при переходе ему больше ничего не нужно
делать — соединение уже разорвано. Освобождение соединения позволяет
серверу отвечать другим клиентам и, таким образом, общая эффективность сервера
повышается.
Однако в последнее время разработчики серверов экспериментируют с
кэшированием соединений, когда сервер не закрывает соединение немедленно
после отклика. Кэширование позволяет ему быстро откликаться на
«повторные визиты» клиента. С ростом сложности узлов Web, предлагающих
пользователям все больше локальных ссылок, кэширование соединений (для
известных локальных ссылок) улучшает производительность сервера*
C/C++ в разработках CGI 609
( ЗАМЕЧАНИЕ ПРОГРАММИСТА
Не путайте кэширование соединений с локальным кэшированием,
имеющимся в большинстве обозревателей сети.
Четыре шага транзакции HTTP
Перед тем как приступить к проектированию приложений CGI,
работающих на стороне сервера, давайте исследуем отдельные шаги,
предпринимаемые двумя машинами при обмене между клиентом и сервером.
До того, как клиент и сервер смогут обмениваться данными, они должны
установить соединение. Клиенты и серверы Internet также должны до
коммуникации посредством протокола TCP/IP установить между собой соединение.
Вы знаете, что клиенты запрашивают у серверов данные, а серверы
откликаются, пересылая запрошенные данные; эти запросы и отклики
осуществляются р использованием HTTP. Вы, кроме того, уже знаете, что сервер и клиент
поддерживают свое соединение TCP/IP лишь на протяжении одной
транзакции (HTTP не сохраняет статус соединения), и что сервер обычно закрывает
соединение немедленно по ее завершении.
/ Если сложить вместе все вышесказанное, получится процесс транзакции
HTTP, состоящий из четырех шагов.
1-й шаг: Создание соединения
Перед тем, как клиент и сервер смогут обмениваться информацией, они
должны установить соединение TCP/IP — которое будет использоваться в
процессе коммуникации, необходимой для передачи данных. Для различения
протоколов приложения обращаются к различным номерам портов.
Стандартные протоколы вроде FTP и HTTP имеют, как говорят, известные номера
портов. Разработчики программ серверов и клиентов применяют это слово,
потому что определенные протоколы обычно используют одни и те же порты, хотя
и не существует никакого набора стандартов, специфицирующих данные
порты как «принадлежащие» тому или иному протоколу.
Обычно HTTP присваивают порт 80, но протокол может использовать и
другие порты — при условии, что и клиент, и сервер соглашаются в отношении
номера этого другого порта. В таблице 16.1 перечислены известные
присваивания портов для часто применяемых в Web и Internet протоколов.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
TCP/IP рассматривает все порты ниже 1024 как привилегированные,
и все известные присваивания портов попадают в эту категорию. Вы
никогда не должны назначать своим собственным портам номера
меньшие 1024.
20 Зак. 1308
610
Глава 16
Таблица 16.1. Известные в Internet назначения портов
Протокол
File Transfer
Telnet
Simple Maii Transfer
Trivial File Transfer
Gopher
Finger
Hypertext Transfer
Номер порта
21
23
25 '
69
70
79
80
2-й шаг: Запрос клиента
Каждый запрос HTTP, направленный клиентом серверу, начинается с
метода запроса, за которым следует URL объекта. К методу и URL добавляет
номер используемой версии протокола HTTP и пару символов возврата
каретки/перевода строки. В зависимости от запроса обозреватель может после
CR/LF передать информацию, кодируемую в соответствии с определенным
стилем заголовка. За эти снова следуют CR/LF. Весь запрос, опять-таки в
зависимости от его природы, может сопровождаться телом запроса (в коде Multi-Pur-
pose Internet Mail Extensions — MIME).
Метод HTTP — это команда, которой клиент идентифицирует цель запроса
к серверу. Все методы HTTP соответствуют ресурсу (указанному клиентом
URL). Клиент также специфицирует версию протокола (например, HTTP 1.0).
Вместе метод, URL и версия протокола HTTP составляют строку запроса. Она
является одним из разделов поля заголовка запроса. Например, клиент может
указать метод HTTP GET для запроса у сервера графики для страницы Web.
Поле заголовка запроса сообщает серверу информацию о самом запросе и о
клиенте, его пославшем. Тело запроса содержит просто необходимую
информацию поддержки. При его составлении клиент обычно использует имя
данных, которые должен передать сервер. Рис. 16.2 показывает процесс,
осуществляющийся клиентом и сервером, когда они устанавливают соединение и
клиент посылает запрос.
Запрос HTTP
Соединение
Internet
TCP/IP
Клиент
(обозреватель)
Отклик HTTP
Сервер
Рис. 16.2. Коммуникация между клиентом и сервером при запросе HTTP
C/C++ в разработках CGI
611
3-й шаг: Отклик сервера
После того, как сервер Web получил и интерпретировал сообщение запроса,
он откликается на него, посылая ответное сообщение HTTP. Ответное
сообщение всегда начинается версией протокола HTTP, за которым следует код
состояния из трех цифр и объясняющее предложение. Затем передается пара
CR/LF и конкретная информация, запрошенная клиентом, которую сервер
кодирует в соответствии со стилем, определенным в заголовке. Наконец, сервер
снова посылает CR/LF и опциональный блок тела.
Код состояния представляет собой три цифры, сообщающие, насколько
сервер смог понять и удовлетворить запрос. Объясняющее предложение — просто
короткое словесное описание этого кода. В совокупности версия HTTP, код
состояния и объясняющее предложение составляют строку состояния.
Заголовок отклика может содержать специфическую информацию,
относящуюся к запрошенному ресурсу, и любые объявления MIME, необходимые
серверу для передачи ответа. Когда сервер Web посылает клиенту заголовок
отклика, он обычно включает в него ту же самую информацию, что
содержалась в заголовке запроса клиента. Блок тела ответа (состоящий из отдельных
байтов) в ответе сервера содержит данные, которые должны быть переданы
клиенту. З^ис. 16.3 поясняет отклик сервера на запрос.
1 Соедине
1 «1
^ TCP/i
Клиент N.
Заголовок
Содержимое
ние
t
3 )
Соединение
Ответное
сообщение
Сервер
Рис. 16.3. Отклик сервера на запрос клиента
4-й шаг: Разрыв соединения сервером
За закрытие соединения TCP/IP с клиентом по выполнении запроса
последнего отвечает сервер. Однако и сервер, и клиент должны обрабатывать
непредвиденные разрывы соединения. Другими словами, если вы нажимаете в своем
обозревателе кнопку «Стоп», обозреватель должен закрыть соединение.
Следовательно, еще работоспособный компьютер должен распознавать аварийное
состояние на другом конце соединения. Он, со своей стороны, также должен
закрыть соединение. В любом случае, когда одна или обе стороны закрывают
соединение, текущая транзакция всегда заканчивается, вне зависимости от ее
статуса. Рис. 16.4 показывает все четыре шага транзакции HTTP.
20*
612
Глава 16
Шаг1
Создание соединения
Клиент
Сервер
Шаг 2
Клиент
Соединение
Internet
TCP/IP
Сообщение
запроса
Сервер
ШагЗ
Клиент
Соединение
Internet
TCP/IP
Ответное сообщение
Сервер
Шаг 4
Клиент
Разрыв
соединения
Сервер
Рис. 16.4. Описанные четыре шага составляют полную транзакцию HTTP
Подробнее об URI
Читая литературу по Web, вы наверняка встречали термин универсальный
идентификатор ресурса (Uniform Resource Identifier, сокращенно URI). В
различных текстах URI называют адресами Web, едиными идентификаторами
документов, едиными локаторами ресурсов (URL), едиными именами ресурсов
(URN). HTTP определяет URI как форматированную строку, в которой
используются имена, местоположение и другие характеристики, определяющие
сетевой ресурс. Другими словами, URI представляет собой простую текстовую
строку, адресующую объект Web. Понимание элементов URI и URL
необходимо, поскольку передаваемая в них клиентом информация часто важна для
написания нетривиальных CGI-программ.
Внутри URL
Чтобы найти документ Web, вам нужно знать его адрес в Internet. Этот
адрес называют единым локатором ресурса (URL). Отношение ресурса к URL
можно сравнить с отношением книги к ее предметному указателю. Чтобы
отыскать что-то в книге, вы пользуетесь предметным указателем. Чтобы найти
ресурс Web, вы используете его адрес (URL).
C/C++ в разработках CGI
613
Базовый синтаксис URL прост. URL состоит из двух частей, как показывает
следующий фрагмент кода:
<схема>: Специфическая-информация-схемы>
Следующий фрагмент показывает полный синтаксис URL протокола HTTP:
http: //<машина>: <пора?>/<путь>?<запрос>
Как видите, <схема> URL представлена http, а <специфическая-информа-
ция> идентифицирует машину, необязательный порт и опциональные
маршрут и информацию-поиска. Ели элемент порт опущен, для URL по
умолчанию будет принят порт протокола 80 (известный порт HTTP). Вы часто можете
видеть его в комбинации со страницами сценариев, передающих запросы.
(Машины поиска, например, используют строку информации-поиска при
выполнении запрошенного пользователем поиска.)
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
URL не являются уникальной особенностью Web. На самом деле они
используются и в нескольких других протоколах, включая FTP, Gopher и
Telnet. Однако все URL имеют одно назначение: идентификацию адреса
объекта в Internet.
URL, протоколы и типы файлов
URL^e только сообщает адрес объекта Internet, но также описывает
протокол, который должно использовать приложение для доступа к этому объекту.
Например, схема HTTP в URL указывает на пространство (область) Web, в то
время как схема FTP — на пространство FTP. Пространство в Internet можно
представить себе как некоторую область, зарезервированную для информации
определенного типа. Например, все FTP-документы Internet располагаются в
пространстве FTP,
URL может также содержать идентификатор ресурса документа. Он
специфицирует формат документа — при условии, что автор последнего
придерживался определенных соглашений об именовании ресурсов. Например, файл
и расширением html должен содержать текст в формате HTML, а в файле с
расширением аи должен быть записан звук.
Фрагменты URL
При изучении URL проще будет разбить его на отдельные фрагменты,
чтобы определить, на что именно он ссылается. Например, рассмотрите
следующий (фиктивный) URL:
http://www.osborne.com/books/index.htm
В данном примере <схема> специфицирует протокол HTTP. Две дробных
черты после двоеточия указывают, что объект является объектом Internet. За
ними идет адрес сервера, в данном случае www.osborne.com. Следующая
дробная черта отделяет маршрут каталога, books, а за последней дробью идет имя
614
Глава 16
(index) и необязательный идентификатор ресурса документа,
соответствующего желаемому объекту (расширение htm).
Разбиение URL на отдельные фрагменты важно, когда вы создаете
относительные URL, обсуждаемые далее в этой главе.
URL и HTML
Детальное обсуждение языка Hypertext Markup Language (HTML) не
входит в задачи этой книги. Что касается вопросов, затрагиваемых в данной
главе, вы можете рассматривать HTML как язык, используемый для
структурирования документов Web. Важной частью этой структуры являются
гиперссылки. Когда обозреватель преобразует документ для отображения на экране,
он обычно подсвечивает гиперссылки, чтобы они выделялись на фоне
обычного текста. При создании документа Web язык HTML позволяет вам управлять
формированием каждой гиперссылки, которую вы вводите в документ.
Для представления ссылки в документе Web используется специальный
элемент HTML, называемый якорем. Якорь — это тег, помещаемый автором в
документ для спецификации ссылки (соответствующего URL), которую
обозреватель должен ассоциировать с конкретным текстом или графическим
изображением. Внутри якорного элемента размещается URL, информирующий
обозреватель об адресе ассоциированного ресурса.
Следующий пример показывает ссылку на URL с именем klander.htm,
который находится в иерархии каталогов на два уровня выше текущей страницы
(об этом рассказывается в следующем разделе). В примере также имеется
ссылка на графический GIF-файл.
<А target="main" href-"../../klander.htm"> (
<img align=bottora src=" ../. ./chewie.gif "x/a>
Другими словами, якорь содержит URL ресурса, присоединенного к
гипертексту или, как в этом примере, графическое изображение.
Абсолютные и относительные URL
Вы уже знаете, что гипертекст является документом, содержащим
гиперссылки. Web — это настоящий лабиринт гиперссылок. Когда разработчик
создает документ Web, он обычно соединяет его с другими документами,
созданными им самим или кем-то еще; документы могут соединяться и с
видеофайлами, графикой и другими интерактивными объектами. Для каждой связки
требуется адрес URL, указывающий на соответствующий объект. Как вы уже
знаете, обозреватели используют URL для поиска Web-документов. Адреса
URL бывают двух типов: абсолютные и относительные.
Абсолютный VRL специфицирует полный адрес объекта и протокол.
Другими словами, если в URL присутствует <схема>, то это абсолютный URL. Вот
пример абсолютного URL:
http://www.osborne.com/index.htm
C/C++ в разработках CGI 615
Относительный URL, с другой стороны, специфицирует адрес по
отношению к текущему документу, открытому в обозревателе. Используя ту же
самую <схему>, адрес сервера и маршрут каталога, что и у текущего документа,
обозреватель реконструирует URL, подставляя имя файла и расширение из
указанного относительного URL.
Рассмотрите, например, показанный выше абсолютный URL:
http://www.osborne.com/index.htm
Если гиперссылка в HTML-документе содержит спецификацию
относительного URL AnnotatedArchives /cc++,htm,
<А HREF="/AnnotatedArchives/cc++.htm"> C/C++ Annotated Archives Page
<A/>
то обозреватель реконструирует этот URL в виде http://www.osborne.com/Anno-
tatedArchives/cc++-Mm.
| ЗАМЕЧАНИЕ ПРОГРАММИСТА
Если вы вводите одиночную точку (.) перед относительным. URL
(например, AnnotatedArchives/cc++.htm), то результат будет тот же,
что и в случае ввода AnnotatedArchives/cc++.htm.
Место CGI в модели Web
С точки зрения пользователя модель HTTP является довольно пассивной,
не предлагая никакой или почти никакой интерактивности. Коротко говоря,
пользователь может просто просматривать содержимое страницы Web, не
взаимодействуя с его элементами. Таким образом, интерактивная модель
явилась естественным следующим шагом в эволюции Web. Эта
интерактивность была достигнута благодаря применению интерактивных форм,
создаваемых средствами CGI, включая PERL, C/C++, TCL и другие языки
программирования.
Используя элементы HTML, Web-разработчик может создать форму,
позволяющую пользователям (при посредстве их обозревателей) взаимодействовать
с сервером. Когда пользователь нажимает на своей форме кнопку «Передать»,
обозреватель посылает форму серверу, который, в свою очередь, запускает
программу (написанную нередко на PERL или C/C++), обрабатывающую
данные формы. В зависимости от назначения серверной программы она может
генерировать отклик в виде текста HTML, который сервер отсылает обратно
обозревателю.
Рис. 16.5 показывает модель клиент/сервер с использованием CGI.
Обратите внимание на то, как интерфейс CGI работает с информацией, поступающей
от клиента, производя ее обработку и генерируя результат (который снова
отсылается клиенту). Такая модель взаимодействия является основой всех
операций CGI (хотя можно создавать и приложения CGI, не генерирующие
результатов в форме HTML, и т. п.).
616
Глава 16
запрос
Активация программы CGI
/
страницы
HTML
Машина
клиента
Рис. 16.5. Модель клиент/сервер, использующая CGI
Разработка компонента часов
Теперь, когда вы освоили основные элементы коммуникации HTML и
имеете понятие о том, как происходит взаимодействие с CGI, мы можем
разработать простое CGI-приложение, которое генерирует текстовое представление
текущих даты и времени, посылая полученный результат клиенту.
ЗАМЕЧАНИЕ
L^
Важно отметить, что этот компонент не генерирует полной страницы
HTML, а только некоторую информацию в стиле HTML для включения в
другую страницу.
Компонент часов
Программа, реализующая компонент часов, довольно прямолинейна (файл
clock, срр).
|#include <iostream>
I #include <string>
I #include <ctime>
(using namespace std;
// Определение констант
const int Display_Week_Day = 1;
[const int Display_Month = 1;
const int Display_Month_Day = 1;
[const int Display_Year = 1;
const int Display_Time = 1;
const int Display_Time_Zone = 1;
C/C++ в разработках CGI 617
* 4
г *
У
Г
4
■r »
i.
const char Standard_Time_Zone[4] = "EST";
const char Daylight__Time_Zone[4] = "EDT" ;
/л****************************************************************/
int main()
{
char Week_Days[7][10] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
char Months[12][10] = {"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"};
char Time_Zone[4];
tm *ptm;
time_t *cur_jtime;
cout « "Content-type: text/html\n\n";
// Выделить память для переменной времени и структуры tm.
cur_time = new time_t;
ptm = new tm;
// Получить время и структуру с соответствующими значениями.
time(cur_time);
ptm - localtime(cur__time);
///Определить, используется ли в данный момент летнее время,
if (ptm->tm_isdst)
strcpy(Time_Zone,Daylight_Time_Zone);
else
strcpy(Time_Zone,Standard_Time_Zone);
// Вывести, если требуется, день недели.
if (Display_Week_Day) {
cout « Week_Days[ptm->tm_wday];
if (Display_Month)
cout « ", ";
}
// Вывести, если требуется, название месяца,
if (Display_Month)
cout « Months[ptm->tm_mon] « " ";
// Вывести число.
£ , if (Display_Month_Day \- 0) {
*• if (ptm->tra_mday < 10)
! cout « "0";
cout « ptm->tm_mday;
[ if (Display_Year)
cout « ", ";
}
// Вывести год.
",Jj if (Display_Year) {
cout « ptm->tm_year + 1900;
618
Глава 16
Г'
r
if (Display_Time)
cout « " - ";
else if (Display_Time_Zone)
cout « " ";
}
// Вывести время,
if (Display_Time) {
if (ptm->tm_hour < 10)
cout « "0";
cout « ptm->tm_hour « " : " ;
if (ptm->tm_min < 10)
cout « "0";
cout « ptm->tm_min «
if (ptm->tm_sec < 10)
cout « "0";
cout « ptm->tm_sec;
if (Display_Time_Zone)
cout « " ";
)
// Вывести часовой пояс,
if (Display__Time_Zone)
cout « Time_Zone;
return 0;
II . » ,
( UPV
ПРИМЕЧАНИЯ
Большинство из того, что делает программа clock.срр, достаточно очевидно.
Давайте начнем с анализа очень простых определений констант.
// Определение констант
const int Display_Week_Day = 1;
const int Display_Month = 1;
const int Display_Month_Day = 1;
const int Display_Year = 1;
const int Display_Time - 1;
const int Display_Time_Zone = 1;
const char Standard_Time_Zone[4] = "EST";
const char Daylight_JPinie_Zone[4] = "EDT";
Первые шесть из них задают целые значения, управляющие выводом
отдельных полей результата — от даты до времени и часового пояса. Например,
если вы не хотите, чтобы компонент отображал день недели, то можете просто
присвоить константе Display_Week_Day значение 0. Последние две константы
определяют в текстовом виде часовой пояс; их нужно модифицировать в
соответствии с местонахождением вашего сервера. Например, если вы живете в
Калифорнии, то их значения нужно изменить на "PST" и "PDT".
C/C++ в разработках CGI 619
После объявления констант программа входит в функцию main(),
генерируя действительную информацию, которая будет передаваться клиенту:
int main()
{
char Week_Days[7][10] = ("Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
char Months[12][10] = {"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"};
char Time_Zone[4];
Сначала код определяет несколько массивов, которые затем будут
использоваться для формирования текста результата. Week_Days — многомерный
массив символов, соответствующий семи дням недели. Аналогично, Months
соответствует двенадцати месяцам года. Массив Time_Zone хранит
информацию о текущем часовом поясе — означающую либо стандартное, либо летнее
время.
Затем программа объявляет переменные для внутреннего хранения
информации. Программа будет использовать их для конструирования компонентов
строки с датой и временем:
tm *ptm;
time_t *cur_time;
Первый вывод в cout говорит принимающему узлу (т. е. в данном случае —
обозревателю клиента), что поступающая к нему информация является
текстом, форматированным как HTML:
cout « "Content-type: text/html\n\n";
Как правило, когда вы передаете клиенту данные по конвейеру CGI, вы
всегда форматируете их как text/html. Это позволяет вашему CGI-приложению
посылать клиенту законченные HTML-документы, включающие в себя
различные компоненты с тегами и т. п. (В следующем программном примере вы
увидите форматирование HTML.)
Как и положено, после создания переменных для хранения информации их
нужно инициализировать — здесь для них выделяется память:
// Выделить память для переменной времени и структуры tm.
cur_time = new time_t;
ptm = new tm;
Однако им еще не присвоены действительные значения времени, что и
делается ниже:
// Получить время и структуру с соответствующими значениями.
time(cur_time);
ptm = local time (cur_time) ;
Функция time возвращает число секунд, прошедших с полуночи (00:00:00)
1-го января 1970 года — важное значение, но не особенно полезное, если не
произвести над ним необходимых операций. К счастью, в C/C++ для
упрощения этих вычислений предусмотрена функция localtime. Она преобразует
время как значение типа time_t в структуру типа tm. функция возвращает
указатель на структуру результата (который вы, в свою очередь, присваиваете
указателю ptm).
620
Глава J 6
В полях структуры хранятся следующие значения (все типа int):
tm sec
tm min
tm hour
tm_mday
tm mon
tm year
tmjwday
tm yd ay
tmjsdst
Секунды после целой минуты (0-59)
Минуты после целого часа (0-59)
Час после полуночи (0-23)
День месяца (1-31)
Месяц (0-11)
Год (текущий минус 1900)
День недели (0-6); 0 = воскресенье
День года (0-365)
Положительно, если в данный момент действует соглашение о летнем
времени (dst); в противном случае 0; отрицательно, если об этом ничего
не известно. Исполнительная библиотека C++ вычисляет значения для
летнего времени в соответствии с правилами, принятыми в США.
Затем вы используете эти значения для определения информации о
текущем времени о соответствующих вычислений в программе CGI.
Большая часть кода достаточно очевидна — это последовательность
простых операторов if, которые производят вывод в HTML-файл в соответствии со
значениями элементов структуры tm.
// Определить, используется ли в данный момент летнее время,
if (ptm->tm_isdst)
strcpy (Time_Zone,Daylight_Time_Zone) ;
else
strcpy (Time_Zone, Standard__Time_Zone) ;
Показанный выше оператор проверяет поле tm_isdst и определяет,
соответствуют ли содержащиеся в tm значения летнему времени. Если это так, то в
переменную Time_Zone копируется идентификатор летнего часового пояса, в
противном случае — стандартного.
Затем код проверяет, нужно ли выводить день недели. Если нужно, в
HTML-файл выводится соответствующая строка из массива Week_Days:
// Вывести, если требуется, день недели.
if (Display_Weefc_Day) {
cout « Week_Days[ptm->tm_wday];
if (Display_Month)
cout « ", ";
)
Например, если сегодня воскресенье, элемент tm_wday будет иметь нулевое
значение, и клиенту будет послана строка Week_Days[0].
После вывода дня недели программа проверяет, будет ли печататься
название месяца, и если это так, выводит запятую, разделяющую только что
выведенный день недели и строку месяца. Если месяц вообще выводится,
следующий оператор посылает в cout название месяца (соответствующее значению
поля tm_mon):
// Вывести, если требуется, название месяца.
if (Display_Month)
cout « Months[ptm->tm_mon] « " ";
C/C++ в разработках CGI : 62Г
Следующий оператор if производит аналогичные действия для дня месяца,
но выводит дополнительный 0, если его значение меньше 10. Это делается
ради более аккуратного и единообразного представления даты и времени. Если
день месяца больше 9, программа просто выводит его. Затем проверяется,
будет ли показан год, и если будет, то выводится запятая между днем и годом.
// Вывести число,
if (Display_Month_Day !- 0) {
if (ptm->tm_mday < 10)
cout « "0";
cout « ptm->tm_mday;
if (Display_Year)
cout « "7 ";
} v
Затем выводится текущий год:
// Вывести год.
if (pisplay_Year) {
cout « ptm->tm_year + 1900;
if (Display_Time)
cout « " - ";
else if (Display_Time_Zone)
cout « " ";
)
При этом к значению в элементе tm_year прибавляется 1900. Если год
больше 1999, он все равно будет выведен корректно. Что касается остальных
операторов, то они определяют, что будет выведено дальше, и выводят либо
дефис, если за этим последует время, или пробел, если будет выводиться только
индикатор часового пояса.
Следующий раздел кода выводит текущее время:
// Вывести время.
if (Display_Time) {
if (ptm->tm_hour < 10)
cout « "0";
cout « ptm->tm_hour « ":";
if (ptm->tm_min < 10)
cout « "0";
cout « ptm->tm_min « " : " ;
if (ptm->tm_sec < 10)
cout « "0";
cout « ptm->tm_sec;
if (Display_Time_Zone)
cout « " ";
}
Как и в случае с датой, к значению присоединяется ведущий 0, если оно
меньше 10; в противном случае выводится только само значение. Наконец,
определяется, будет ли выведен часовой пояс, и в зависимости от этого на выход
посылается разделительный пробел.
Последним действием программы является вывод часового пояса — в
результате чего в обозревателе появится вся сформированная строка:
622
Глава 16
// Вывести часовой пояс,
if (Display_Time_Zone)
cout « Time_Zone;
return 0;
}
Как уже отмечалось, программа не делает ничего особенного помимо того,
что выполнялось бы при запуске ее из командной строки — что и можно
сделать, получив в результате строку с текущими датой и временем. Хитрость в
том, что при запуске программы из страницы Web результат ее появится
именно в том месте, где вы активировали компонент. Код страницы Web
вставляет туда действительный текст, возвращаемый программой. Подробнее
об этом в следующем разделе.
Сценарий CGI, использующий метод POST
протокола HTTP
Теперь, увидев элементы CGI в действии, вы должны почувствовать себя
увереннее. Вывод вашего CGI-кода на самом деле не слишком отличается от
обычного текстового вывода; в программировании CGI он просто направляется
в другое место. Второй пример, с которым вы будете работать в этой главе,
берет элементарные принципы CGI, показанные в clock.срр, и развивает их
несколько дальше, генерируя законченную Web-страницу и форму CGI.
Программа в этом примере просто отображает базовую форму,
запрашивающую у пользователя его имя, адрес электронной почты и телефон.
Пользователь может затем нажать кнопку «Send!», а сценарий CGI обработает
введенные данные и пошлет ответное сообщение.
Программа post.exe была создана в Visual C++ 6.0 на основе оболочки MFC;
однако основная обработка производится в заголовке post.h и исходном файле
post.cpp. На них и будет в дальнейшем сосредоточено наше обсуждение. Все
файлы, составляющие приложение, вы можете найти на прилагаемой дискете.
Код
Приведенный ниже листинг показывает содержимое двух
вышеупомянутых файлов.
// Post.h : главный заголовочный файл приложения POST
//
#ifndef _AFXWIN_H_
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h" // главные символы
///////////////////////////////////////////////////////////////////
// CPostApp:
C/C++ в разработках CGI 623
.]// См. код Post.cpp с реализацией данного класса
Л'//
i
п
\ * 1
class CPostDoc;
class CMainFrame;
class CPostView;
class CPostApp : public CWinApp
{
public:
CPostApp();
CFostDoc *m__pDoc;
:. ] CMainFrame *m_pFrame;
"'J\ CPostView *m_pView;
U ! CStringArray m_saParams;
J- -ml
■;1
(:.] // Overrides
' 'a // ClassWizard generated virtual function overrides
i'l //{ {AFX_VIRTUAL (CPostApp)
i\.\ public:
"rj virtual BOOL Initlnstance() ;
»,,"1 virtual int Exitlnstance () ;
t " J //}}AFX VIRTUAL
I 1
i,^j void ParseCmdLine(CString& slniFile, CStringfi sContentFile,
r : CStringfi sOutputFile);
■»y void ParselniFile(CString slniFile);
' J! void CreateResponse(CString sOutputFile, CString slniFile);
M
[' ,j //({AFX_MSG (CPostApp)
V //}}AFX_MSG
DECLARE MESSAGE MAP()
I -
[ =
};
///////////////////////////////////////////////////////////////////
// Post.cpp : Определяет поведение классов приложения.
//
#include "stdafx.h"
«include "Post.h"
#include "MainFrm.h"
«include "PostDoc.h"
#include "PostView.h"
#ifdef _DEBUG
j? 'j «define new DEBUG_NEW
=" 1#undef THIS FILE
static char THIS_FILE[] = FILE
I'-Uendif
624
Глава 16
- *///////////////////////////////////////////////////////////////////
Л// CPostApp
BEGIN_MESSAGE_MAP(CPostApp, CWinApp)
// { {AFX_MSG~MAP (CPostApp)
//)}AFX_MSG_MAP
ON_COMMAND (ID_FILE_NEW, CWinApp: : OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp: :0nFileOpen)
END_MESSAGE_MAP()
// Конструктор CPostApp
CPostApp::CPostApp()
|<
j m_pView = NULL;
J m_pFrame = NULL;
t
1)
'i
[// Единственный объект CPostApp
jCPostApp theApp;
1
^///////////////////////////////////////////////////////////////////
// Инициализация CPostApp
^BOOL CPostApp::Initlnstance()
LoadStdProfileSettings(); // Загрузить стандартные опции INI-файла
mjpFrame = new CMainFrame;
m_pView = new CPostView;
CCreateContext newContext;
newContext.m_pNewViewClass = NULL;
newContext.m_pNewDocTemplate = NULL;
newContext.mjpLastView = NULL;
newContext.mjpCurrentFrame = NULL;
newContext.m_pCurrentDoc = NULL;
DWORD dwStyle = WS_OVERLAPPED;
// Создать обрамление с нулевыми размерами,
< // так как это фоновый процесс
\ ■ if (!m_j>Frame->Create(NULL, NULL, dwStyle, CRect(0,0,0,0),
. t NULL, NULL, OL, finewContext))
I return FALSE;
i
m_pMainWnd = m_pFrame;
// Создать вид с нулевыми размерами
m_pView->Create(NULL,NULL,WS_CHlLD,CRect(0,0,0,0) ,m_pFrarne,
AFX_IDW_PANE_FIRST,finewContext) ;
m_pView-X)nInitialUpdate () ;
C/C++ в разработках CGI
625
м/4 if (m_lpCmdLine[0] != '\0') {
f *l CString sIniFile;
:" CString sContentFile;
i>. CString sOutputFile;
][Ji ParseCmdLine {sIniFile, sContentFile, sOutputFile) ;
ParselniFile(sIniFile.GetBuffer(O));
CreateResponse(sOutputFile, sIniFile);
J
PostQuitMessage(0);
return TRUE;
t
Г
I"
int CPostApp::ExitInstance ()
{
if (m_pView) // очистка
delete m_pView;
if (m_pFrame)
delete mjpFrame;
* j return 0;
■ jvoid CPostApp::ParseCmdLine(CStringS sIniFile,
', I CStringfi sContentFile, CStringfi sOutputFile)
4 J
j CString sCmdLine = m_lpCmdLine;
int nCmdPos = 0;
int nWordPos = 0;
!■' I
&
i.
while (nCmdPos < sCmdLine.GetLength() &&
(sCmdLine [nCmdPos] = ' ' |j sCmdLine[nCmdPos] = '\t'))
nCmdPos++;
nWordPos = nCmdPos;
while (nCmdPos < sCmdLine.GetLength() &&
sCmdLine[nCmdPos] !=''&& sCmdLine[nCmdPos] != '\t')
nCmdPos++;
sIniFile = sCmdLine.Mid(nWordPos, (nCmdPos - nWordPos));
while (nCmdPos < sCmdLine.GetLength() &&
(sCmdLine[nCmdPos] == ' ' || sCmdLine[nCmdPos] == '\t'))
p nCmdPos++;
6 •A nWordPos = nCmdPos;
v "A
\ while (nCmdPos < sCmdLine.GetLength() &&
Z.]~] sCmdLine [nCmdPos] '= ' ' && sCmdLine [nCmdPos] != ' \t*)
u'l nCmdPos++;
I *
} sContentFile = sCmdLine.Mid(nWordPos, (nCmdPos - nWordPos));
i while (nCmdPos < sCmdLine.GetLength() &&
1«j (sCmdLine[nCmdPos] == ' ' || sCmdLine[nCmdPos] == '\t'))
626
Глава 16
nCmdPos++;
nWordPos = nCmdPos;
l_ *j while (nCmdPos < sCmdLine .GetLength () &&
* sCmdLine[nCmdPos] 1= ' ' && sCmdLine[nCmdPos] •= '\t')
nCmdPos++;
sOutputFile = sCmdLine.Mid(nWordPos, (nCmdPos ~ nWordPos));
}
^(f| nBufLen = GetPrivateProfileSection("Form Literal",
(LPSTR)IpszBuf,2048,slniFile.GetBuffer(O) );
do {
sLine =
while (nlniPos < nBufLen && IpszBuf[nlniPos] != • \0') {
r
1 r
r
void CPostApp::ParseIniFile(CString slniFile)
{
int nlniPos = 0;
int nBufLen;
CString sLine;
char IpszBuf[2048];
sLine += IpszBuf[nlniPos]
nIniPos++;
}
!■ nIniPos++;
* ! if (sLine != "") {
m_saParams.Add(sLine);
}
f* } while (sLine != "");
}
void CPostApp::CreateResponse(CString sOutputFile, CString slniFile)
{
int i;
Ьч" int nlndex;
CString sResponse;
CFile fOutput;
CFileStatus fStatus;
CString sUser;
CString sKey;
CString sVal;
%
// Послать формат HTML
sResponse = "Content-type: text/html\r\n";
sResponse +— "\r\n";
SResponse += "<!DOCTYPE HTML PUBLIC \"-//W30//DTD W3 HTML
3.0//EN\">\r\n";
! sResponse += "<HTML>\r\n";
sResponse += "<HEAD>\r\n";
sResponse += "<TITLE>POST Application</TITLE>\r\n";
Lb sResponse +- "</HEAD>\r\n";
C/C++ в разработках CGI 627
i ч
for (i =0; i < m__saParams.GetSize{) ; i++) {
sKey.EmptyO ;
sVal.Empty() ;
if ((nlndex = m_saParams[i].Find('=')) != -1) {
sKey = m_saParams[i].Left(nlndex);
sVal = m_saParams[i].Right(m_saParams[i].GetLength() -
nlndex - 1);
г | }
' j if (sVal = "") {
[" ] sResponse += "<Hl>Please enter all data</Hl>\r\n";
"j break;
г ' >
I sUser += "<H3>";
1 J sUser += sKey;
. j sUser += ": ";
ij sUser += sVal;
*. , sUser += "</H3>\r\n";
i
i
l*>
}
if (i -= m_saParams.GetSize()) {
- sResponse +- "<Hl>Server's Reply:</Hl>\r\n";
sResponse += sUser;
J
sResponse +- "</BODY>\r\n";
sResponse += "</HTML>\r\n";
if (fOutput.Open(sOutputFile.GetBuffer(0),
CFile::modeCreate | CFile::modeWrite | CFile::typeBinary)) {
fOutput.Write(sResponse.GetBuffer(O),sResponse.GetLength());
fOutput.Close()i
)
| HPk
ПРИМЕЧАНИЯ
Заголовок post.h начинается специфическим для MFC кодом, а затем
подключает файл с ресурсами проекта. В нем определяются некоторые ресурсы
строк, а также информация о виде (хотя приложение и не реализует вид).
Давайте посмотрим на содержимое заголовочного файла:
// Post.h : главный заголовочный файл приложения POST
//
#ifndef AFXWIN H_
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h" // главные символы
Следующие четыре объявления относятся к представителям классов,
используемых в приложении. Класс CPostDoc хранит информацию о классе
документа. Класс CPostApp содержит информацию о приложении Windows и
является производным от определяемого MFC класса CWinApp.
628
Глава 16
class CPostDoc;
class CMainFrame;
class CPostView;
class CPostApp : public CWinApp
{
CPostApp включает в себя конструктор по умолчанию и четыре элемента
данных, что показано ниже. Три из них являются указателями на другие
классы приложения. Четвертый определяет объект CStringArray,
поддерживающий массив из объектов-строк.
public:
CPostApp();
CPostDoc *m_j?Doc;
CMainFrame *mjpFrame;
CPostView *mjpView;
CStringArray m_saParams;
После объявления этих открытых элементов заголовок определяет
функции, вызываемые классом:
// ClassWizard generated virtual function overrides
//{ {AFX_VIRTUAL(CPostApp)
public:
virtual BOOL InitlnstanceO ;
virtual int Exitlnstance();
//}JAFX_VIRTDAL
void ParseCmdLine(CString& slniFile, CStringfi sContentFile, "
CStringfi sOutputFile);
void ParselniFile(CString slniFile);
void CreateResponse(CString sOutputFile, CString slniFile);
Функции InitlnstanceO и Exitlnstance() являются расширениями,
специфическими для Windows, которые осуществляют инициализацию и очистку
каждого экземпляра приложения. Три других функции — пользовательские и
вызываются для действительной обработки данных. ParseCmdLine()
производит синтаксический разбор командной строки. ParseIiniFile() анализирует
INI-файл, возвращаемый функцией ParseCmdLine(). Наконец, CreateRespon-
се() генерирует конечный код HTML, посылаемый программой клиенту.
Завершающий код заголовочного файла объявляет таблицы сообщений
Visual C++, которые автоматически создаются оболочкой при генерировании
нового приложения. В приложении Post они не играют никакой существенной
роли.
//{ {AFXJfSG(CPostApp)
//})AFX_MSG
DECLARE_MESSAGE_MAP()
);
Директивы #include в показанном далее файле post.cpp просто
подключают необходимые заголовки для других используемых в приложении классов,
а также специфический заголовок MFC, необходимый для корректного
запуска приложения.
C/C++ в разработках CGI ___™ G2&
«include "stdafx.h"
«include "Post.h"
«include "MainFrm.h"
«include "PostView.h"
Идущая далее последовательность условных директив служит для
поддержки интегрированного отладчика Visual C++. Переменная THISFILE
просто устанавливается равной имени файла post.cpp, возвращаемому макросом
__FILE__.
«ifdef _DEBUG
«undef THIS_FILE
static char THTS_FILE[] = FILE ;
#endif
Раздел таблицы сообщений объявляет стандартные функции для
приложения MFC. Два макроса, определяемые таблицей, создаются по умолчанию для
оболочки MFC, которую использует Visual C++; они не выполняют в этом
приложении ничего полезного и оставлены просто для того, чтобы файл содержал
все элементы, необходимые для правильной его компиляции.
BEGIN_MESSAGE_MAP(CPostApp, CWinApp)
//{{AFX_MSG_MAP(CPostApp)
//}} AFX_MSG_MAP
_jON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
END_MESSAGE_MAP()
Конструктор класса CPostApp инициализирует указатели двух
вспомогательных классов нулями. Это делается потому, что приложение при генерации
вывода не использует ни обрамление, ни вид. Это просто заглушки на случай,
если они понадобятся вам впоследствии при расширении возможностей
программы.
CPostApp::CPostApp()
{
m_pView = NULL;
m_pFrame - NULL;
}
Каждое приложение, использующее оболочку MFC, должно
инициализировать единственный объект, соответствующий данному приложению:
CPostApp theApp;
Как правило, оболочка называет этот объект theApp, и вы не должны
менять его имя.
Функция Initlnstance() вызывается всякий раз при создании нового
экземпляра пришожения:
BOOL CPostApp::lnitlnstance()
<
LoadStdProfileSettings(); // Загрузить стандартные опции INI-файла
Другими словами, если доступ к CGI-приложению запрашивают десять
пользователей, Initlnstance() будет вызвана десять раз, для каждого
экземпляра приложения, открытого пользователями. Функция LoadStdProfileSet-
630
Глава 16
tings() — также специфическая для API Windows и загружает информацию о
последних обрабатывавшихся файлах и т. п.
Следующие две строчки инициализируют элементы, объявленные в файле
post.h, соответствующие единственным представителям классов главного окна
и вида.
m_pFrame = new CMainFrame;
m_j?View - new CPostView;
Структура CCreateContext содержит указатели на документ, окно
обрамления, вид и шаблон документа. Программа создает ее представитель с именем
newContext:
CCreateContext newContext;
Она также содержит указатель на CRuntimeClass, идентифицирующий тип
создаваемого вида. Информация о классе времени выполнения и указатель
текущего документа используются при динамическом сознании нового вида.
Оболочка MFC использует структуру CCreateContext, когда создает окно
обрамления и ассоциированные с документом виды. При создании окна
значения структуры предоставляют информацию, необходимую для соединения
компонентов документа с визуальным представлением его данных. Вам
понадобится работать с CCreateContext только в случае, если вы переопределяете
что-то в процессе создания окна.
Поскольку приложение Post не использует многое из того, что имеется в
MFC-архитектуре документ/вид, оно переопределяет элементы структуры
newContext, приписывая нулевые указатели объектам, которые обычно
оболочка использует.
newContext,m_pNewViewClass = NULL;
newContext.m_pNewDocTemplate = NULL;
newContext.m_pLastView = NULL;
newContext.m_pCurrentFrame = NULL;
newContext,m_pCurrentDoc = NULL;
Переменная dwStyle содержит определяемую в Windows константу WS_OVER-
LAPPED, системное значение, определяющее поведение создаваемого окна:
DWORD dwStyle = WS_OVERLAPPED;
Эту переменную вы будете использовать при создании окна обрамления с
нулевыми размерами.
Затем создается обрамляющее окно, не имеющее размера и со стилем
OVERLAPPED, а также модифицированной структурой newContext.
if (!m_pFrame->Create(NULL, NULL, dwStyle, CRect(0,0,0,0),
NULL, NULL, 0L, finewContext))
return FALSE;
Созданное таким образом окно не имеет действительных размеров —
приложение ничего не выводит ни в какие окна (это фоновое приложение), так что
вы создаете окно с нулевой шириной и высотой.
После создания этого нулевого окна оно присваивается указателю главного
окна. Затем приложение выполняет схожие действия, создавая вид с
нулевыми размерами. Вид обновляется набором незначащих значений.
C/C++ в разработках CGI 631
m_pMainWnd = m__pFrame;
// Создать вид с нулевыми размерами
m_pView->Create(NULL,NULL,WS_CHILD,CRect(0,0,0,0), m_pFrame,
AFX_IDW_PANE_FIRST,&newContext);
m_pView->OnlnitialUpdate();
Затем код проверяет, содержит ли командная строка какие-нибудь
аргументы. Если нет, блок следующего оператора if пропускается и программа,
как вы увидите далее, завершается. Если же параметры существуют, она
создает три строковые переменные, которые будут переданы функции ParseCmd-
Line().
if (mJLpCmdLine[0] •= '\0'> {
CString slniFile;
CString sContentFile;
CString sOutputFile;
После вызова упомянутой функции и создания INI-файла программа
производит синтаксический разбор последнего. И наконец, генерируется и
отсылается клиенту сообщение отклика; это делает функция CreateResponse().
ParseCmdLine(slniFile, sContentFile, sOutputFile);
ParselniFile(slniFile.GetBuffer(O));
CreateResponse(sOutputFile, slniFile);
)
После завершения обработки программа закончит свое выполнение и
вызовет функцию Exit Ins tance(). Вообще говоря, эта функция выполняет
действия, сходные с происходящими в деструкторе — освобождение дескрипторов,
удаление выделенной памяти и т. п.
PostQuitMessage(0);
return TRUE;
}
Функция Exitlnstanse() освобождает ресурсы в порядке, обратном их
выделению. Поэтому сначала удаляется указатель не вид:
int CPostApp::ExitInstance()
{
if (m_pView) // очистка
delete m_pView;
a затем функция удаляет указатель окна обрамления:
if (m_pFrame)
delete m_pFrame;
return 0;
>
Как уже отмечалось, функция ParseCmdLine() принимает три параметра, в
которых возвращает дешифрованные аргументы командной строки:
void CPostApp::ParseCmdLine(CString& slniFile,
CStringfi sContentFile, CStringS sOutputFile)
{
CString sdndLine = m_lpCmdLine;
int nCmdPos ~ 0;
int nWordPos =0;
632
Глава 16
Функция начинается с объявления строки, в которой находится содержимое
команды. Затем она инициализирует нулями (что соответствует началу
командной строки) две целые переменные, используемые при анализе команды.
Следующий далее цикл while перебирает символы строки, пока не найдет
первый символ, не являющийся пробелом или табуляцией. Когда такой
символ найден, цикл завершается и обработка продолжается.
while (nCmdPos < sCmdLine.GetLength() &6
(sCmdLine [nCmdPos] = ' ' || sCmdLine[nCmdPos] == '\t')>
nCmdPos++;
Код устанавливает переменную, отслеживающую слова, на позицию первой
буквы, а затем входит в следующий цикл while, проходящий по строке до
конца слова:
nWordPos ж nCmdPos;
while (nCmdPos < sCmdLine.GetLength() &&
sCmdLine[nCmdPos] != ' ' && sCmdLine[nCmdPos] ! = '\t')
nCmdPos++;
slniFile — sCmdLine.Mid(nWordPos, (nCmdPos - nWordPos));
Когда цикл обнаруживает пробел, табуляцию или конец командной строки,
он завершается и обработка продолжается дальше.
Далее производятся аналогичные операции; отыскивается начало
следующего слова и определяется позиция его конца. Выделенное слово
присваивается переменной sContentFile:
while (nCmdPos < sCmdLine.GetLength() &6
(sCmdLine[nCmdPos] == ' ' || sCmdLine[nCmdPos] = '\t'))
nCmdPos++;
nWordPos = nCmdPos;
while (nCmdPos < sCmdLine.GetLength() £&
sCmdLine[nCmdPos] != ' ' && sCmdLine[nCmdPos] != '\t')
nCmdPos++;
sContentFile = sCmdLine.Mid(nWordPos, (nCmdPos - nWordPos));
Тоже самое выполняется еще один, последний, раз, и полученное третье
слово командной строки помещается в переменную sOutputFile:
while (nCmdPos < sCmdLine.GetLength() £fi
(sCmdLine[nCmdPos] — * • | | sCmdLine[nCmdPos] == '\t'))
nCmdPos++;
nWordPos = nCmdPos;
while (nCmdPos < sCmdLine.GetLength() &&
sCmdLine[nCmdPos] != ' ' fifi sCmdLine[nCmdPos] != '\t')
nCmdPos++;
sOutputFile = SCmdLine.Mid(nWordPos, (nCmdPos - nWordPos));
)
Вспомните, как выше мы говорили, что функция Initlnstance() после
разбора командной строки передает полученное от ParseCmdLine() имя файла
другой функции — ParselniFileO, которая обрабатывает содержащуюся в нем
информацию. Эта функция начинается с объявления нескольких
автоматических переменных, включая пару счетчиков, переменную CString, где будет
содержаться очередная обрабатываемая строка INI-файла, и массив char, пере-
C/C++ в разработках CGI 633
даваемый программой при вызове системной функции для чтения файла
инициализации.
void CPostApp::ParselniFile(CString slniFile)
{
int nlniPos - 0;
int nBufLen;
CString sLine;
char IpszBuf[2048];
GetPrivateProfileSection() является функцией API для чтения данных
INI-файлов. В данном случае она читает информацию файла из раздела "Form
Literal":
nBufLen = GetPrivateProfileSection("Form Literal",
(LPSTR)lpszBuf,2048,slniFile.GetBuffar(0));
Оставшаяся часть ParseIniFile() анализирует символьный массив,
полученный из файла инициализации. Для обработки его содержимого организуется
цикл do.
Этот цикл повторяется всякий раз, когда из символьного массива
извлекается целиком очередная строка (ограниченная нуль-символом). Когда процесс
доходит до конца массива — т. е. когда в sLine оказывается нулевая строка, —
цикл заканчивается.
do {
sLine - "";
Внутренний цикл while увеличивает счетчик текущей позиции, пока не
обнаружит нулевой символ, указывающий на конец текущей строки. В цикле
каждый ненулевой символ массива присоединяется к строке sLine. Когда
встречается нулевой символ, итерации цикла прекращаются.
while (nlniPos < nBufLen &£ IpszBuf[nlniPos] != '\0') {
sLine += IpszBuf[nlniPos];
nIniPos++;
>
Далее оператор if проверяет, что извлеченная из массива строка не пуста.
Бели это так, она вносится в массив строк, объявленный ранее в классе
приложения. Если же строка отсутствует, управление переходит в конец внешнего
цикла.
nlniPos++;
if (sLine !« "") {
m_saParams.Add(sLine);
}
Условие его завершения также проверяет, является ли последняя
полученная строка не пустой. Если строка непустая, цикл повторяется с начала; в
противном случае цикл завершается и функция возвращает управление.
} while (sLine !~ "");
}
Как уже говорилось, после разбора INI-файла Initlnstance() вызывает
функцию CreateResponse(). Последняя, используя содержимое массива m_sa-
Params, генерирует файл HTML, отсылаемый затем клиенту.
634
Глава 16
Функция CreatcRcsponse(), как и предыдущая, начинается с объявления
пары локальных счетчиков. Она создает также одиночную строку sResponse, в
которой будет храниться отсылаемый клиенту текст. После этого функция
создает файловый объект, в который она потом запишет сконструированную строку:
void CPostApp::CreateResponse(CString sOutputFile, СString slniFile)
{
int i;
int nlndex;
CString sResponse;
CFile fOutput;
Переменная f Status отслеживает информацию о состоянии файлового
объекта, в частности, дату и время создания и т. п. Значение sKey будет представлять
имя ключа, а в sVal будет содержаться ассоциированное с ключом значение:
CFileStatus fStatus;
CString sUser;
CString sKey;
CString sVal;
Действительная обработка начинается с создания заголовков для файла
HTML. Как вы видели в программе clock.срр, в начале указывается, что типом
содержимого файла является text/html, чтобы обозреватель знал, как
правильно интерпретировать полученный код. Затем специфицируется
заголовочная информация HTML, требуемая протоколом, и заглавие генерируемой
HTML-страницы:
// Сформировать заголовок HTML /
sResponse = "Content-type: text/html\r\n";
sResponse += "\r\n";
sResponse +=
"<!DOCTYPE HTML PUBLIC \"-//W30//DTD W3 HTML 3.0//EN\">\r\nM;
sResponse += "<HTML>\r\n";
sResponse += ,,<HEAD>\r\n" ;
sResponse += "<TITLE>POST Application</TTTLE>\r\nH;
sResponse += "</HEAD>\r\n";
Затем начинается цикл for, обрабатывающий строки из строкового массива
класса приложения:
for (i = 0; i < m_saParams.GetSize(); i++) {
Цикл будет повторяться до тех пор, пока не будет обработано все
содержимое INT-файла.
Функция Empty() стирает содержимое объекта-строки, не удаляя при этом
сам объект. Строки очищаются, поскольку на каждом проходе цикла они
будут содержать другие значения.
sKey.Empty();
sVal.Empty();
Следующий оператор if проверяет, что в текущей строке массива
содержится знак равенства — его должна содержать каждая строка INI-файла:
if ((nlndex - m_saParams[i].Find('=')) •= -1) {
sKey = m_saParams[i].Left(nlndex);
C/C++ в разработках CGI 635
sVal = m_saParams[i].Right(m_saParams[i].GetLength()
- nIndex - 1);
}
Например, в файле INI может находиться следующая строка:
Name = Osborne Reader
Код в операторе if извлекает имя параметра (Name) и присваивает его
переменной sKey, а затем извлекает значение ключа (Osborne Reader) и
записывает его в sVal.
Другой оператор if проверяет, указал ли пользователь значения для
параметра. Если не указал, ему посылается Web-страница с требованием ввести все
данные, а не значения только отдельных полей:
if (sVal — "") {
sResponse += "<Hl>Please enter all data</Hl>\r\n" ,-
break;
}
После проверки того, что значения для обеих частей строки указаны
корректно, информация каждой из них присоединяется к строке sUser:
sUser += "<НЗ>";
sUser += sKey;
' sUser += ": ";
sUser += sVal;
SUser += "</H3>\r\n";
}
(Кстати, для форматирования текста, отображаемого на Web-странице,
программа применяет тег <НЗ>. Конечно, этот формат можно изменить,
чтобы он отвечал требованием вашего приложения.)
После обработки всех имеющихся строк (или при ошибке) цикл завершится
и будет выполнен следующий оператор if:
if (i == m_saParams.GetSize()) {
sResponse += "<Hl>Server's Reply:</Hl>\r\n";
sResponse += sUser;
}
Он проверяет, что для всех параметров пользователь ввел действительные
значения. Если нет, программа просто возвращает сообщение "Введите все
данные", которое вы видели выше, а не отображает в странице отклика какие-то
ущербные сведения. Если же пользователь все ввел правильно, программа
присоединяет к файлу заглавие и информацию, извлеченную из INI-файла.
Наконец, функция берет код, необходимый для добавления завершающих
тегов, соответствующих тем, которые она создала в начале, и присоединяет его
в конец страницы. Теперь sResponse содержит законченную страницу HTML:
sResponse += "</BODY>\r\n";
sResponse += "</HTML>\r\n";
Последний оператор if пробует открыть выходной файл. Если ему это
удается, строка sResponse сбрасывается в файл. В противном случае его код
пропускается и программа завершается. Если программа не сумела корректно
записать файл, то при выходе из приложения пользователь получит ошибку HTTP.
636
Глава 16
if {fOutput.Open(sOutputFile.GetBuffer(0),
CFile::modeCreate I CFile:tmodeWrite | CFile::typeBinary)) {
fOutput.Write(sResponse.GetBuffer(O),sResponse.GetLength());
fOutput.Close();
}
}
Использование приложений CGI
Теперь, когда вы выполнили основную часть необходимой для приложения
Post работы, нужно организовать на вашем сервере поддержку передачи его
CGI-вывода пользователю. Как вы знаете, когда обозреватель извлекает
HTML-документ Web, он сначала связывается с сервером и затем запрашивает
содержимое документа (обычно он делает это командой HTTP GET). Затем,
если документ существует, сервер откликается на запрос, передавая
HTML-документ обозревателю, и после передачи закрывает соединение.
Когда вы пишете сценарий CGI, изменения в описанном процессе
происходят только со стороны сервера. Обозреватель (клиент) не знает, что сервер
активирует сценарий CGI, и ему неважно, что за данные он получает от сервера.
С вашей точки зрения, при написании CGI-сценариев вам нужно позаботиться
только о вводе/выводе сервера. Обозреватель свяжется с программой сервера,
который в свою очередь запустит ваш сценарий. Сценарий затем выполнит
обработку, необходимую для получения желаемого вывода.
Обычно сервер передает вывод вашего сценария обозревателю через HTMfy.
Для этого к выводу сценария он добавляет необходимую заголовочную
информацию и отсылает все вместе программе обозревателя, первоначально
активировавшей сервер. Затем сервер закрывает соединение и ждет
следующего'запроса соединения.
Как вы, возможно, знаете, работающие на 32-битных системах серверы
обычно могут одновременно обрабатывать запросы от нескольких
пользователей. Это подразумевает, что ваш сценарий может использоваться сразу
несколькими клиентами, и при этом вам не нужно будет писать какой-то
специальный код. Как правило, большинство серверов умеют создавать столько
параллельных экземпляров сценария/программы CGI, сколько их требуетря для
обслуживания запросов всех пользователей.
Взаимодействие сервера с программой CGI
Когда сервер активирует ваш сценарий CGI, он должен выполнить
несколько ключевых действий, включая следующие:
♦ Активировать сценарий, предоставив ему необходимые данные,
посланные обозревателем
♦ Предоставить значения переменных среды, к которым обращается
сценарий
♦ Обработать вывод сценария, в том числе включить в него необходимую
заголовочную информацию, чтобы обозреватель мог правильно
интерпретировать данные сценария
C/C++ в разработках CGt 637
Как вы знаете, протоколом, используемым при коммуникации клиентов и
серверов Web, является HTTP. Информация заголовков HTTP обеспечивает
эффективную коммуникацию программ; таким образом, вам нужно
внимательно изучить заголовочную информацию, передаваемую от сервера
обозревателю. Например, когда программа сервера готова послать данные, она
посылает сначала набор заголовков, описывающих статус данных, их тип (тип
содержимого файла) и т. д. Обозреватель, в свою очередь, использует заголовок типа
содержимого, чтобы подготовиться к обработке данных, которые за ним
последуют. Сервер отвечает за предоставление этих мета-данных при каждой
отправке данных обозревателю.
Обращение из обозревателя к программе CGI
i
Нельзя исполнить CGI-сценарий непосредственно из обозревателя. Чтобы
использовать сценарий, вы должны найти его на машине сервера HTTP.
Чтобы можно было просмотреть вывод CGI-сценария с помощью обозревателя,
сервер должен этот сценарий запустить. Как правило, сценарии, доступные
серверу, размещают в его каталоге cgi-bin (или cgi\bin, или что-то другое в
этом роде). Они должны быть откомпилированы и готовы к исполнению.
Вам нужно также создать страницу клиента, которая будет вызывать
исполняемый CGI-файл. Страница, пригодная для работы с описываемой
программой, будет выглядеть примерно так:
[ <!DOCTYPE HTML PUBLIC "-//W30//DTD W3 HTML 3.0//EN">
<HTML>
<HEAD>
<TITLE>C++ Annotated Archives</TITLE>
<META NAME="AUTHOR" CONTENT**"Osborne Reader">
</HEAD>
<BODY>
<P>
<Hl>Exampla using HTTP POST method</Hl>
<HR>
<FORM METHOD="POST" ACTION="/cgi-bin/post.exe">
<H2>Enter your name, e-mail address, and phone number, below:</H2>
<P>
<PRE>
Name: <INPUT NAME="Name" VALUE""">
Address: <INPUT NAME="E-Mail" VALUE»"">
Phone: <INPUT NAME^'Phone" VALUE»"">
</PRE>
<P>
To run this form, click this button:
<INPOT TYPE="submit" VALUE="Send!">
</FORM>
<HR>
</BODY>
</HTML>
Ключевые моменты здесь заключены в объявлениях формы. Первый раздел
файла заканчивается тегом </HEAD>:
638
Глава 16
-ODOCTYPE HTML PUBLIC "-//W30//DTD W3 HTML 3.0//EN">
<HTML>
<HEAD>
<TITLE>C++ Annotated Archives</TITLE>
<META NAME="AUTHOR" CONTENT^"Osborne Reader">
</HEAD>
Файл HTML сообщает сначала свой тип, затем некоторую заголовочную
информацию и мета-информацию, которую могут читать машины поиска. Он
также инициализирует страницу для отображения HTML.
<BODY>
<р>
<Hl>Example using HTTP POST method</Hl>
<HR>
<FORM METHOD="POST" ACTION="/cgi-bin/post.exe">
Следующие несколько операторов выполняют некоторое форматирование и
предварительное отображение информации. Ключевым здесь, однако,
является тег <FORM>. Он сообщает обозревателю, что далее следует определение
формы. Атрибут METHOD говорит форме, что она будет использовать для
передачи данных функцию HTTP POST, а атрибут ACTION сообщает клиенту,
какую программу нужно вызвать на сервере для выполнения требуемой
задачи. Заметьте, что программа клиента находится в подкаталоге cgi-bin
корневого каталога сервера.
<H2>Enter your name, e-mail address, and phone number, below:</H2>
<P>
<PRE>
Name: <INPOT NAME="Name" VALUE="">
Address: <INPUT NAME="E-Mail" VALUE="">
Phone: <INPUT NAME="Phone" VALUE="">
</PRE>
Следующие строки отображают действительную форму для ввода данных.
Каждое из значений INPUT NAME будет транслировано в имя
соответствующего ключа INI-файла. Параметр VALUE указывает, что значением каждого
из ключей по умолчанию является нулевая строка.
<р>
То run this form, click this button:
<INPUT TYPE="submit" VALUE="Send!">
</FORM>
<HR>
</BODY>
</HTML>
Наконец, значение TYPE="submit" говорит форме, что она должна
отображать кнопку. Когда пользователь на нее нажмет, клиент пошлет информацию
серверу с помощью метода POST — это то же самое, как если бы программа
сервера вызывалась с несколькими параметрами командной строки.
Когда вы загрузите файл HTML и отошлете введенные в его поля данные,
сервер инициирует программу, передаст ей данные в возвратит вам страницу
HTML с откликом, генерированным функцией CreateResponse().
Научно-техническое издание
Apr Фридман, Ларе Кландер, Марк Михаэлис, Херб Шильдт
C/C++. Архив программ
Компьютерная верстка СВ. Лычагииа
Подписано в печать 25.12.2000. Формат 70x100 /и. Усл. печ. л. 52,
Гарнитура Школьная. Бумага газетная.
Печать офсетная. Тираж 4000 экз. Заказ № 1208.
ЗАО «Издательство БИНОМ», 2001 г.
103473, Москва, Краснопролетарская, 16
Лицензия на издательскую деятельность № 065249 от 26 июня 1997 г.
Международный центр /2?\ International Centre for
научной и технической MQn Scientific and Technical
информации \=£s Information
Международный центр
научной и технической информации
125252, Москва, ул. Куусинена, д. 21-Б
Лицензия ЛР №090179
Отпечатано в ордена Трудового Красного Знамени ГП «Техническая книга»
Министерства РФ по делам печати,
телерадиовещания и средств массовых коммуникаций.
198005, Санкт-Петербург, Измайловский пр., 29.