libcats.org
Самая большая электронная библиотека рунета. Поиск книг и журналов
↓
Только точные совпадения
#1
C# 4.0: полное руководство
Шилдт, Герберт
Категория: Компьютеры, Программирование, Языки программирования
7.66 Mb
#2
C# 4.0 полное руководство
Герберт Шилдт
Категория: computers, computers, prog
7.68 Mb
#3
С++ Базовый курс
Шилдт Г.
Категория: computers, computers, prog
10.63 Mb
#4
С++ Базовый курс
Шилдт Г.
Категория: computers, computers, prog
51 Kb
#5
C++: базовый курс
Герберт Шилдт
Категория: computers, computers, prog
10.54 Mb
#6
C++: базовый курс CD
Герберт Шилдт
Категория: computers, computers, prog
51 Kb
#7
C# 3.0. Полное руководство
Шилдт Г.
Категория: БИЗНЕС, ГУМАНИТАРНЫЕ НАУКИ
9.97 Mb
#8
Java 2 v5.0 (Tiger). Новые возможности
Герберт Шилдт
Категория: НАУКА и УЧЕБА, ПРОГРАММИНГ
2.61 Mb
#9
Java 2 v5.0 (Tiger). Новые возможности
Герберт Шилдт
Категория: НАУКА и УЧЕБА, ПРОГРАММИНГ
1.09 Mb
#10
Полный справочник по C++
Г. Шилдт
Категория: КНИГИ ПРОГРАММИНГ
7.03 Mb
#11
Java. Полное руководство. 8-е издание
Шилдт Г
Категория: Компьютеры, Программирование, Языки программирования
14.41 Mb
#12
Java. Полное руководство. 8-е издание. 2012 г. — исходные коды
Шилдт Г
98 Kb
#13
Искусство программирования на JAVA (файлы)
Шилдт Г., Холмс Дж. (Schildt, Holmes)
Категория: Computer science, Programming languages
60 Kb
#14
Искусство программирования на JAVA
Шилдт Г., Холмс Дж. (Schildt, Holmes)
Категория: Computer science, Programming languages
3.55 Mb
#15
Java 2. Наиболее полное руководство
Ноутон П., Шилдт Г.
25.77 Mb
#16
C#: Учебный курс
Шилдт Г.
7.82 Mb
#17
C++. Руководство для начинающих
Шилдт Г.
77.73 Mb
#18
Самоучитель C++
Шилдт Г.
65.55 Mb
#19
C для профессиональных программистов
Шилдт Г.
217 Kb
#20
Самоучитель C++
Шилдт Г.
7.58 Mb
#21
C# Учебный курс
Шилдт Г.
Категория: computers, computers, prog
7.82 Mb
#22
C++. Руководство для начинающих
Шилдт Г.
17.55 Mb
#23
Полный справочник по С#
Шилдт Г.
3.99 Mb
#24
Java 2. Наиболее полное руководство
Ноутон П., Шилдт Г.
10.22 Mb
#25
Искусство программирования на C++
Шилдт Г.
3.91 Mb
#26
Искусство программирования на JAVA
Шилдт Г., Холмс Дж.
10.23 Mb
#27
Полный справочник по Java
Шилдт Г.
5.54 Mb
#28
Полный справочник по C#
Герберт Шилдт
Категория: computers, computers, prog
24.15 Mb
#29
C++. Руководство для начинающих
Шилдт Г.
10.49 Mb
#30
Java2. Наиболее полное руководство
П.Ноутон, Г.Шилдт
45.75 Mb
#31
C++. Руководство для начинающих
Герберт Шилдт
Категория: computers, computers, prog
17.18 Mb
#32
Самоучитель C++
Г. Шилдт
8.21 Mb
#33
C# Учебный курс
Шилдт Г.
7.83 Mb
#34
C-C++. Справочник программиста
Герберт Шилдт
Категория: computers, computers, prog
4.03 Mb
#35
Искусство программирования на C++
Шилдт Г.
3.92 Mb
#36
Java 2 v5.0 (Tiger). Новые возможности
Герберт Шилдт (2005)
Категория: computers, computers, prog
1.15 Mb
#37
Самоучитель С++
Герберт Шилдт
Категория: computers, computers, prog
8.30 Mb
#38
C/C++. Справочник программиста
Герберт Шилдт
Категория: С, С++, Visual C
3.59 Mb
#39
Полный справочник по Java
Герберт Шилдт
Категория: Java
27.45 Mb
#40
JAVA 2
Ноутон, Шилдт
Категория: 576081-[Физика и математика] Небольшая подборка книг, Computer science
25.60 Mb
#41
Самоучитель C++ (с дискетой)
Шилдт Г.
80.40 Mb
#42
Работа с Турбо Паскалем
Шилдт Г.
265 Kb
#43
C для профессиональных программистов
Шилдт Г.
182 Kb
#44
C для профессиональных программистов
Шилдт.
139 Kb
#45
C# 3.0. Полное руководство.
Шилдт Г.
Категория: computers, , computers, prog
6.78 Mb
#46
Искусство программирования на С++
Герберт Шилдт
Категория: СЕТЕВЫЕ ТЕХНОЛОГИИ, ПРОГРАММИНГ
3.91 Mb
#47
Swing руководство для начинающих
Герберт Шилдт
30.76 Mb
#48
Искусство программирования на С++ 2005 cd
Герберт Шилдт
42 Kb
#49
Java 2.Наиболее полное руководство
Ноутон П., Шилдт Г.
45.73 Mb
#50
Java2.Наиболее полное руководство (файлы к книге)
Ноутон П., Шилдт Г.
92 Kb
#51
C# 3.0: полное руководство
Шилдт Г.
Категория: Computer science
6.88 Mb
#52
Полный справочник по C
Г.Шилдт
Категория: Программирование
905 Kb
^
^
скачать книгу бесплатно
|
Кй □ S В □ R N E ЕЯ Полный справочник Четвертое издание "Герберт Шилдт рассказывает программистам об интересующей их теме доходчиво, кратко и авторитетно." ACM Computing Reviews Подробности о языке и библиотеках функций ваа илвайшиа апаплтва eouiva Р рассмотрены все новейшие средства языка и, включая применение restrict к указателям, встроенные функции, массивы переменной длины, а также операции над комплексными Сотни примеров и приложе BONUS! www.HMantfpub№hing.(MNn числами и функции комплексного переменного Автор самых популярных книг по программированию. На его счету более 2,5 миллионов проданных книг!
Полный справочник по С 4-е издание
C: The Complete Reference, Fourth Edition Herbert Schildt Osborne/McGraw-Hill Berkeley New York St. Louis San Francisco Auckland Bogota Hamburg London Madrid Mexico City Milan Montreal New Delhi Panama City Paris Sao Paulo Singapore Sydney Tokyo Toronto
Полный справочник по С 4-е издание Герберт Шилдт Издательский дом “Вильямс” Москва ♦ Санкт-Петербург♦ Киев 2002
ББК 32.973.26-018.2.75 Ш55 УДК 681.3.07 Издательский дом “Вильямс” Зав. редакцией С.Н. Тригуб Перевод с английского А.Г. Беляева, И.В. Константинова, И.С. Литвинова, В.Д. Лузина, В.Н. Романова, А.Г. Сысонюка Под редакцией Я. К Шмидского По общим вопросам обращайтесь в Издательский дом “Вильямс” по адресу: info@williamspublishing.com, http://www.williamspublishing.com Шилдт, Герберт. Ш55 Полный справочник по С, 4-е издание. : Пер. с англ. — М. : Издательский дом “Вильямс”, 2002. — 704 с.: ил. — Парал. тит. англ. ISBN 5-8459-0226-6 (рус.) В данной книге, задуманной как справочник для программистов, работающих на языке С, под- робно описаны все аспекты языка С и его библиотеки стандартных функций. Главный акцент сделан на стандарте ANSI/ISO языка С. Приведено описание как стандарта С89, так и С99. Особое внима- ние уделяется учету характеристик трансляторов, среды программирования и операционных систем, использующихся в настоящее время. Уже в самом начале подробно представлены все средства языка С, такие как ключевые слова, инструкции препроцессора и другие. Вначале описывается главным образом С89, а затем приводится подробное описание новых возможностей языка, введенных стан- дартом С99. Такая последовательность изложения позволяет облегчить практическое программиро- вание на языке С, так как в настоящее время именно эта версия для большинства программистов представляется как “собственно С”, к тому же это самый распространенный в мире язык программи- рования. Кроме того, эта последовательность изложения облегчает освоение C++, который является надмножеством С89. В книге много содержательных, нетривиальных примеров. Рассмотрены наиболее важные и рас- пространенные алгоритмы и приложения, необходимые для каждого программиста, а также приме- нение методов искусственного интеллекта и программирование для Windows 2000. Обсуждаются вопросы эффективности, переносимости и отладки программ. А в конце книги возможности языка С иллюстрируются на примере разработки его интерпретатора. Это, несомненно, самый лучший спо- соб для осмысления, постижения и понимания чистоты и элегантности языка С. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответст- вующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Osborne Publishing. Authorized translation from the English language edition published by McGraw-Hill Companies, Copyright © 2000 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enter- prises International, Copyright © 2001 ISBN 5-8459-0226-6 (рус.) © Издательский дом “Вильямс”, 2001 ISBN 0-07-212124-6 (англ.) © The McGraw-Hill Companies, 2000
Оглавление Часть I. Основы языка С.................................................29 Глава 1. Обзор возможностей языка С.....................................31 Глава 2. Выражения......................................................43 Глава 3. Операторы......................................................81 Глава 4. Массивы и строки..............................................107 Глава 5. Указатели.....................................................125 Глава 6. Функции.......................................................147 Глава 7. Структуры, объединения, перечисления и декларация typedef.....169 Глава 8. Ввод/вывод на консоль.........................................195 Глава 9. Файловый ввод/вывод...........................................215 Глава 10. Препроцессор и комментарии...................................241 Часть II. Стандарт С99 ................................................253 Глава 11. С99......................................................... 255 Часть III. Стандартная библиотека......................................273 Глава 12. Редактирование связей, использование библиотек и заголовков..275 Глава 13. Функции ввода/вывода.........................................283 Глава 14. Строковые и символьные функции...............................319 Глава 15. Математические функции.......................................345 Глава 16. Функции времени, даты и локализации..........................373 Глава 17. Функции динамического распределения памяти...................385 Глава 18. Служебные функции............................................391 Глава 19. Функции обработки двухбайтовых символов......................417 Глава 20. Библиотечные средства, добавленные в версии С99............. 425 Часть IV. Алгоритмы и приложения.......................................435 Глава 21. Сортировка и поиск......................................... 437 Глава 22. Очереди, стеки, связанные списки и деревья...................459 Глава 23. Разреженные массивы..........................................493 Глава 24. Синтаксический разбор и вычисление выражений.................509 Глава 25. Решение задач с помощью искусственного интеллекта............529 Часть V. Разработка программ с помощью С ..............................571 Глава 26. Создание скелета приложения для Windows 2000.................573 Глава 27. Проектирование программ с помощью С..........................591 Глава 28. Производительность, переносимость и отладка..................603 Часть VI. Интерпретатор языка С........................................621 Глава 29. Интерпретатор языка С........................................623 Предметный указатель...................................................681
Содержание Предисловие..........................................................26 Часть I. Основы языка С................................................29 Глава 1. Обзор возможностей языка С....................................31 Краткая история развития языка С.....................................32 С — язык среднего уровня...................’.........................32 Язык С хорошо структурирован.........................................34 Язык С создан для программистов......................................35 Компиляторы и интерпретаторы.........................................36 Структура программы на языке С.......................................37 Библиотеки и компоновка..............................................38 Раздельная компиляция................................................39 Компиляция программы на языке С......................................39 Карта памяти программы на языке С....................................39 Сравнительная характеристика языков С и C++..........................40 Словарь терминов.....................................................41 Глава 2. Выражения.....................................................43 Базовые типы данных..................................................44 Модификация базовых типов............................................45 Имена переменных.....................................................46 Переменные...........................................................47 Где объявляются переменные........................................47 Локальные переменные..............................................47 Формальные параметры функции......................................50 Глобальные переменные.............................................51 Четыре типа областей видимости.......................................52 Квалификаторы типа...................................................52 Квалификатор const................................................53 Квалификатор volatile.............................................54 Спецификаторы класса памяти..........................................54 Спецификатор extern...............................................55 Спецификатор static...............................................57 Спецификатор register.............................................59 Инициализация переменных.............................................60 Константы............................................................60 Шестнадцатеричные и восьмеричные константы........................61 Строковые константы...............................................61 Специальные символьные константы..................................62 Операции.............................................................62 Оператор присваивания.............................................63 Арифметические операции...........................................65 Операции увеличения (инкремента) и уменьшения (декремента)........66 Операции сравнения и логические операции..........................67 Поразрядные операции..............................................69 Операция ?........................................................72 Операции получения адреса (&) и раскрытия ссылки (*)..............73
Операция определения размера sizeof..............................74 Оператор последовательного вычисления: оператор “запятая”........75 Оператор доступа к члену структуры (оператор . (точка)) и оператор доступа через указатель -> (оператор стрелка).........75 Операторы [ ] и ( )..............................................76 Сводка приоритетов операций......................................76 Выражения...........................................................77 Порядок вычислений...............................................77 Преобразование типов в выражениях................................77 Явное преобразование типов: операция приведения типов............78 Пробелы и круглые скобки.........................................79 Глава 3. Операторы................................................... 81 Логические значения ИСТИНА (True) и ЛОЖЬ (False) в языке С..........82 Условные операторы..................................................82 Оператор if......................................................82 Вложенные условные операторы if.....................................84 Лестница if-else-if.................................................85 Оператор альтернативный условному................................86 Условное выражение............................................. 88 Оператор выбора — switch.........................................89 Вложенные операторы switch.......................................92 Операторы цикла.....................................................92 Цикл for.........................................................92 Варианты цикла for...............................................93 Бесконечный цикл.................................................96 Цикл for без тела цикла..........................................97 Объявление переменных внутри цикла...............................97 Цикл while.......................................................98 Цикл do-while.................................................. 100 Операторы перехода.................................................101 Оператор return.................................................101 Оператор goto...................................................101 Оператор break..................................................102 Функция exit ()................................................ 103 Оператор continue...............................................104 Оператор-выражение..................................................Ю5 Блок операторов Ю5 Глава 4. Массивы и строки.............................................107 Одномерные массивы.................................................108 Создание указателя на массив.......................................109 Передача одномерного массива в функцию..............................ПО Строки.............................................................110 Двухмерные массивы.................................................112 Массивы строк...................................................115 Многомерные массивы................................................116 Индексация указателей..............................................117 Инициализация массивов.............................................118 Инициализация безразмерных массивов.............................120 Массивы переменной длины...........................................121 Содержание 7
Приемы использования массивов и строк на примере игры в крестики-нолики....................................................121 Глава 5. Указатели......................................................125 Что такое указатели..................................................126 Указательные переменные..............................................126 Операции для работы с указателями....................................127 Указательные выражения...............................................127 Присваивание указателей............................................127 Преобразование типа указателя......................................128 Адресная арифметика................................................129 Сравнение указателей...............................................130 Указатели и массивы..................................................132 Массивы указателей.................................................133 Многоуровневая адресация.............................................134 Инициализация указателей.............................................135 Указатели на функции................................................ 137 Функции динамического распределения..................................140 Динамическое выделение памяти для массивов.........................141 Указатели с квалификатором restrict..................................143 Трудности при работе с указателями...................................144 Глава 6. Функции........................................................147 Общий вид функции....................................................148 Что такое область действия функции.................................. 148 Аргументы функции....................................................149 Вызовы по значению и по ссылке.....................................149 Вызов по ссылке....................................................150 Вызов функций с помощью массивов...................................152 Аргументы функции main(): argv и argc................................154 Оператор return......................................................157 Возврат из функции.................................................157 Возврат значений...................................................158 Возвращаемые указатели.............................................160 Функции типа void................................................ 161 Что возвращает функция main()?.......................................161 Рекурсия.............................................................161 Прототипы функций....................................................163 Старомодные объявления функций.....................................165 Прототипы стандартных библиотечных функций.........................166 Объявление списков параметров переменной длины.......................166 Правило “неявного int”...............................................166 Старомодные и современные объявления параметров функций..............167 Ключевое слово inline................................................168 Глава 7. Структуры, объединения, перечисления и декларация typedef......169 Структуры............................................................170 Доступ к членам структуры..........................................172 Присваивание структур..............................................172 Массивы структур.....................................................173 Пример со списком рассылки.........................................173 Передача структур функциям...........................................179 Передача членов структур функциям..................................179 Передача целых структур функциям...................................179 8 Содержание
Указатели на структуры..............................................181 Объявление указателя на структуру................................ 181 Использование указателей на структуры.............................181 Массивы и структуры внутри структур.................................183 Объединения.........................................................184 Битовые поля........................................................187 Перечисления........................................................188 Важное различие между С и C++.......................................190 Использование sizeof для обеспечения переносимости..................191 Средство typedef....................................................192 Глава 8. Ввод/вывод на консоль.........................................195 Чтение и запись символов............................................197 Трудности использования getchar().................................197 Альтернативы getchar()............................................198 Чтение и запись строк...............................................199 Форматный ввод/вывод на консоль.....................................201 printf()............................................................201 Вывод символов....................................................202 Вывод чисел.......................................................202 Отображение адреса................................................204 Спецификатор преобразования %п....................................204 Модификаторы формата..............................................205 Модификатор минимальной ширины поля...............................205 Модификатор точности..............................................206 Выравнивание вывода...............................................207 Обработка данных других типов.....................................207 Модификаторы * и #................................................208 scanf().............................................................208 Спецификаторы преобразования......................................209 Ввод чисел........................................................209 Ввод целых значений без знака.....................................210 Чтение одиночных символов с помощью scanf().......................210 Чтение строк......................................................210 Ввод адреса.......................................................211 Спецификатор %п...................................................211 Использование набора сканируемых символов.........................211 Пропуск лишних разделителей.......................................212 Символы в управляющей строке, не являющиеся разделителями.........212 Функции scanf() необходимо передавать адреса......................213 Модификаторы формата..............................................213 Подавление ввода..................................................214 Глава 9. Файловый ввод/вывод...........................................215 Файловый ввод/вывод в С и C++.......................................216 Файловый ввод/вывод в стандартном С и UNIX..........................216 Потоки и файлы......................................................216 Потоки............................................................217 Файлы.............................................................217 Основы файловой системы.............................................218 Указатель файла...................................................219 Открытие файла....................................................219 Закрытие файла....................................................220 Запись символа....................................................221 Содержание 9
Чтение символа.....................................................221 Использование fopen(), getc(), putc() и fclose()...................222 Использование feof()...............................................223 Ввод/вывод строк: fputs() и fgets()................................224 Функция rewind()...................................................225 Функция ferror()...................................................226 Стирание файлов....................................................227 Дозапись потока....................................................228 Функции fread() и fwrite()...........................................228 Использование fread() и fwrite()...................................228 Пример со списком рассылки.........................................229 Ввод/вывод при прямом доступе: функция fseek().......................234 Функции fprintfQ и fscanf()..........................................235 Стандартные потоки...................................................236 Связь с консольным вводом/выводом..................................238 Перенаправление стандартных потоков: функция freopen().............238 Глава 10. Препроцессор и комментарии....................................241 Препроцессор.........................................................242 Директива #define....................................................242 Определение макросов с формальными параметрами.....................243 Директива #еггог.....................................................244 Директива #include...................................................245 Директивы условной компиляции........................................245 Директивы #if, #else, #elif и #endif...............................245 Директивы #ifdef и #ifndef.........................................247 Директива #undef.....................................................248 Использование defined................................................249 Директива #line......................................................249 Директива #pragma....................................................250 Операторы препроцессора # и ##.......................................250 Имена предопределенных макрокоманд...................................251 Комментарии..........................................................251 Однострочные комментарии...........................................252 Часть II. Стандарт С99 .................................................253 Глава И. С99 ...........................................................255 Сравнение С99 с С89. Общее впечатление...............................256 Новые возможности..................................................256 Удаленные средства.................................................257 Измененные средства................................................257 Указатели, определенные с квалификаторами типа restrict..............257 Ключевое слово inline................................................258 Новые встроенные типы данных.........................................259 _Воо1..............................................................259 -Complex и -Imaginary..............................................260 Типы целых данных long long........................................260 Расширение массивов..................................................260 Массивы переменной длины...........................................261 Использование квалификаторов типов в объявлении массива............261 Однострочные комментарии.............................................262 Распределение кода и объявлений......................................262 Изменения препроцессора..............................................263 10 Содержание
Переменные списки аргументов....................................263 Оператор -Pragma................................................263 Встроенные прагмы...............................................263 Новые встроенные макросы........................................264 Объявление переменных внутри цикла for............................264 Составные литералы................................................265 Массивы с переменными границами в качестве членов структур........266 Назначенные инициализаторы........................................266 Новые возможности семейства функций printf() и scanf()............267 Новые библиотеки С99............................................. 267 Зарезервированный идентификатор__func.............................268 Расширение граничных значений трансляции..........................268 Неявный int больше не поддерживается..............................269 Удалены неявные объявления функций................................270 Ограничения на return.............................................270 Расширенные целые типы............................................270 Изменения в правилах продвижения целых типов......................271 Часть III. Стандартная библиотека....................................273 Глава 12. Редактирование связей, использование библиотек и заголовков.275 Редактор связей...................................................276 Раздельная компиляция...........................................276 Переместимые коды и абсолютные коды.............................277 Редактирование связей с оверлеями...............................Т11 Связывание с динамически подсоединяемыми библиотеками (DLL).....278 Стандартная библиотека С..........................................279 Библиотечные файлы и объектные файлы............................279 Заголовки.........................................................279 Макросы в заголовках............................................281 Переопределение библиотечных функций 281 Глава 13. Функции ввода/вывода.......................................283 Функция clearerr..................................................284 Пример..........................................................284 Зависимые функции...............................................285 Функция fclose....................................................285 Пример..........................................................286 Зависимые функции...............................................286 Функция feof......................................................286 Пример..........................................................286 Зависимые функции...............................................286 Функция ferror....................................................287 Пример..........................................................287 Зависимые функции...............................................287 Функция fflush....................................................287 Пример..........................................................287 Зависимые функции...............................................288 Функция fgetc.....................................................288 Пример..........................................................288 Зависимые функции...............................................288 Функция fgetpos...................................................289 Пример..........................................................289 Зависимые функции...............................................289 Содержание 11
Функция fgets.........................................................289 Пример.............................................................289 Зависимые функции..................................................290 Функция fopen.........................................................290 Пример.............................................................291 Зависимые функции..................................................291 Функция fprintf..........................................,............291 Пример.............................................................292 Зависимые функции..................................................292 Функция fputc.........................................................292 Пример.............................................................293 Зависимые функции..................................................293 Функция fputs.........................................................293 Пример........................................................... 293 Зависимые функции..................................................293 Функция fread....................................................... 293 Пример.............................................................294 Зависимые функции..................................................294 Функция freopen.......................................................294 Пример.............................................................295 Зависимые функции..................................................295 Функция fscanf........................................................295 Пример.............................................................296 Зависимые функции..................................................296 Функция fseek.........................................................296 Пример.............................................................296 Зависимые функции..................................................297 Функция fsetpos.......................................................297 Пример.............................................................297 Зависимые функции..................................................297 Функция ftell.........................................................298 Пример.............................................................298 Зависимые функции..................................................298 Функция fwrite........................................................298 Пример.............................................................298 Зависимые функции..................................................299 Функция getc..........................................................299 Пример.............................................................299 Зависимые функции..................................................300 Функция getchar.......................................................300 Пример.............................................................300 Зависимые функции..................................................300 Функция gets..........................................................300 Пример.............................................................301 Зависимые функции..................................................301 Функция реггог........................................................301 Пример.............................................................302 Функция printf........................................................302 Модификаторы формата функции printf(), добавленные стандартом С99. 304 Пример.............................................................305 Зависимые функции..................................................305 Функция putc..........................................................305 Пример.............................................................305 12 Содержание
Зависимые функции............................................305 Функция putchar................................................305 Пример.......................................................306 Зависимые функции............................................306 Функция puts...................................................306 Пример.......................................................306 Зависимые функции............................................306 Функция remove.................................................307 Пример.......................................................307 Зависимые функции............................................307 Функция rename.................................................307 Пример.......................................................307 Зависимые функции............................................308 Функция rewind.................................................308 Пример.......................................................308 Зависимые функции............................................308 Функция scanf..................................................308 Модификаторы формата, добавленные к функции scanf() Стандартом С99.... 311 Пример.......................................................311 Зависимые функции............................................312 Функция setbuf.................................................312 Пример.......................................................312 Зависимые функции............................................312 Функция setvbuf................................................312 Пример.......................................................313 Зависимые функции............................................313 Функция snprintf...............................................313 Зависимые функции............................................313 Функция sprintf..............................:.................313 Пример.......................................................314 Зависимые функции............................................314 Функция sscanf.................................................314 Пример.......................................................314 Зависимые функции............................................315 Функция tmpfile................................................315 Пример.......................................................315 Зависимые функции............................................315 Функция tmpnam.................................................315 Пример.......................................................316 Зависимые функции............................................316 Функция ungetc.................................................316 Пример.......................................................316 Зависимые функции............................................317 Функции vprintf, vfprintf, vsprintf и vsnprintf................317 Пример.......................................................317 Зависимые функции............................................318 Функции vscanf, vfscanf и vsscanf..............................318 Зависимые функции............................................318 Глава 14. Строковые и символьные функции..........................319 Функция isalnum................................................320 Пример.......................................................320 Зависимые функции............................................321 Содержание 13
Функция isalpha..................................................321 Пример........................................................321 Зависимые функции.............................................321 Функция isblank..................................................322 Пример........................................................322 Зависимые функции.............................................322 Функция iscntrl..................................................322 Пример........................................................322 Зависимые функции.............................................323 Функция isdigit..................................................323 Пример........................................................323 Зависимые функции.............................................323 Функция isgraph..................................................324 Пример........................................................324 Зависимые функции.............................................324 Функция islower................................................ 324 Пример........................................................324 Зависимые функции.............................................325 Функция isprint..................................................325 Пример........................................................325 Зависимые функции.............................................325 Функция ispunct..................................................325 Пример........................................................326 Зависимые функции.............................................326 Функция isspace................................................ 326 Пример........................................................326 Зависимые функции.............................................327 Функция isupper..................................................327 Пример........................................................327 Зависимые функции.............................................327 Функция isxdigit.................................................327 Пример........................................................328 Зависимые функции.............................................328 Функция memchr...................................................328 Пример........................................................328 Еще один пример...............................................329 Зависимые функции.............................................329 Функция memcmp................................................. 329 Пример........................................................330 Зависимые функции.............................................330 Функция memcpy...................................................330 Пример........................................................331 Зависимые функции.............................................331 Функция memmove..................................................331 Пример........................................................331 Зависимые функции.............................................332 Функция memset...................................................332 Пример........................................................332 Зависимые функции.............................................332 Функция strcat...................................................332 Пример........................................................332 Зависимые функции.............................................333 Функция strchr...................................................333 14 Содержание
Пример......................................................333 Зависимые функции...........................................333 Функция strcmp.................................................333 Пример......................................................334 Зависимые функции...........................................334 Функция strcoll................................................334 Пример......................................................334 Зависимые функции...........................................334 Функция strcpy..................................ч..............335 Пример......................................................335 Зависимые функции...........................................335 Функция strcspn................................................335 Пример......................................................335 Зависимые функции...........................................335 Функция strerror...............................................336 Пример......................................................336 Функция strlen.................................................336 Пример......................................................336 Зависимые функции...........................................336 Функция stmcat.................................................336 Пример......................................................337 Зависимые функции...........................................337 Функция stmcmp.................................................337 Пример......................................................337 Зависимые функции...........................................338 Функция strncpy................................................338 Пример......................................................338 Зависимые функции...........................................338 Функция strpbrk................................................339 Пример......................................................339 Зависимые функции...........................................339 Функция strrchr................................................339 Пример......................................................339 Зависимые функции...........................................340 Функция strspn.................................................340 Пример......................................................340 Зависимые функции...........................................340 Функция strstr.................................................340 Пример......................................................340 Зависимые функции...........................................341 Функция strtok.................................................341 Пример......................................................341 Зависимые функции...........................................342 Функция strxfrm................................................342 Пример......................................................342 Зависимые функции...........................................342 Функция tolower................................................342 Пример......................................................342 Зависимые функции...........................................343 Функция toupper................................................343 Пример......................................................343 Зависимые функции...........................................343 Содержание 15
Глава 15. Математические функции......................................345 Семейство функций acos..............................................347 Пример...........................................................348 Зависимые функции................................................348 Семейство функций acosh.............................................348 Зависимые функции................................................348 Семейство функций asin..............................................348 Пример...........................................................349 Зависимые функции................................................349 Семейство функций asinh.............................................349 Зависимые функции................................................349 Семейство функций atan..............................................349 Пример...........................................................350 Зависимые функции................................................350 Семейство функций atanh.............................................350 Зависимые функции................................................350 Семейство функций atan2.............................................350 Пример...........................................................351 Зависимые функции................................................351 Семейство функций cbrt..............................................351 Пример...........................................................351 Зависимые функции................................................351 Семейство функций ceil..............................................351 Пример...........................................................352 Зависимые функции................................................352 Семейство функций copysign..........................................352 Зависимые функции................................................352 Семейство функций cos...............................................352 Пример...........................................................352 Зависимые функции..................;.............................353 Семейство функций cosh..............................................353 Пример...........................................................353 Зависимые функции................................................353 Семейство функций erf...............................................353 Зависимые функции................................................354 Семейство функций erfc..............................................354 Зависимые функции................................................354 Семейство функций ехр...............................................354 Пример...........................................................355 Зависимые функции................................................355 Семейство функций ехр2..............................................355 Зависимые функции................................................355 Семейство функций expml.............................................355 Зависимые функции................................................355 Семейство функций fabs..............................................355 Пример...........................................................356 Зависимые функции................................................356 Семейство функций fdim..............................................356 Зависимые функции................................................356 Семейство функций floor.............................................356 Пример...........................................................356 Зависимые функции................................................357 16 Содержание
Семейство функций fma..............................................357 Зависимые функции.......................'........................357 Семейство функций fmax.............................................357 Зависимые функции................................................357 Семейство функций fmin.............................................357 Зависимые функции................................................357 Семейство функций fmod.............................................358 Пример...........................................................358 Зависимые функции................................................358 Семейство функций frexp............................................358 Пример...........................................................359 Зависимые функции....................................;...........359 Семейство функций hypot............................................359 Зависимые функции................................................359 Семейство функций ilogb............................................359 Зависимые функции................................................359 Семейство функций Idexp............................................359 Пример...........................................................360 Зависимые функции................................................360 Семейство функций Igamma...........................................360 Зависимые функции................................................360 Семейство функций llrint...........................................360 Зависимые функции................................................360 Семейство функций llround..........................................361 Зависимые функции................................................361 Семейство функций log..............................................361 Пример...........................................................361 Зависимые функции................................................361 Семейство функций loglp............................................362 Зависимые функции................................................362 Семейство функций log 10...........................................362 Пример......................................................... 362 Зависимые функции................................................362 Семейство функций log2.............................................363 Зависимые функции................................................363 Семейство функций logb.............................................363 Зависимые функции................................................363 Семейство функций Irint........................................... 363 Зависимые функции................................................363 Семейство функций Iround...........................................364 Зависимые функции................................................364 Семейство функций modf.............................................364 Пример...........................................................364 Зависимые функции................................................364 Семейство функций пап..............................................364 Зависимые функции................................................365 Семейство функций nearbyint........................................365 Зависимые функции................................................365 Семейство функций nextafter........................................365 Зависимые функции................................................365 Семейство функций nexttoward.......................................365 Зависимые функции................................................365 Семейство функций pow................,........................... 366 Содержание 17
Пример...........................................................366 Зависимые функции................................................366 Семейство функций remainder.........................................366 Зависимые функции...........,....................................366 Семейство функций remquo............................................367 Зависимые функции................................................367 Семейство функций rint..............................................367 Зависимые функции................................................367 Семейство функций round.............................................367 Зависимые функции................................................367 Семейство функций scalbln...........................................368 Зависимые функции................................................368 Семейство функций scalbn............................................368 Зависимые функции................................................368 Семейство функций sin...............................................368 Пример...........................................................368 Зависимые функции................................................369 Семейство функций sinh..............................................369 Пример...........................................................369 Зависимые функции................................................369 Семейство функций sqrt............................................ 370 Пример...........................................................370 Зависимые функции................................................370 Семейство функций tan...............................................370 Пример...........................................................370 Зависимые функции................................................371 Семейство функций tanh..............................................371 Пример...........................................................371 Зависимые функции.............................................. 371 Семейство функций tgamma............................................371 Зависимые функции................................................371 Семейство функций trunc.............................................372 Зависимые функции................................................372 Глава 16. Функции времени, даты и локализации.........................373 Функция asctime.....................................................374 Пример......................................................... 374 Зависимые функции................................................375 Функция clock.......................................................375 Пример...........................................................375 Зависимые функции................................................375 Функция ctime.......................................................375 Пример...........................................................376 Зависимые функции............................................... 376 Функция difftime....................................................376 Пример...........................................................376 Зависимые функции................................................376 Функция gmtime......................................................377 Пример...........................................................377 Зависимые функции................................................377 Функция localeconv..................................................377 Пример...........................................................379 Родственная функция..............................................379 18 Содержание
Функция localtime..............................................379 Пример.......................................................380 Зависимые функции............................................380 Функция mktime.................................................380 Пример.......................................................380 Зависимые функции............................................381 Функция setlocale..............................................381 Пример.......................................................381 Зависимые функции............................................382 Функция strftime...............................................382 Пример.......................................................383 Зависимые функции............................................384 Функция time...................................................384 Пример.......................................................384 Зависимые функции............................................384 Глава 17. Функции динамического распределения памяти..............385 Функция calloc.................................................386 Пример.......................................................386 Зависимые функции............................................386 Функция free................................................. 387 Пример.......................................................387 Зависимые функции............................................387 Функция malloc.................................................387 Пример.......................................................388 Зависимые функции............................................388 Функция realloc................................................388 Пример.......................................................389 Зависимые функции............................................389 Глава 18. Служебные функции.......................................391 Функция abort..................................................392 Пример.................;.....................................392 Зависимые функции............................................392 Функция abs....................................................393 Пример.......................................................393 Зависимая функция............................................393 Функция-макрос assert..........................................393 Пример.......................................................393 Зависимая функция............................................394 Функция atexit.................................................394 Пример.......................................................394 Зависимые функции............................................394 Функция atof...................................................394 Пример.......................................................395 Зависимые функции............................................395 Функция atoi...................................................395 Пример.......................................................395 Зависимые функции............................................396 Функция atol...................................................396 Пример.......................................................396 Зависимые функции............................................396 Функция atoll..................................................396 Зависимые функции............................................396 Содержание 19
Функция bsearch..................................................397 Пример.........................................................397 Зависимая функция..............................................398 Функция div......................................................398 Пример.........................................................398 Зависимые функции............................................. 398 Функция exit.....................................................398 Пример.........................................................399 Зависимые функции..............................................399 Функция _Exit....................................................399 Зависимые функции..............................................399 Функция getenv...................................................399 Пример.........................................................400 Зависимая функция..............................................400 Функция labs.....................................................400 Пример.........................................................400 Зависимые функции..............................................400 Функция llabs....................................................400 Зависимые функции..............................................401 Функция Idiv.....................................................401 Пример.........................................................401 Зависимые функции..............................................401 Функция lidiv....................................................401 Зависимые функции..............................................401 Функция longjmp..................................................402 Пример.........................................................402 Зависимая функция..............................................403 Функция mblen....................................................403 Пример.........................................................403 Зависимые функции..............................................403 Функция mbstowcs............................................... 403 Пример.........................................................403 Зависимые функции..............................................403 Функция mbtowc...................................................404 Пример.........................................................404 Зависимые функции..............................................404 Функция qsort....................................................404 Пример.........................................................405 Зависимая функция..............................................405 Сортировка в убывающем порядке.................................405 Функция raise....................................................405 Зависимая функция..............................................406 Функция rand.....................................................406 Пример.........................................................406 Зависимая функция..............................................406 Функция setjmp...................................................406 Зависимая функция..............................................407 Функция signal...................................................407 Зависимая функция..............................................407 Функция srand..........:.........................................407 Пример.........................................................408 Зависимая функция..............................................408 Функция strtod...................................................408 20 Содержание
Пример..........................................................409 Зависимые функции...............................................409 Функция strtof....................................................409 Зависимые функции...............................................409 Функция strtol....................................................410 Пример..........................................................410 Зависимые функции...............................................410 Функция strtold...................................................411 Зависимые функции...............................................411 Функция strtoll...................................................411 Зависимые функции...............................................411 Функция strtoul...................................................411 Пример..........................................................412 Зависимые функции...............................................412 Функция strtoull..................................................412 Зависимые функции...............................................412 Функция system....................................................413 Пример..........................................................413 Зависимая функция...............................................413 Функции-макросы va_arg, va_start, va_end и va_copy................413 Пример..........................................................414 Зависимая функция...............................................415 Функция wcstombs..................................................415 Зависимые функции...............................................415 Функция wctomb....................................................415 Зависимые функции...............................................415 Глава 19. Функции обработки двухбайтовых символов....................417 Функции классификации двухбайтовых символов.......................418 Функции ввода-вывода двухбайтовых символов........................420 Функции для операций над строками двухбайтовых символов...........421 Преобразование строк двухбайтовых символов........................422 Функции для обработки массивов двухбайтовых символов..............423 Функции для преобразования многобайтовых и двухбайтовых символов..424 Глава 20. Библиотечные средства, добавленные в версии С99............425 Библиотека поддержки арифметических операций с комплексными числами... 426 Библиотека поддержки среды вычислений с плавающей точкой..........430 Заголовок <stdint.h>..............................................431 Функции для преобразования формата целочисленных значений.........432 Математические макросы обобщенного типа...........................433 Заголовок <stdbool.h>.....;.......................................434 Часть IV. Алгоритмы и приложения................................... 435 Глава 21. Сортировка и поиск.........................................437 Сортировка........................................................438 Классы алгоритмов сортировки......................................439 Оценка алгоритмов сортировки......................................439 Пузырьковая сортировка............................................440 Сортировка посредством выбора.....................................443 Сортировка вставками..............................................444 Улучшенные алгоритмы сортировки...................................445 Сортировка Шелла................................................446 Быстрая сортировка..............................................448 Содержание 21
Выбор метода сортировки.............................................451 Сортировка других структур данных...................................451 Сортировка строк..................................................451 Сортировка структур...............................................453 Сортировка дисковых файлов с произвольной выборкой..................454 Поиск...............................................................457 Методы поиска.....................................................457 Последовательный поиск............................................457 Двоичный поиск....................................................458 Глава 22. Очереди, стеки, связанные списки и деревья...................459 Очереди.............................................................460 Циклическая очередь.................................................464 Стеки................................................................ 467 Связанные списки....................................................471 Односвязные списки..................................................471 Двусвязные списки...................................................476 Пример списка рассылки..............................................479 Двоичные деревья....................................................484 Глава 23. Разреженные массивы..........................................493 Зачем нужны разреженные массивы?....................................494 Представление разреженного массива в виде связанного списка.........495 Анализ метода представления в виде связанного списка..............498 Представление разреженного массива в виде двоичного дерева..........498 Анализ метода представления в виде двоичного дерева...............500 Представление разреженного массива в виде массива указателей........500 Анализ метода представления разреженного массива в виде массива указателей.........................................502 Хэширование.........................................................503 Анализ метода хэширования.........................................506 Выбор метода........................................................507 Глава 24. Синтаксический разбор и вычисление выражений.................509 Выражения...........................................................510 Разбиение выражения на лексемы......................................511 Разбор выражений....................................................514 Простая программа синтаксического анализа выражений.................515 Работа с переменными в анализаторе..................................520 Проверка синтаксиса в рекурсивном нисходящем анализаторе............526 Глава 25. Решение задач с помощью искусственного интеллекта............529 Представление и терминология........................................530 Комбинаторные взрывы................................................532 Методы поиска.......................................................534 Оценка поиска.......................................................535 Представление в виде графа..........................................536 Поиск в глубину.....................................................537 Анализ поиска в глубину...........................................545 Полный перебор, или поиск в ширину..................................546 Анализ поиска в ширину............................................547 Добавление эвристики................................................548 Поиск методом наискорейшего подъема.................................548 Анализ наискорейшего подъема......................................554 Поиск с использованием частичного пути минимальной стоимости........554 22 Содержание
Анализ поиска с использованием частичного пути минимальной стоимости . 555 Выбор метода поиска................................................556 Поиск нескольких решений...........................................556 Удаление путей..................................................557 Удаление вершин.................................................557 Поиск “оптимального” решения.......................................562 И снова возвращаемся к поиску потерянных ключей....................567 Часть V. Разработка программ с помощью С..............................571 Глава 26. Создание скелета приложения для Windows 2000................573 Общая картина специфики программирования для Windows 2000..........574 Модель рабочего стола ..........................................575 Мышь............................................................575 Пиктограммы, растровые изображения и другая графика.............575 Меню, средства управления и диалоговые окна.....................576 Интерфейс прикладного программирования Win32.......................576 Компоненты окна....................................................577 Взаимодействие прикладных программ с Windows.......................578 Базовые концепции функционирования приложений для Windows 2000.... 578 WinMain().......................................................$19 Процедура окна..................................................579 Классы окон.....................................................579 Цикл обработки сообщений........................................580 Типы данных Windows.............................................580 Скелет программы для Windows 2000..................................580 Определение класса окна.........................................583 Создание окна...................................................585 Цикл обработки сообщений........................................587 Функция окна....................................................588 Файл описания больше не нужен......................................589 Соглашения об именовании 589 Глава 27. Проектирование программ с помощью С.........................591 Проектирование сверху вниз.........................................592 Структурирование программы......................................592 Выбор структуры данных..........................................593 “Пуленепробиваемые” функции........................................594 Использование программы МАКЕ.......................................597 Использование макросов в МАКЕ...................................600 Применение интегрированной среды разработки........................601 Глава 28. Производительность, переносимость и отладка.................603 Эффективность......................................................604 Операции увеличения и уменьшения................................604 Применение регистровых переменных...............................605 Указатели вместо индексации массива.............................606 Применение функций............................................. 606 Перенос программ...................................................610 Использование #define...........................................610 Зависимость от операционной системы.............................611 Различия в размерах данных......................................611 Отладка............................................................611 Ошибки очередности вычисления...................................611 Содержание 23
Проблемы с указателями.............................................612 Интерпретация синтаксических ошибок................................614 Ошибки, вызванные “потерей” единицы................................615 Ошибки из-за нарушения границ......................................616 Пропуск прототипов функций.........................................617 Ошибки при задании аргументов......................................618 Переполнение стека.................................................619 Применение отладчика...............................................619 Теория отладки в общих чертах......................................619 Часть VI. Интерпретатор языка С.........................................621 Глава 29. Интерпретатор языка С.........................................623 Практическое значение интерпретаторов................................624 Определение языка Little С...........................................625 Ограничения языка Little С.........................................626 Интерпретация структурированного языка...............................628 Неформальная теория языка С..........................................628 Выражения языка С..................................................629 Определение значения выражения.....................................630 Синтаксический анализатор выражений..................................631 Синтаксический разбор исходного текста программы...................631 Рекурсивный нисходящий синтаксический анализатор Little С..........636 Интерпретатор Little С.............................................. 647 Предварительный проход интерпретатора..............................648 Функция main().....................................................650 Функция interp_block().............................................651 Обработка локальных переменных.....................................664 Вызов функций, определенных пользователем..........................665 Присваивание значений переменным...................................668 Выполнение оператора if............................................669 Обработка цикла while..............................................670 Обработка цикла do-while...........................................671 Цикл for...........................................................671 Библиотечные функции Little С........................................672 Компиляция и компоновка интерпретатора Little С......................675 Демонстрация Little С................................................675 Усовершенствование интерпретатора Little С...........................679 Расширение Little С..................................................680 Добавление новых средств в язык Little С.:.........................680 Создание дополнительных средств программирования...................680 Предметный указатель.................................................681 24 Содержание
Об авторе Герберт Шилдт (Herbert Schildt) — выдающийся автор книг по программированию, об- щепризнанный авторитет в области программирования на языках С, C++, Java и прило- жений для Windows. Тираж его книг, переведенных на многие языки мира, составляет более 2,5 миллионов экземпляров. Он является автором многочисленных бестселлеров, среди которых наиболее известны такие издания, как C++: The Complete Reference, Teach Yourself C, Teach Yourself C++, C++ from the Ground Up, Windows 2000 Programming from the Ground Up, Java: The Complete Reference (Полный справочник no Java, изд. “Диалектика”). Г. Шилдг имеет степень магистра наук в области вычислительной математики, присвоен- ную ему Иллинойским университетом (University of Illinois).
Предисловие Зто четвертое издание книги С: The Complete Reference (Полный справочник по С). Со времен третьего издания в области программирования произошло много прогрессивных изменений, в частности, получили широкое распространение In- ternet и World Wide Web, был изобретен язык Java, а C++ был стандартизирован. Так- же был создан новый стандарт С, названный С99. Несмотря на то, что Стандарт С99 пока не завоевал всеобщего признания, его создание стало одним из самых выдаю- щихся событий в области программирования за последние пять лет. В условиях стре- мительного развития компьютерных технологий порой нелегко сразу определить фун- даментальные элементы, на которых строится будущее этих технологий. Именно та- ким основополагающим элементом является язык С. Во всем мире значительная часть текстов программ написана на этом языке. На его основе построен язык C++, а син- таксис языка С является фундаментом языка Java. Если бы язык С был всего лишь отправной точкой для других языков программирования, это был бы интересный, но мертвый язык. К счастью, это не так. Сегодня язык С не менее актуален, чем во вре- мя своего создания. Как будет видно из дальнейшего изложения, Стандарт С99 со- держит такие новые перспективные конструкции, благодаря которым язык С по праву считается одним из самых прогрессивных в области программирования. Несмотря на большое распространение “потомков” языка С (Java и C++), значение самого С по- прежнему остается первостепенным. В создании Стандарта С99 участвовали наиболее выдающиеся специалисты по языкам программирования, среди них такие, как Рекс Джашке (Rex Jaeschke), Джим Томас (Jim Thomas), Том МакДональд (Tom MacDonald) и Джон Бенито (John Benito). Как член комиссии по стандартизации я наблюдал процесс созда- ния стандарта и участвовал в дискуссиях по поводу каждого нововведения. В ре- зультате ежедневного обмена идеями и мнениями по электронной почте между участниками этого процесса, находящимися практически во всех странах мира, удалось выработать единую концепцию, что и привело в конечном итоге к значи- тельному усовершенствованию языка С. Надо признать, что, работая над первым изданием этой книги, я не предполагал столь быстрого роста достижений в области программирования, хотя некоторые из них, например, большой успех C++, были предопределены уже с самого начала. Но язык С я считаю одним из лучших среди всех языков программирования. Это изящ- ный, элегантный, логичный и, что особенно важно, мощный язык. Его столь успеш- ное развитие и распространение очень меня радует и вдохновляет. Я Книга для всех программистов Эта книга задумана как справочник для всех программистов, работающих на языке С, независимо от уровня их подготовки. Предполагается, что читатель уже имеет не- которое представление об основах языка С и может написать на нем хотя бы про- стейшую программу. Однако, если читатель только начал изучать С, эта книга послу- жит отличным дополнением к любому учебнику по С, так как в ней можно будет най- ти ответы на многие трудные вопросы, возникающие в процессе изучения. Книга также будет полезна в качестве подробного справочника по основам C++, который, как известно, является объектно-ориентированным расширением языка С, т.е. она пригодится любому программисту, пишущему программы на С или C++. 26 Предисловие
И Новое в четвертом издании По сравнению с тремя предыдущими изданиями структура книги в основном оста- лась неизменной. Большинство изменений определено новыми возможностями язы- ка, появившимися после введения Стандарта С99. Все эти новые возможности под- робно описаны в части II книги, в предыдущих изданиях эта информация отсутствует. Часть III, в которой описывается библиотека стандартных функций, в этом издании значительно дополнена, в нее включено описание многих новых библиотечных функ- ций, введенных Стандартом С99. Но, конечно, не изменено полное описание Стан- дарта С89, который, как известно, очень важен, ведь именно на его основе создан C++. Кроме того, большинство программистов работают с версией С89, потому что до настоящего времени фактически все еще нет общедоступного компилятора, под- держивающего Стандарт С99. Книга в целом была значительно переработана с целью ознакомления с новыми характеристиками трансляторов, среды программирования и операционных систем, использующихся в настоящее время. Щ 0 чем эта книга В книге подробно описаны все аспекты языка С и его библиотеки стандартных функций. Главный акцент сделан на Стандарте ANSI/ISO этого языка. Дано описание как Стандарта С89, так и С99. Книга состоит из следующих шести частей: Основополагающие элементы языка С, определенные в С89 Расширение С99 Библиотеки стандартных функций С Распространенные алгоритмы и приложения Среда программирования С Создание интерпретатора С В части I подробно представлены все средства языка С, т.е. его ключевые слова, инструкции препроцессора и другие. В этой части в основном описывается Стандарт С89, а также упоминаются некоторые новые свойства, введенные Стандартом С99. В части II подробно рассматриваются новые возможности языка, введенные стан- дартом С99. Есть два аргумента в пользу отдельного описания стандартов С89 и С99. Во-первых, подавляющее большинство программистов используют сегодня С89. Эта версия языка С воспринимается ими как “собственно С”. К тому же это самый рас- пространенный в мире язык программирования. Существенно также и то, что С89 яв- ляется подмножеством C++. Поэтому версия С89 крайне актуальна как сегодня, так и в обозримом будущем. По этим причинам в книге должно быть сделано четкое раз- граничение между этими версиями языка С. Во-вторых, многие читатели этой книги уже хорошо знакомы с версией С89, и им будет значительно легче найти новый для себя материал, если новые свойства С99 будут изложены в отдельной части книги. В части III дается описание библиотеки стандартных функций С. Рассматриваются как функции Стандарта С89, так и функции Стандарта С99, причем особо выделены функции, введенные Стандартом С99. В части IV можно ознакомиться с наиболее важными и распространенными алго- ритмами и приложениями, необходимыми для каждого программиста. Здесь рассмат- риваются методы искусственного интеллекта и их применение, а также программиро- вание для Windows 2000. Предисловие 27
В части V вы узнаете много интересного о среде программирования С, здесь обсу- ждаются вопросы эффективности, переносимости и отладки программ. В части VI возможности языка С демонстрируются на примере разработки его ин- терпретатора. Это наиболее увлекательная и даже забавная часть книги. Поэкспери- ментировать с этим интерпретатором будет истинным наслаждением для любого программиста! Нам кажется, это самый лучший способ для того, чтобы по достоинст- ву оценить чистоту и элегантность языка С. В Тексты программ в Web Тексты программ, рассматриваемых в этой книге, можно бесплатно получить по адресу www.williamspublishing.coin HS 21 марта 2000 г. Магомет (Mahomet), штат Иллинойс Ц Материал для дальнейшего изучения Эта книга Герберта Шилдта — лишь одна из многих его книг по программирова- нию. Ниже приведены другие книги, представляющие интерес для программистов. Изучающим программирование под Windows мы рекомендуем следующие книги: Windows 2000 Programming from the Ground Up Windows 98 Programming from the Ground Up Windows NT 4 Programming from the Ground Up The Windows Programming Annotated Archives Для изучающих язык С будут интересны такие книги: Teach Yourself С C/C++ Annotated Archives Следующие книги будут полезны тем, кто изучает C++: C++: The Complete Reference Teach Yourself C++ C++from the Ground Up Expert C++ C/C++ Annotated Archives Изучающим язык Java мы рекомендуем прочесть: Java: The Complete Reference (Полный справочник no Java, изд. “Диалектика”). Ц Если вам нужна квалифицированная консультация, обращайтесь к Герберту Шилдту, общепризнанному авторитету в области программирования 28 Предисловие
Полный справочник по Описание языка С в данной книге разделено на две части. В части I рассматриваются свойства С, опреде- ленные Стандартом ANSI 1989 года (Стандарт С89) с учетом 1-й Поправки, принятой в 1995 году. В насто- ящее время эта версия С широко распространена и поддерживается всеми существующими компилятора- ми С. Эта версия является также основой языка C++, поэтому на нее обычно ссылаются как на подмноже- ство C++. В части II описываются новые свойства С,
введенные Стандартом 1999 года (С99); здесь же дано подробное объяснение, чем С99 от- личается от С89. Новый стандарт 1999 года почти полностью базируется на Стандарте 1989 года, появились лишь некоторые новые возможности языка, принципиально не повлияв- шие на его суть. Таким образом, С89 является основой как С99, так и C++. Отдельное рассмотрение языка С в двух аспектах — С89 как основы и специфиче- ских свойств С99 — имеет три главных преимущества: Достаточно четко определены различия между С89 и С99. В настоящее время пока еще нет общедоступных компиляторов С99, поэтому для программиста очень важно понимание этих различий. В противном случае может оказаться, что, решая определенную задачу, программист рассчитывает на средства языка, не поддерживаемые ни одним из существующих компиляторов. Многие читатели, знакомые с С89, без труда обнаружат новые свойства языка, вве- денные Стандартом С99. Изложение версии С99 в отдельной части облегчает для квалифицированных программистов задачу поиска новой информации о версии С99. Но следует отметить, что и в части I там, где это уместно, упоминаются отли- чия С89 от С99 и новые возможности языка, введенные Стандартом С99. Отдельное рассмотрение версии С89 позволяет предельно четко определить версию С, образующую подмножество C++. Это особенно важно при написании тех про- грамм на С, которые будут транслироваться компилятором C++. Это необходимо также в том случае, если планируется переход на компилятор C++ или возникает необходимость работать с обоими компиляторами одновременно. Знание различий между С89 и С99 является обязательным для каждого высококва- лифицированного специалиста С, работающего с программами на языке С. Часть I построена следующим образом: Глава 1 — обзор возможностей языка С; Глава 2 — рассматриваются базовые типы данных, переменные, операторы и выражения; Глава 3 — рассматриваются управляющие операторы программы; Глава 4 — обсуждаются массивы и строки; Глава 5 — описание указателей; Глава 6 — рассматриваются функции; Глава 7 — описание структур, объединений и пользовательских типов данных. Глава 8 — рассматривается консольный ввод/вывод данных; Глава 9 — рассматривается ввод и вывод данных в файлы; Глава 10 — описание препроцессора и комментарии.
Полньй справочник по Обзор возможностей языка С
Целью этой главы является описание возможностей языка программирования С, ознакомление с его происхождением, использованием, а также лежащими в его основе концепциями. Глава рассчитана главным образом на программистов, которые только приступили к изучению языка С. В Краткая история развития языка С Язык С был изобретен и реализован Деннисом Ритчи (Dennis Ritchie) для компьютера DEC PDP-11 в операционной системе Unix. Этот язык был разработан на основе “более старого” языка BCPL, созданного в свое время Мартином Ричардсом (Martin Richards). BCPL оказал определенное влияние на язык В, разработанный Кеном Томпсоном (Кеи Thompson). В свою очередь развитие языка В привело к созданию в 1970 году языка С. На протяжении многих лет стандартом С была фактически версия, поставляемая вместе с операционной системой Unix. Эта версия впервые была описана Брайаном Керниганом (Brian Kernighan) и Деннисом Ритчи в книге The С Programming Language (Englewood Cliffs, N.J.: Prentice-Hall, 1978). Летом 1983 года был образован комитет по созданию для языка С стандарта ANSI (American National Standards Institute — Нацио- нальный институт стандартизации США). Надо отметить, что процесс стандартизации занял весьма немалый срок — шесть лет. Стандарт ANSI был окончательно принят в декабре 1989 года и впервые опублико- ван в начале 1990 года. Этот стандарт был также принят организацией ISO (International Standards Organization — Международная организация по стандартизации), поэтому он называется стандартом ANSI/ISO языка С. В 1995 году была принята 1-я Поправка к стандарту С, согласно которой, среди прочего, было добавлено несколько библиотечных функций. В 1989 году стандарт С вместе с 1-й Поправкой стал базовым документом Стандарта C++, определяющего С как подмножество C++. Версию С, определенную стандартом 1989 года, обычно называют С89. На протяжении 90-х годов внимание программистов было приковано главным об- разом к развитию стандарта C++. Тем временем разработка С также продолжалась, приведя в 1999 году к появлению стандарта С, который принято называть С99. В це- лом С99 сохранил все основные черты С89, т.е., можно сказать, что язык С остался самим собой! Комиссия стандартизации С99 уделила основное внимание двум на- правлениям: добавлению нескольких численных библиотек и развитию новых узко- специальных средств, таких как массивы переменной длины и модификатор указателя restrict. Благодаря этим нововведениям язык С опять оказался на переднем крае развития языков программирования. Как указано выше, часть I этой книги посвящена основам С в версии стандарта 1989 года. В настоящее время это наиболее распространённая версия, она использует- ся всеми компиляторами С, она же является основой для C++. Поэтому при написа- нии программ на С для любого из существующих в настоящее время компиляторов необходимо придерживаться описания, приведенного в части I книги. В части II рас- сматриваются свойства, введенные стандартом С99. ш С - язык среднего уровня Язык С часто называют языком программирования среднего уровня. Но это не значит, что С менее мощный, менее развитой или более трудный в использовании, чем языки высокого уровня, такие как Basic или Pascal. Это также не значит, что С такой же громоздкий и неудобный, как ассемблер. Языком среднего уровня его назы- 32 Часть I. Основы языка С
вают скорее потому, что он объединяет в себе лучшие черты языков высокого уровня с возможностями ассемблера. В табл. 1.1 показано, какое место занимает С среди других языков программирования. Таблица 1.1. Место языка С среди других языков программирования Языки высокого уровня Ada Modula-2 Pascal COBOL FORTRAN Basic Языки среднего уровня Java C++ C FORTH Языки низкого уровня Макроассемблер Ассемблер Как язык среднего уровня С позволяет манипулировать битами, байтами и адреса- ми, то есть теми базовыми элементами данных, с которыми работает компьютер. Не- смотря на это программа, написанная на С, обладает высокой переносимостью. Пере- носимость — это свойство программного обеспечения, созданного для одного типа компьютера или операционной системы, позволяющее легко переделать его для дру- гого типа, т.е. перенести в другую вычислительную среду. Например, если программу, работающую под управлением DOS, легко переделать так, чтобы она работала под управлением Windows 2000, то такая программа называется переносимой1. Все языки высокого уровня придерживаются концепции типов данных. Тип данных представляет собой набор значений, хранящихся в переменных, а также набор операций, выполнение которых допускается над этими значениями. Обычные типы данных — это целые числа, символы и числа с плавающей точкой. Язык С имеет несколько встроенных типов данных, однако он не является сильно типизированным языком, как Pascal или Ada. В языке С допускаются почти все преобразования типов. Например, в выражениях можно свободно смешивать переменные символьного и целого типов. В отличие от большинства языков высокого уровня, в С почти отсутствует кон- троль ошибок в процессе выполнения программы. Например, не проверяется нару- шение границ массивов. Ответственность за подобные ошибки полностью возлагается на программиста. Аналогично этому С не требует строгой совместимости параметров и аргументов функций. В языках программирования высокого уровня обычно необходимо, чтобы тип аргумента более или менее соответствовал типу параметра. Для С это не харак- терно, здесь аргумент может иметь почти любой тип, если его можно разумно преоб- разовать в тип параметра. Более того, компилятор С автоматически осуществляет все виды необходимых преобразований. Отличительной особенностью языка С является возможность манипулирования непосредственно битами, байтами, словами и указателями. Поэтому С хорошо при- способлен для системного программирования. Другая важная особенность С — это малое количество ключевых слов, составляю- щих команды языка. В С89 определено 32 ключевых слова, причем в С99 добавлено только 5 слов. Языки высокого уровня обычно имеют значительно больше ключевых слов, например, в большинстве версий языка Basic их количество превышает сотню! 1 А также машинонезависимой, мобильной, а иногда даже портабильной. — Прим. ред. Глава 1. Обзор возможностей языка С 33
Язык С хорошо структурирован В книгах по программированию часто используется понятие блочной структурированно- сти языка. Хоть этот термин и нельзя применить в полной мере к языку С, его обычно называют просто структурированным языком, так как в этом отношении он очень похож на другие структурированные языки, такие как ALGOL, Pascal и Modula-2. На заметку Блочно-структурированные языки допускают определение функций внутри других функций. Поскольку в С такой возможности нет, формально он не мо- жет быть причислен к блочно-структурированным языкам. Отличительной особенностью структурированного языка является отдельное раз- мещение различных частей кода программы и данных. Таким способом программист может “скрыть” часть информации, используемую для выполнения специфической задачи, от тех участков программы, где эта информация не нужна. Один из способов достижения этого — использование подпрограмм с локальными переменными. В этом случае любые действия внутри программы не вызовут побочных эффектов в других ее частях. Это позволяет программам, написанным на С, совместно использовать гото- вые части кода. Для использования функции, хранящейся отдельно, необходимо толь- ко знать, что эта функция делает, при этом вовсе не обязательно знать, как именно она это делает. Но следует помнить, что чрезмерное использование глобальных пере- менных (то есть переменных, видимых во всей программе) приводит к ошибкам и по- бочным эффектам, которые очень трудно устранить (особенно хорошо знакомы с этой трудностью программисты, работавшие на стандартной версии языка Basic). Структурированный язык предоставляет программисту много различных возможностей. Например, структурированные языки обычно содержат несколько типов операторов цик- ла, таких как while, do-while и for. В структурированных языках использование опера- тора goto или запрещено, или не рекомендуется, для них он не является приемлемым средством управления процессом (что, однако, не относится к стандартной версии языка Basic и традиционной версии языка FORTRAN). Структурированный язык позволяет по- местить оператор в любом месте строки, не привязывая его к определенному полю (что характерно, например, для старых версий языка FORTRAN). Ниже приведены примеры структурированных и неструктурированных языков: Неструктурированные Структурированные FORTRAN BASIC COBOL Pascal ADA C++ С Java Modula-2 Структурированные языки появились сравнительно недавно. Фактически призна- ком того, что язык создан довольно давно, служит его неструктурированность. Сего- дня мало кто из программистов решится писать серьезную программу на неструкту- рированном языке. На заметку Попытки добавить элементы структурированности во многие старые языки предпринимались неоднократно. Так, одна из самых смелых попыток была предпринята для языка Basic. Однако преодолеть недостатки этих языков не удалось, так как при их создании изначально были проигнорированы принципы структурированности. Главная конструкция структурного программирования на языке С — функция, яв- ляющаяся здесь единственным видом подпрограммы. Функция С — это строительный 34 Часть I. Основы языка С
кирпичик, в котором осуществляются все действия программы. Функции позволяют определить и отдельно закодировать различные задачи, решаемые программой, благо- даря чему эта программа становится модульной. Написав правильно функцию, можно быть уверенным в ее надежной работе в различных ситуациях без побочных эффектов в других частях программы. При работе над большим проектом, когда особенно важ- но, чтобы одна часть кода ни в коем случае не могла непредвиденно подействовать на другую часть, умение создать отдельную функцию приобретает для программиста ис- ключительное значение. Другой способ структурирования программы, написанной на языке С, заключается в использовании программных блоков. Программный блок — это логически связанная группа операторов программы, которую можно рассматривать как отдельную про- граммную единицу. В языке С блок представляет собой последовательность операто- ров программы, заключенную в фигурные скобки. В примере кода Iif (х<10) { printf("Слишком мало, попытайтесь еще раз.п"); scant("%d", &х); } два оператора, стоящие после if в фигурных скобках, выполняются только в том случае, если значение х меньше десяти. Эти два оператора вместе со скобками составляют про- граммный блок. В данном примере эти операторы представляют собой логический блок, или программную единицу, так как один оператор не может быть выполнен без выполне- ния другого. Использование программных блоков позволяет сделать программу понятной, элегантной и эффективной. Более того, программные блоки помогают лучше формализо- вать задачу и более точно запрограммировать алгоритм ее решения. В Язык С создан для программистов Как ни удивительно, но не все языки программирования созданы для программистов. Классические примеры языков для непрограммистов — COBOL и Basic. COBOL был соз- дан не для того, чтобы облегчить жизнь программистам или повысить надежность про- граммного продукта, и даже не для повышения продуктивности труда программиста, а для того, чтобы непрограммисты могли читать и понимать написанные на нем программы. При создании языка Basic в значительной степени преследовалась цель сделать для не- программиста доступным решение на компьютере относительно простых задач. В противовес этому язык С был создан и апробирован активно работающими про- граммистами. В результате С обеспечивает то, чего и ждут от него именно программисты: небольшое количество ограничений, блочную структуру, автономные функции и малое количество ключевых слов. Программы, написанные на языке С, обладают эффективно- стью программ, написанных на языке ассемблера, и структурированностью, присущей программам, созданным на языках Pascal или Modula-2. Поэтому неудивительно, что во всем мире С стал универсальным языком программирования. Решающим фактором успеха языка С стало то, что во многих случаях он может быть использован вместо ассемблера, который основан на символическом представлении би- нарного кода, непосредственно выполняемого компьютером. Каждая операция ассемблера представляет для компьютера одну элементарную задачу. Разрабатывая программу на язы- ке ассемблера, программист может сделать программу максимально гибкой и эффектив- ной, однако работа с самой программой ассемблера и ее отладка — чрезвычайно трудоем- кий процесс. Более того, из-за отсутствия средств структурного программирования в языке ассемблера окончательная программа представляет собой то, что программисты называют “спагетти” — хаотичную совокупность переходов, индексов и вызовов функций. Из-за своей неструкгурированности программа, написанная на языке ассемблера, с большим Глава 1. Обзор возможностей языка С 35
трудом поддается расширению, модификации и даже просто пониманию. И что, возмож- но, наиболее существенно, процедуры, написанные на языке ассемблера, не обладают пе- реносимостью на компьютеры с процессорами, система команд которых отличается от системы команд исходного процессора. Первоначально С использовался для решения задач системного программирова- ния. Системная программа — это часть операционной системы компьютера или ути- лита, как, например, редактор, транслятор, компоновщик и т.п. По мере роста попу- лярности С, многие программисты стали использовать его для решения других задач благодаря его переносимости и эффективности, а также потому, что им это нрави- лось! Поистине этот язык стал долгожданным и впечатляющим достижением в облас- ти языков программирования. С появлением языка C++ многим программистам стало казаться, что С прекратил свое существование как отдельный язык программирования. Однако это не так. Во-первых, не для всех программ нужны объектно-ориентированные возможности C++. Например, та- кие приложения как системы внедрения объектов, по-прежнему программируются глав- ным образом на С. Во-вторых, в настоящее время во всем мире работает чрезвычайно много программ, написанных на С, причем разработчики продолжают модернизировать и поддерживать эти программы. В-третьих, разработка нового стандарта С99 убеждает в том, что развитие и совершенствование С продолжается. То, что С стал базисом для C++, на- всегда останется его неоспоримой заслугой, и в то же время язык С сам остается одним из лучших языков программирования. И Компиляторы и интерпретаторы Программист должен понимать, что язык программирования определяет характер программы, а не способ ее выполнения компьютером. Есть два способа выполнения про- граммы компьютером: она может быть подвергнута компиляции или интерпретации. Про- грамма, написанная на любом языке программирования, может как компилироваться, так и интерпретироваться, однако многие языки изначально созданы для выполнения пре- имущественно одним из этих способов. Например, Java рассчитан в основном на интер- претацию программы, а язык С — на компиляцию. Необходимо помнить, что при разра- ботке языка С его конструкции оптимизировались специально для компиляции. И хотя интерпретаторы С существуют и доступны для программистов (особенно как средства от- ладки или объект для экспериментов — в качестве такого объекта можно использовать, например, интерпретатор, рассмотренный в части VI этой книги), С разрабатывался пре- имущественно для компиляции. Поэтому при разработке программ на С большинство программистов используют именно компилятор, а не интерпретатор. Поскольку не все чи- татели этой книги достаточно хорошо понимают отличие компилятора от интерпретатора, ниже дается краткое разъяснение по этому поводу. В простейшем случае интерпретатор читает исходный текст программы по одной строке за раз, выполняет эту строку и только после этого переходит к следующей. Так работали ранние версии языка Basic. В языках типа Java исходный текст программы сначала конвертируется в промежуточную форму, а затем интерпретируется. В этом случае программа также интерпретируется в процессе выполнения. Компилятор читает сразу всю программу и конвертирует ее в объектный код, то есть транслирует исходный текст программы в форму, более пригодную для непосред- ственного выполнения компьютером. Объектный код также называют двоичным или машинным кодом. Когда программа скомпилирована, в ее коде уже нет отдельных строк исходного кода. В общем случае интерпретируемая программа выполняется медленнее, чем ском- пилированная. Необходимо помнить, что компилятор преобразует исходный текст 36 Часть I. Основы языка С
программы в объектный код, который выполняется компьютером непосредственно. Значит, потеря времени на компиляцию происходит лишь единожды, а в случае ин- терпретации — каждый раз при очередной компиляции фрагмента программы в про- цессе ее выполнения. Структура программы на языке С В табл. 1.2 перечислены 32 ключевых слова, определенные стандартом С89. Они же являются ключевыми словами языка С как подмножества C++. В табл. 1.3 приве- дены ключевые слова, добавленные стандартом С99. Набор ключевых слов вместе с формальным синтаксисом С составляет язык программирования С. Кроме стандартных ключевых слов, многие компиляторы для лучшего функциони- рования в среде программирования разрешают дополнительно использовать некото- рые нестандартные ключевые слова. Например, несколько компиляторов, рассчитан- ных на создание кода, выполняемого в моделях памяти, поддерживаемых процессора- ми семейства 8086, с целью поддержки взаимодействия программ, написанных на разных языках, а также для обеспечения доступа к прерываниям дополнительно вво- дят следующие ключевые слова: asm _ds huge pascal cdecl _es interrupt _ss _cs far near Для наиболее эффективного использования возможностей конкретного компиля- тора программист обязательно должен ознакомиться с набором дополнительных клю- чевых слов. Таблица 1.2. Ключевые слова стандарта С89 auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while Таблица 1.3. Ключевые слова, добавленные стандартом С99 _Bool -Imaginary restrict -Complex inline В языке С различаются верхний и нижний регистры символов: else — ключевое слово, a ELSE — нет. В программе ключевое слово может быть использовано только как ключевое слово, то есть никогда не допускается его использование в качестве пе- ременной или имени функции. Любая программа на С состоит из одной или нескольких функций. Обязательно должна быть определена единственная главная функция main (), именно с нее всегда начинается выполнение программы. В хорошем исходном тексте программы главная функция всегда содержит операторы, отражающие сущность решаемой задачи, чаще всего это вызовы функций. Хотя main () и не является ключевым словом, относиться Глава 1. Обзор возможностей языка С 37
к нему следует как к ключевому. Например, не следует использовать main как имя переменной, так как это может нарушить работу транслятора. Структура программы С изображена на рис. 1.1, здесь f 1 () - fN() означают функции, написанные программистом. Объявление глобальных переменных int main(список параметров) { последовательность операторов } тип_возвращаемого_значения fl(список п { последовательность операторов } тип_возвращаемого_значения f2(список п { последовательность операторов } тип_возвращаемого_значения fN(список п { последовательность операторов } Рис. 1.1. Структура программы на языке С Библиотеки и компоновка Следует отметить, что на С в принципе возможно создать программу, содержащую только имена переменных и ключевые слова. Но обычно так не поступают, потому что в С нет ключевых слов для выполнения многих операций, например, таких как ввод/вывод, вычисление математических функций, обработка строк и т.п. Поэтому в большинстве программ присутствуют вызовы различных функций, хранящихся в биб- лиотеке стандартных функций С. Все компиляторы С поставляются вместе с библиотекой стандартных функций, предназначенных для выполнения наиболее общих задач. Стандарт С определяет ми- нимальный набор функций, которые должны поддерживаться каждым компилятором. Но обычно библиотеки, поставляемые с компиляторами, имеют и много других, до- полнительных, функций. Например, в стандартной библиотеке нет функций для рабо- ты с графикой, зато они есть почти в каждом компиляторе. При вызове библиотечной функции компилятор “запоминает” ее имя. Потом компоновщик связывает код исходной программы с объектным кодом, уже найден- ным в стандартной библиотеке. Этот процесс называется компоновкой1. У некоторых компиляторов есть свой собственный компоновщик, другие пользуются стандартным компоновщиком, поставляемым вместе с операционной системой. В библиотеке функции хранятся в переместимом формате. Это значит, что адреса машинных инструкций в памяти являются не абсолютными, а относительными. При 1 Или редактированием связей. — Прим. ред. 38 Часть 1. Основы языка С
компоновке программы с функциями из стандартной библиотеки эти относительные адреса, или смещения, используются для определения действительных адресов. Для того чтобы научиться программировать на С (а значит и понять дальнейший материал данной книги), этого объяснения достаточно, более подробно процесс настройки ад- ресов изложен в других книгах. Библиотека стандартных функций содержит большое количество функций, необ- ходимых для написания программы. Это своего рода кирпичики, из которых про- граммист собирает программу. Кроме того, программист может написать свою функ- цию и поместить ее в библиотеку. В Раздельная компиляция Короткая программа на языке С может состоять всего лишь из одного файла ис- ходного текста. Однако при увеличении длины программы увеличивается также и время компиляции (при этом чем длиннее компиляция, тем короче терпение про- граммиста!). Программа на С может состоять из двух или более файлов, компилируе- мых отдельно. Скомпилированные файлы программы компонуются с процедурами из библиотеки, формируя таким образом объектный код программы. Преимущество раз- дельной компиляции состоит в том, что при изменении одного файла нет необходи- мости перекомпилировать заново всю программу. При работе со сложными проекта- ми это экономит много времени. Раздельная компиляция позволяет также несколь- ким программистам работать над одним проектом, так как она служит средством организации исходного текста программы для большого проекта. Более детально ис- пользование средств раздельной компиляции изложено в части V данной книги. Н Компиляция программы на языке С Создание выполнимой программы на языке С состоит из следующих трех шагов: разработка, компиляция и компоновка программы с библиотечными функциями. В настоящее время большинство компиляторов поставляется вместе с оболочкой программирования, содержащей редактор текста. Оболочки содержат обычно также автономный компилятор. При наличии автономного компилятора для написания программы можно использовать любой удобный редактор. В противном случае нужно быть очень внимательным, так как встроенный компилятор нормально работает толь- ко со стандартным текстовым файлом. Например, компиляторы не могут обрабаты- вать файлы, созданные некоторыми текстовыми процессорами, так как эти файлы со- держат управляющие коды и непечатаемые символы. Конкретный способ компиляции программы зависит от типа используемого компилятора. Для разных компиляторов и оболочек способы компоновки также могут быть разными, например, компоновка может выполняться компилятором, а может и отдельной программой. Эти вопросы обычно освещаются в документации компилятора. И Карта памяти программы на языке С Скомпилированная программа С имеет четыре логически обособленные области памяти. Первая — это область памяти, содержащая выполнимый код программы. Во второй области хранятся глобальные переменные. Оставшиеся две области — это стек Глава 1. Обзор возможностей языка С 39
и динамически распределяемая область памяти1. Стек используется для хранения вспомогательных переменных во время выполнения программы. Здесь находятся ад- реса возврата функций, аргументы функций, локальные переменные и т.п. Текущее состояние процессора также хранится в стеке. Динамически распределяемая область памяти, или куча — это такая свободная область памяти, для получения участков па- мяти из которой программа вызывает функции динамического распределения памяти. На рис. 1.2 показано, как распределяется память во время выполнения программы. Но не следует забывать, что конкретное распределение может быть разным в зависи- мости от типа процессора и реализации языка. Сравнительная характеристика языков С и C++ В заключение необходимо сказать несколько слов о языке C++. Начинающие программисты не всегда ясно представляют, что такое C++ и чем именно он отлича- ется от С. В нескольких словах, язык C++ — это объектно-ориентированный язык программирования, фундаментом которого является С. Язык С — это подмножество C++ и, следовательно, C++ — надмножество С. Стек Динамически распределяемая область памяти Глобальные переменные Код программы Рис. 1.2. Распределение памяти (карта памяти) при выполнении программы, на- писанной на языке С В общем случае компилятор C++ можно использовать для компиляции програм- мы, написанной на С. В настоящее время большинство компиляторов могут работать с программами, написанными как на С, так и на C++. Поэтому многие программи- сты используют компилятор C++ для компиляции программы, написанной на С. Но, поскольку C++ основан на стандарте С89, при написании программы С, рассчитан- 1 Называется также динамической областью, динамически распределяемой областью, кучей и неупорядоченным массивом. — Прим. ред. 40 Часть I. Основы языка С
ной на компилятор C++, допускается использование только тех возможностей языка, которые предусмотрены в С89 (они рассматриваются в части I). При написании программы на С, рассчитанной на компилятор C++, необходимо правильно указывать расширение файла, содержащего текст программы. Согласно действующему соглашению, файлы программ, написанных на С имеют расширение .С, а написанных на C++ — .СРР. Присвоение расширения .СРР файлу программы, написанной на С, недопустимо, потому как эти языки все же существенно отличают- ся друг от друга, и компилировать программу на С так, будто это программа на C++, нельзя. Расширение .С указывает транслятору на то, что он должен компилировать программу, написанную именно на С. Полное описание языка C++ приведено в книге Herbert Schildt. C++: The Complete Reference (Berkeley, CA: Osborne/McGraw-Hill). На заме i ky HI Словарь терминов Исходный текст (или код) программы. Текст программы, который можно про- честь. Обычно его и называют программой. Исходный текст программы вво- дится в компилятор С. Объектный код. Результат трансляции исходного текста в машинный код, кото- рый может быть прочитан и выполнен компьютером. Объектный код обычно вводится в компоновщик. Компоновщик или редактор связей. Программа, которая компонует (связывает) от- дельно оттранслированные модули в одну программу. Компоновщик также присое- диняет функции стандартной библиотеки С и функции, написанные программи- стом. Результатом работы компоновщика является выполнимая программа. Библиотека. Файл, содержащий стандартные функции, используемые програм- мой. Этот файл содержит операции ввода/вывода и другие полезные функции. Время компиляции. Время, затраченное компьютером на компиляцию программы. Время выполнения. Время, затраченное компьютером на выполнение программы. Глава 1. Обзор возможностей языка С 41
Полный ( ia о m ( по Глава 2 Выражения
В этой главе рассматриваются выражения — фундаментальные элементы языка С. По сравнению с другими языками программирования выражения языка С гораздо более гибкие и мощные. Составляющими элементами выражения являются данные и операторы1. Данные могут быть представлены переменными, константами или значе- ниями, возвращаемыми функциями. В языке С есть различные типы данных и боль- шое количество операторов. В Базовые типы данных Стандарт С89 определяет пять фундаментальных типов данных: char — символьные данные, int — целые, float — с плавающей точкой, double — двойной точности, void — без значения. На основе этих типов формируются другие типы данных. Размер (объем занимаемой памяти) и диапазон значений этих типов данных для разных процес- соров и компиляторов могут быть разными. Однако объект типа char всегда занимает 1 байт. Размер объекта int обычно совпадает с размером слова в конкретной среде про- граммирования. В большинстве случаев в 16-разрядной среде (DOS или Windows 4.1) int занимает 16 битов, а в 32-разрядной (Windows 95/98/NT/2000) — 32 бита. Однако полно- стью полагаться на это нельзя, особенно при переносе программы в другую среду. Необхо- димо помнить, что стандарт С обусловливает только минимальный диапазон значений каж- дого типа данных, но не размер в байтах. На заметку Кроме перечисленных пяти типов, стандарт С99 определяет еще три: _воо1, —Complex и-Imaginary. Они описаны в части II. Конкретный формат числа с плавающей точкой зависит от его реализации в трансляторе. Переменные типа char обычно используются для обозначения набора символов стандарта ASCII, символы, не входящие в этот набор, разными компилято- рами обрабатываются по-разному. Диапазон значений типов float и double зависит от формата представления чи- сел с плавающей точкой. Стандарт С определяет для чисел с плавающей точкой ми- нимальный диапазон значений от 1Е-37 до 1Е+37. Минимальное количество цифр мантиссы для типов с плавающей точкой приведено в табл. 2.1. Тип void служит для объявления функции, не возвращающей значения, или для создания универсального указателя. Оба эти применения будут рассмотрены в после- дующих главах. 1 В оригинале operators. Вообще в контексте языков программирования английскому слову operator соответствует два понятия: оператор и операция/знак операции. (В данном случае автор имеет в виду, конечно, знаки операций.) Еще каких-нибудь 30-40 лет назад более предпочтительным термином в данном случае был бы знак операции, поскольку в языках программировании под оператором пони- мались более сложные конструкции. Вместе с тем в математике, особенно в векторном анализе, а значит и в теории дифференциальных уравнений и математической физике под оператором часто имели в виду именно операцию или ее знак. (Например, говорили: “применим оператор набла” и тут же навешивали знак V!) Таким образом, использование термина оператор как синонима термина опе- рация/знак операции имеет глубокие корни (ведь первые ЭВМ были сконструированы для решения задач математической физики) и очень давнюю (столетнюю!) традицию. Что же касается русскоязыч- ной компьютерной литературы, то здесь тоже достаточно давно (хотя и не сто лет, конечно!) имеет место тенденция все более частого употребления термина оператор вместо термина операция/знак опе- рации. Первой книгой на русском языке, посвященной языку С, был перевод легендарного учебника The С Programming Language, написанного Б. Керниганом и Д. Ритчи. В первом издании перевода ис- пользовался термин операция. Однако уже во втором издании (1992 г.) его заменил термин оператор'. Конечно, такое употребление этого термина идет вразрез со “школьной” традицией, но, как показала многолетняя практика, каких-либо серьезных трудностей при этом не возникает. — Прим. ред. 44 Часть I. Основы языка С
S Модификация базовых типов Базовые типы данных (кроме void) могут иметь различные спецификаторы1, предшествующие им в тексте программы. Спецификатор типа так изменяет значение базового типа, чтобы он более точно соответствовал своему назначению в программе. Полный список спецификаторов типов: signed unsigned long short Базовый тип int может быть модифицирован каждым из этих спецификаторов. Тип char модифицируется с помощью unsigned и signed, double — с помощью long. (Стандарт С99 также позволяет модифицировать long с помощью long, созда- вая таким образом long long, см. часть II). В табл. 2.1 приведены все допустимые комбинации типов данных с их минимальным диапазоном значений и типичным размером. Обратите внимание, в таблице приведены минимально возможные, а не ти- пичные диапазоны значений. Например, если в компьютере арифметические опера- ции выполняются над числами в дополнительных кодах (а именно так спроектирова- ны почти все компьютеры!), то в диапазон значений целых попадут все целые числа от -32767 до 32768. Таблица 2.1. Все типы данных, определенные Стандартом С Тип Типичный размер в битах Минимально допустимый диапазон значений char 8 от-127 до 127 unsigned char 8 от 0 до 255 signed char 8 от -127 до 127 int 16 или 32 от-32767 до 32767 unsigned int 16 или 32 от 0 до 65535 signed int 16 или 32 то же, что int short int 16 от-32767 до 32767 unsigned short int 16 от 0 до 65535 signed short int 16 то же, что short int long int 32 от -2 147 483 647 до 2 147 483 647 long long int 64 от—(263—1)до (2е3—1), добавлен стандартом С99 signed long int 32 то же, что long int unsigned long int 32 от 0 до 4 294 967 295 unsigned long long int 64 от 0 до (г64-^, добавлен в С99 float 32 ОТ1Е-37 до 1Е+37, с точностью не менее 6 значащих десятичных цифр double 64 OT1E-37 до 1Е+37, с точностью не менее 10 значащих десятичных цифр long double 80 ОТ1Е-37 ДО1Е+37, с точностью не менее 10 значащих десятичных цифр 1 Называются также описателями, модификаторами и квалификаторами. — Прим. ред. Глава 2. Выражения 45
Для целых можно использовать спецификатор signed, но в этом нет необходимо- сти, потому что при объявлении целого он предполагается по умолчанию. Специфи- катор signed чаще всего используется для типа char, который в некоторых реализа- циях по умолчанию может быть беззнаковым. Целые числа со знаком и без знака отличаются интерпретацией нулевого бита чис- ла. Если целое объявлено со знаком, компилятор считает, что нулевой бит содержит знак числа. Если в нулевом бите записан 0, число считается положительным, а если 1 — отрицательным. В большинстве реализаций отрицательные числа представлены в двоичном дополни- тельном коде. Это значит, что для отрицательного числа все биты, кроме нулевого, ин- вертируются, к полученному числу добавляется 1, а нулевой бит устанавливается в 1. Целые числа со знаком используются почти во всех алгоритмах, но абсолютная ве- личина наибольшего из них составляет примерно только половину максимального це- лого без знака. Например, знаковое целое число 32767 в двоичном коде имеет вид 01111111 11111111 Если в нулевой бит записать 1, то оно будет интерпретироваться как —1. Однако если полученную запись рассматривать как представление числа, объявленного как unsigned int, то оно будет интерпретироваться как 65535. Если спецификатор типа записать сам по себе (без следующего за ним базового типа), то предполагается, что он модифицирует тип int. Таким образом, следующие спецификаторы типов эквивалентны: Спецификатор______________________________То же самое________________________________ signed signed int unsigned unsigned int long long int short short int Хотя базовый тип int и предполагается по умолчанию, его, тем не менее, обычно указывают явно. Н Имена переменных В языке С имена переменных, функций, меток и т.п. называются идентификато- рами. Длина идентификатора (количество символов, из которых состоит идентифика- тор) является натуральным числом, обычно идентификатор представляет собой после- довательность из одного или нескольких символов. Первый символ должен быть бук- вой или символом подчеркивания, последующие символы должны быть буквами, цифрами или символами подчеркивания. Ниже приведены примеры правильных и неправильных записей идентификаторов: Правильные count test23 high balance Неправильные 1 count hi’here high...balance В языке С длина идентификатора может быть любой, однако не все его символы будут значащими. Объясним это на примере внешних и внутренних идентификаторов. Внешние идентификаторы участвуют во внешнем процессе компоновки1. Эти иденти- фикаторы, называемые внешними именами, обозначают имена функций и глобальных 1 Редактирование связей, или разрешение внешних ссылок. — Прим. ред. 46 Часть I. Основы языка С
переменных, которые используются совместно в различных исходных файлах. Если идентификатор не участвует в процессе редактирования внешних ссылок, то он назы- вается внутренним именем. К таким именам принадлежат, например, имена локальных переменных. В стандарте С89 значащими являются как минимум первые 6 символов внешнего имени и первые 31 символ внутреннего имени. Стандарт С99 увеличил этот диапазон, в нем значащими являются для внешнего идентификатора первые 31 сим- вол, а для внутреннего — первые 63 символа. Кстати, в C++ значащими являются как минимум 1024 символа любого идентификатора. Эти отличия необходимо учитывать при конвертировании программ, написанных на языках С89, С99 или просто С, в программы на C++. Верхние и нижние регистры символов рассматриваются как различные. Следова- тельно, count, Count и COUNT — это три разных идентификатора. Идентификатор не может совпадать с ключевым словом С или с именем библио- течной функции. S Переменные Переменная — это именованный участок памяти, в котором хранится значение, которое может быть изменено программой. Все переменные перед их использованием должны быть объявлены. Общая форма объявлений имеет такой вид: тип список_переменных; Здесь тип означает один из базовых или объявленных программистом типов (если необходимо — с одним или несколькими спецификаторами), а список_переменных со- стоит из одного или более идентификаторов, разделенных запятыми. Ниже приведены примеры объявлений: Iint i t j t 1; short int si; unsigned int ui; double balance, profit, loss; Необходимо помнить, что в С имя переменной никогда не определяет ее тип. Где объявляются переменные Объявление переменных может быть расположено в трех местах: внутри функции, в определении параметров функции и вне всех функций. Это — места объявлений со- ответственно локальных переменных, формальных параметров функций и глобальных переменных. Локальные переменные Переменные, объявленные внутри функций, называются локальными переменными. В некоторых книгах по С они называются динамическими переменными1 2. В этой книге используется более распространенный термин локальная переменная. Локальную пере- менную можно использовать только внутри блока, в котором она объявлена. Иными словами, локальная переменная невидима за пределами своего блока. (Блок програм- мы — это описания и инструкции, объединенные в одну конструкцию путем заклю- чения их в фигурные скобки.) 1 Называется также описанием или декларацией. — Прим. ред. 2 А в книгах по C++ переменной автоматического класса памяти (т.е. такой, что создается при входе в блок, где она объявлена, и уничтожается при выходе из него). — Прим. ред. Глава 2. Выражения 47
Локальные переменные существуют только во время выполнения программного блока, в котором они объявлены, создаются они при входе в блок, а разрушаются — при выходе из него. Более того, переменная, объявленная в одном блоке, не имеет никакого отношения к переменной с тем же именем, объявленной в другом блоке. Чаще всего блоком программы, в котором объявлены локальные переменные, яв- ляется функция. Рассмотрим, например, следующие две функции: void fund (void) { int x; x = 10; } void func2(void) { int x; x = -199; } Целая переменная x объявлена дважды: один раз в funcl() и второй— в func2 (). При этом переменная х в одной функции никак не связана и никак не влияет на переменную с тем же именем в другой функции. Это происходит потому, что локальная переменная видима только внутри блока, в котором она объявлена, за пределами этого блока она невидима. В языке С есть ключевое слово auto (спецификатор класса памяти), которое мож- но использовать в объявлении локальной переменной. Однако так как по умолчанию предполагается, что все переменные, не являющиеся глобальными, являются динами- ческими, то ключевое слово auto почти никогда не используется, а поэтому в приме- рах в данной книге отсутствует. Из соображений удобства и в силу устоявшейся традиции все локальные переменные функции чаще всего объявляются в самом начале функции, сразу после открывающейся фигурной скобки. Однако можно объявить локальную переменную и внутри блока про- граммы (блок функции — это частный случай блока программы). Например: void f(void) { int t; scanf("%d%*c", &t); if(t==l) { char s[80]; /* эта переменная создается только при входе в этот блок */ printf("Введите имя:"); gets(s); /* некоторые операторы */ } /* здесь переменная s невидима */ } В этом примере локальная переменная s создается при входе в блок i f и разруша- ется при выходе из него. Следовательно, переменная s видима только внутри блока if и не может быть использована ни в каких других местах, даже если они находятся внутри функции, содержащей этот блок. 48 Часть I. Основы языка С
Объявление переменных внутри блока программы помогает избежать нежелательных побочных эффектов. Переменная не существует вне блока, в котором она объявлена, сле- довательно, “посторонний” участок программы не сможет случайно изменить ее значение. Если имена переменных, объявленных во внутреннем и внешнем (по отношению к нему) блоках совпадают, то переменная внутреннего блока “прячет” (т.е. скрывает, делает невидимой) переменную внешнего блока. Рассмотрим следующий пример: #include <stdio.h> int main(void) { int x; x = 10; if(x == 10){ int x; /* эта x прячет внешнюю x */ x = 99; printf("Внутренняя x: %dn", x) ; } printf("Внешняя x: %dn", x); return 0; } Результат выполнения программы следующий: (Внутренняя х: 99 Внешняя х: 10 В этом примере переменная х, объявленная внутри блока if, делает невидимой внешнюю переменную х. Следовательно, внутренняя и внешняя х — это два разных объекта. Когда блок заканчивается, внешняя х опять становится видимой. В стандарте С89 все локальные переменные должны быть объявлены в начале бло- ка, до любого выполнимого оператора. Например, следующая функция вызовет ошибку компиляции в С89: /* Эта функция вызывает ошибку крмпиляции на компиляторе С89 */ void f(void) int i ; i = 10; int j; /* Ошибка в этой строке */ j = 20; } Однако в С99 (и в C++) эта функция вполне работоспособна, потому что в них локальная переменная может быть объявлена в любом месте внутри блока до ее пер- вого использования. Так как локальные переменные создаются и уничтожаются при каждом входе и выходе из блока, их значение теряется каждый раз, когда программа выходит из бло- ка. Это необходимо учитывать при вызове функции. Локальная переменная создается Глава 2. Выражения 49
при входе в функцию и разрушается при выходе из нее. Это значит, что локальная переменная не сохраняет свое значение в период между вызовами (однако можно дать указание компилятору сохранить значение локальной переменной, для этого нужно объявить ее с модификатором static). По умолчанию локальные переменные хранятся в стеке. Стек — динамически из- меняющаяся область памяти. Вот почему в общем случае локальные переменные не сохраняют свое значение в период между вызовами функций. Локальные переменные можно инициализировать каким-либо заранее заданным значением. Это значение будет присвоено переменной каждый раз при входе в тот блок программы, в котором она объявлена. Например, следующая программа напеча- тает число 10 десять раз: #include <stdio.h> void f(void); int main(void) { int i; for(i=0; i<10; i++) f(); return 0; } void f(void) { int j = 10; printf(”%d ", j) ; j++; /* этот оператор не влияет на результат */ } Формальные параметры функции Если функция имеет аргументы, значит должны быть объявлены переменные, ко- торые примут их значения. Эти переменные называются формальными параметрами функции. Внутри функции они фигурируют как обычные локальные переменные. Как показано в следующем фрагменте программы, они объявляются после имени функ- ции внутри круглых скобок: /* Возвращает 1, если в строке s содержится символ с, в противном случае возвращает 0 */ int is__in(char *s, char с) { while (*s) if(*s==c) return 1; else s++; return 0; } Функция is_in() имеет два параметра: s и с, она возвращает 1, если символ, за- писанный в переменной с, входит в строку s, в противном случае она возвращает 0. Внутри функции формальные параметры ничем не отличаются от обычных ло- кальных переменных, единственное их отличие состоит в том, что при входе в функ- цию они получают значения аргументов. Можно, например, присваивать параметру какое-либо значение или использовать его в выражении. Необходимо помнить, что, как и локальные переменные, формальные параметры тоже являются динамическими переменными и, следовательно, разрушаются при выходе из функции. 50 Часть I. Основы языка С
Глобальные переменные В отличие от локальных, глобальные переменные видимы и могут использоваться в любом месте программы. Они сохраняют свое значение на протяжении всей работы программы. Чтобы создать глобальную переменную, ее необходимо объявить за пре- делами функции. Глобальная переменная может быть использована в любом выраже- нии, независимо от того, в каком блоке это выражение используется. В следующем примере переменная count объявлена вне каких бы то ни было функ- ций. Ее объявление расположено перед main (), однако, оно может находиться в любом месте перед первым использованием этой переменной, но только не внутри функции. Объявлять глобальные переменные рекомендуется в верхней части программы. #include <stdio.h> int count; /* глобальная переменная count */ void fund (void) ; void func2(void); int main(void) { count = 100; fund () ; return 0; } void fund (void) { int temp; temp = count; func2(); printf (’’count равно %d”, count); /* напечатает 100 */ } void func2(void) { int count; for(count=l; countclO; count++) putchar(’.’); } Внимательно посмотрите на эту программу. Обратите внимание на то, что ни в fund (), ни в func2 () нет объявления переменной count, однако они обе могут ее использовать. В f unc2 () эта возможность не реализуется, так как в ней объяв- лена локальная переменная с тем же именем. Когда внутри func2() происходит обращение к переменной count, то это будет обращение к локальной, а не гло- бальной переменной. Таким образом, выполняется следующее правило: если ло- кальная и глобальная переменные имеют одно и то же имя, то при обращении к ней внутри блока, в котором объявлена локальная переменная, происходит ссыл- ка на локальную переменную, а на глобальную переменную это никак не влияет. Глобальные переменные хранятся в отдельной фиксированной области памяти, созданной компилятором специально для этого. Глобальные переменные исполь- зуются в тех случаях, когда разные функции программы используют одни и те же данные. Однако рекомендуется избегать излишнего использования глобальных Глава 2. Выражения 51
переменных, потому что они занимают память в течение всего времени выполне- ния программы, а не только тогда, когда они необходимы. Кроме того, и это еще более важно, использование глобальной переменной делает функцию менее уни- версальной, потому что в этом случае функция использует нечто, определенное вне ее. К тому же большое количество глобальных переменных легко приводит к ошибкам в программе из-за нежелательных побочных эффектов. При увеличении размера программы серьезной проблемой становится случайное изменение значе- ния переменной где-то в другой части программы, а когда глобальных перемен- ных много, предотвратить это очень трудно. В Четыре типа областей видимости В предыдущем (как, впрочем, и в последующем) изложении для объяснения раз- личий между идентификаторами, объявленными вне блока и внутри его, используют- ся термины глобальная переменная и локальная переменная. Однако в языке С преду- смотрено более тонкое подразделение этих двух широких категорий. Стандарт С оп- ределяет четыре типа областей видимости1 идентификаторов: Тил области видимости Область видимости область действия — файл (имя, объявленное вне всех блоков и классов, можно ис- пользовать в транслируемом файле, содержащем это объ- явление; такие имена назы- ваются глобальными (global)) область действия — блок область действия — прототип функции область действия— функция (имена, объявленные в функ- ции, могут быть использованы только в теле функции) Начинается в начале файла (единица трансляции) и кончается в конце файла. Такую область видимости имеют только идентифи- каторы, объявленные вне функций. Эти идентификаторы видимы в любом месте файла. Переменные с этой областью видимости являются глобальными Начинается открывающейся фигурной скобкой { блока и конча- ется с его закрытием скобкой }. Эту область видимости имеют также параметры функции. Переменные, имеющие такую об- ласть видимости, являются локальными в своем блоке Идентификаторы, объявленные в прототипе функции, видимы внутри прототипа Начинается открывающейся фигурной скобкой { функции и кон- чается с ее закрытием скобкой }. Такую область видимости име- ют только метки. Метка используется оператором goto и должна находится внутри той же функции В этой книге используется главным образом более общее подразделение на гло- бальные и локальные имена. Однако при необходимости более тонкого подразделения используются изложенные выше типы областей видимости. В Квалификаторы типа В языке С определяются квалификаторы типа1 2, указывающие на доступность и модифицируемость переменной. Стандарт С89 определяет два квалификатора: const и volatile. (С99 добавляет третий, restrict, описанный в части II.) Квалификатор типа должен предшествовать имени типа, который он квалифицирует (уточняет). 1 Область видимости называется также контекстом или областью действия (имен). — Прим. ред. 2 Называются также классификаторами, описателями, спецификаторами. — Прим. ред. 52 Часть I. Основы языка С
Квалификатор const Переменная, к которой в объявлении (декларации) применен квалификатор const, не может изменять свое значение1. Ее можно только инициализировать, то есть присвоить ей значение в начале выполнения программы. Компилятор может по- местить переменную этого типа в постоянное запоминающее устройство, так назы- ваемое ПЗУ (ROM, read-only memory). Например, в объявлении | const int а=10; создается переменная с именем а, причем ей присваивается начальное значение 10, которое в дальнейшем в программе изменить никак нельзя. Переменную, к которой в объявлении применен квалификатор const, можно использовать в различных выра- жениях. Однако свое значение она может получить только в результате инициализа- ции или с помощью аппаратно-зависимых средств. Квалификатор const часто используется для того, чтобы предотвратить изменение функцией объекта, на который указывает аргумент функции. Без него при передаче в функцию указателя эта функция может изменить объект, на который он указывает. Одна- ко если в объявлении параметра-указателя применен квалификатор const, функция не сможет изменить этот объект. В следующем примере функция sp_to_dash() печатает минус вместо каждого пробела в строке, передаваемой ей как аргумент. То есть строка “тестовый пример” будет напечатана как “тестовый-пример”. Применение квалифика- тора const в объявлении параметра функции гарантирует, что внутри функции объект, на который указывает параметр функции, не будет изменен. tfinclude <stdio.h> void sp_to_dash(const char *str); int main(void) { sp_to_dash("тестовый пример"); return 0; } void sp_to_dash(const char *str) { while(*str){ if(*str== ’ ’) printf("%c", ’-’); else printf("%c", *str); str++; } } Если написать sp_to_dash () таким образом, что внутри функции строка изменя- ется, то еще на этапе компиляции в программе будет обнаружена ошибка. Например, на этапе компиляции возникнет ошибка, если написать так: I /* Неправильный пример. */ I void sp_to_dash(const char *str) I { 1 На жаргоне программистов: переменная “типа” const не может изменять значение. В описании многих языков такие переменные часто называются константами. Но если исключить левые части операторов присваивания, то переменные этого “типа” могут использоваться в тех же ситуациях, что и “настоящие” переменные. В этом смысле константы являются частным случаем переменных. — Прим. ред. Глава 2. Выражения
while(*str){ if(*str==’ ’) *str= /* Это неправильно */ printf("%c", *str); str++; } } Квалификатор const используется в объявлениях параметров многих функций стандартной библиотеки. Например, прототип функции strlen() выглядит так: | size__t strlen (const char ★str) ; Применение квалификатора const в объявлении str гарантирует, что функция не изменит строку, на которую указывает str. Если функция стандартной библиотеки не предназначена для изменения аргумента, то практически всегда в объявлении указате- ля на аргумент применяется квалификатор const. Программист тоже может применять квалификатор const для того, чтобы гаран- тировать сохранность объекта. Но следует помнить, что переменная, даже если к ней применен квалификатор const, может быть изменена в результате какого-нибудь внешнего по отношению к программе воздействия. Например, ей может быть при- своено значение каким либо устройством. Однако применение квалификатора const в объявлении переменной гарантирует, что ее изменение может произойти только в ходе внешнего по отношению к программе события. Квалификатор volatile Квалификатор volatile указывает компилятору на то, что значение перемен- ной может измениться независимо от программы, т.е. вследствие воздействия еще чего-либо, не являющегося оператором программы. Например, адрес глобальной переменной можно передать в подпрограмму операционной системы, следящей за временем, и тогда эта переменная будет содержать системное время. В этом слу- чае значение переменной будет изменяться без участия какого-либо оператора программы. Знание таких подробностей важно потому, что большинство компи- ляторов С автоматически оптимизируют некоторые выражения, предполагая при этом неизменность переменной, если она не встречается в левой части оператора присваивания. В этом случае при очередной ссылке на переменную может ис- пользоваться ее предыдущее значение. Некоторые компиляторы изменяют поря- док вычислений в выражениях, что может привести к ошибке, если в выражении присутствует переменная, вычисляемая вне программы. Квалификатор volatile предотвращает такие изменения программы. Квалификаторы const и volatile могут применяться и совместно. Например, если 0x30 — адрес порта, значение в котором может задаваться только извне, то следующее объявление предотвратит всякую возможность нежелательных побочных эффектов: | const volatile char *port = (const volatile char *) 0x30; И Спецификаторы класса памяти Стандарт С поддерживает четыре спецификатора класса памяти: extern static register auto 54 Часть I. Основы языка С
Эти спецификаторы сообщают компилятору, как он должен разместить соответст- вующие переменные в памяти. Общая форма объявления переменных при этом такова: спецификатор_класса_памяти тип имя_переменой; Спецификатор класса памяти в объявлении всегда должен стоять первым. Стандарты С89 и С99 из соображений удобства синтаксиса утверждают, что typedef— это спецификатор класса памяти. Однако typedef не яв- ляется собственно спецификатором. Подробнее typedef рассматрива- ется в книге далее. На заметку Спецификатор extern Прежде чем приступить к рассмотрению спецификатора extern, необходимо ко- ротко остановиться на компоновке программы. В языке С при редактировании связей к переменной может применяться одно из трех связываний: внутреннее, внешнее или же не относящееся ни к одному из этих типов. (В последнем случае редактирование связей к ней не применяется.) В общем случае к именам функций и глобальных пе- ременных применяется внешнее связывание. Это означает, что после компоновки они будут доступны во всех файлах, составляющих программу. К объектам, объявленным со спецификатором static и видимым на уровне файла, применяется внутреннее связывание, после компоновки они будут доступны только внутри файла, в котором они объявлены. К локальным переменным связывание не применяется и поэтому они доступны только внутри своих блоков. Спецификатор extern указывает на то, что к объекту применяется внешнее свя- зывание, именно поэтому они будут доступны во всей программе. Далее нам понадо- бятся чрезвычайно важные понятия объявления и описания. Объявление (декларация) объявляет имя и тип объекта. Описание1 выделяет для объекта участок памяти, где он будет находиться. Один и тот же объект может быть объявлен неоднократно в разных местах, но описан он может быть только один раз. В большинстве случаев объявление переменной является в то же время и ее опи- санием. Однако, если перед именем переменной стоит спецификатор extern, то объ- явление переменной может и не быть ее описанием. Таким образом, если нужно со- слаться на переменную, определенную в другой части программы, необходимо объя- вить ее как внешнюю (extern). Приведем пример использования спецификатора extern. Обратите внимание, что глобальные переменные first и last объявлены после main (). tfinclude <stdio.h> int main(void) { extern int first, last; /* используются глобальные переменные */ printf("%d %d", first, last); return 0; } /* описание глобальных переменных first и last */ int first = 10, last = 20; 1 Синонимы: определение, дефиниция. — Прим. ред. Глава 2. Выражения 55
Программа напечатает 10 20, потому что глобальные переменные first и last ини- циализированы этими значениями. Объявление extern сообщает компилятору, что пере- менные first и last определены в другом месте, поэтому программа компилируется без ошибки, несмотря даже на то, что first и last используются до своего описания. Обратите внимание, в этом примере объявление переменных со спецификатором extern необходимо только потому, что они не были объявлены до main (). Если бы их объявление встретилось перед main (), то в объявлении со спецификатором ex- tern не было бы необходимости. При компиляции выполняются следующие правила. Если компилятор находит пере- менную, не объявленную внутри блока, он ищет ее объявление во внешних блоках. Если не находит ее и там, то ищет среди объявлений глобальных переменных. В предыдущем примере, если бы не было объявления extern, компилятор не нашел бы first и last среди глобальных переменных, потому что они объявлены после main (). Здесь специфи- катор extern сообщает компилятору, что эти переменные будут объявлены в файле позже. Как сказано выше, спецификатор extern позволяет объявить переменную, не описы- вая ее. Но если в объявлении со спецификатором extern инициализировать переменную, то это объявление становится также и описанием. При этом программист обязательно должен учитывать, что объект может иметь много объявлений, но лишь одно описание. Спецификатор extern играет большую роль в программах, состоящих из многих файлов. В языке С программа может быть записана в нескольких файлах, которые компилируются раздельно, а затем компонуются в одно целое. В этом случае необхо- димо как-то сообщить всем файлам о глобальных переменных программы. Самый лучший (и наиболее переносимый) способ сделать это — определить (описать) все глобальные переменные в одном файле и объявить их со спецификатором extern в остальных файлах, как показано на рис. 2.1. Первый файл int х, у; char ch; int main(void) { /* ... */ } void fund (void) { x=123; } Второй файл extern int x, y; extern char ch; void func22(void) { x = у / 10; } void func23(void) { У = 10; } Рис 2.1. Использование глобальных переменных в раздельно компилируемых модулях Во втором файле спецификатор extern сообщает компилятору, что эти перемен- ные определены в других файлах. Таким образом компилятор узнает имена и типы переменных, размещенных в другом месте, и может отдельно компилировать второй файл, ничего не зная о первом. При компоновке этих двух модулей все ссылки на глобальные переменные будут разрешены. На практике программисты обычно включают объявления extern в заголовочные файлы, которые просто подключаются к каждому файлу исходного текста программы. Это более легкий путь, который к тому же приводит к меньшему количеству ошибок, чем повторение этих объявлений вручную в каждом файле. 56 Часть I. Основы языка С
I 111 HlMC I Ky Спецификатор extern можно применять в объявлении функций, но в этом нет необходимости. Спецификатор static Переменные, объявленные со спецификатором static, хранятся постоянно внут- ри своей функции или файла. В отличие от глобальных переменных они невидимы за пределами своей функции или файла, но они сохраняют свое значение между вызо- вами. Эта особенность делает их полезными в общих и библиотечных функциях, ко- торые будут использоваться другими программистами. Спецификатор static воздей- ствует на локальные и глобальные переменные по-разному. Локальные статические переменные Для локальной переменной, описанной со спецификатором static, компилятор выде- ляет в постоянное пользование участок памяти, точно так же, как и для глобальных пере- менных. Коренное отличие статических локальных от глобальных переменных заключает- ся в том, что статические локальные переменные видны только внутри блока, в котором они объявлены. Говоря коротко, статические локальные переменные — это локальные пе- ременные, сохраняющие свое значение между вызовами функции. Статические локальные переменные очень важны при создании функций, работающих отдельно, так как многие процедуры требуют сохранения некоторых значений между вы- зовами. Если бы не было статических переменных, вместо них пришлось бы использовать глобальные, подвергая их риску непреднамеренного изменения другими участками про- граммы. Рассмотрим пример функции, в которой особенно уместно применение статиче- ской локальной переменной. Это — генератор последовательности чисел, каждое из кото- рых зависит только от предыдущего. Для хранения числа между вызовами можно исполь- зовать глобальную переменную. Однако тогда при каждом использовании функции придется объявлять эту переменную и, что особенно неудобно, постоянно следить за тем, чтобы ее объявление не конфликтовало с объявлениями других глобальных переменных. Значительно лучшее решение — объявить эту переменную со спецификатором static: int series(void) { static int series__num; series__num = series__num+23; return series_num; } В этом примере переменная series__num продолжает существовать между вызова- ми функций, в то время как обычная локальная переменная создается заново при ка- ждом вызове, а затем уничтожается. Поэтому в данном примере каждый вызов se- ries () генерирует новое число, зависящее от предыдущего, причем удается обойтись без глобальных переменных. Статическую локальную переменную можно инициализировать. Это значение при- сваивается ей только один раз — в начале работы всей программы, но не при каждом входе в блок программы, как обычной локальной переменной. В следующей версии функции series () статическая локальная переменная инициализируется числом 100: Iint series(void) < static int series__num = 100; Глава 2. Выражения 57
|series_num = series_num+23; return series_num; } Теперь эта функция всегда будет генерировать последовательность, начинающуюся с числа 123. Однако во многих случаях необходимо дать пользователю программы возможность ввести первое число вручную. Для этого переменную series_num можно сделать глобальной и предусмотреть возможность задания начального значения. Если же отказаться от объявления переменной series_num в качестве глобальной, то необ- ходимо ее объявить со спецификатором static. Глобальные статические переменные Спецификатор static в объявлении глобальной переменной заставляет компилятор создать глобальную переменную, видимую только в том файле, в котором она объявлена. Статическая глобальная переменная, таким образом, подвергается внутреннему связыва- нию, как описано ранее в разделе “Спецификатор extern”. Это значит, что хоть эта пере- менная и глобальная, тем не менее процедуры в других файлах не увидят ее и не смогут случайно изменить ее значение. Этим снижается риск нежелательных побочных эффектов. А в тех относительно редких случаях, когда для выполнения задачи статическая локальная переменная не подойдет, можно создать небольшой отдельный файл, который содержит только функции, в которых используется эта статическая глобальная переменная. Затем этот файл необходимо откомпилировать отдельно; тогда можно быть уверенным, что по- бочных эффектов не будет. В следующем примере иллюстрируется применение статической глобальной пере- менной. Здесь генератор последовательности чисел переделан так, что начальное чис- ло задается вызовом другой функции, series_start (): /* Это должно быть в одном файле отдельно от всего остального */ static int series_num; void series_start(int seed); int series(void); int series(void) { series_num = series_num+23; return series_num; } /* инициализирует переменную series_num ★/ void series_start(int seed) { series_num = seed; } Вызов функции series_start () с некоторым целым числом в качестве параметра инициализирует генератор series (). После этого можно генерировать последова- тельность чисел путем многократного вызова series (). Обзор: Имена локальных статических переменных видимы только внутри блока, в котором они объявлены; имена глобальных статических переменных видимы только внутри файла, в котором они объявлены. Если поместить функции series () и series_num() в библиотеку, то уже нельзя будет сослаться на переменную series_num, она оказалась спрятанной от любых операторов всей остальной программы. При этом в программе (конечно, в других файлах) можно объ- явить и использовать другую переменную под именем series_num. Иными словами, спе- 58 Часть I. Основы языка С
цификатор static позволяет создать переменную, видимую только для функций, в кото- рых она нужна, что исключает нежелательные побочные эффекты. Таким образом, при разработке больших и сложных программ для “сокрытия” пе- ременных можно применять спецификатор static. Спецификатор register Первоначально спецификатор класса памяти register применялся только к пере- менным типа int, char и для указателей. Однако стандарт С расширил использование спецификатора register, теперь он может применяться к переменным любых типов. В первых версиях компиляторов С спецификатор register сообщал компилятору, что переменная должна храниться в регистре процессора, а не в оперативной памяти, как все остальные переменные. Это приводит к тому, что операции с переменной register осуществляются намного быстрее, чем с обычными переменными, потому такая переменная уже находится в процессоре и не нужно тратить время на выборку ее значения из оперативной памяти (и на запись в память). В настоящее время определение спецификатора register существенно расширено. Стандарты С89 и С99 попросту декларируют “доступ к объекту так быстро, как только возможно”. Практически при этом символьные и целые переменные по-прежнему раз- мещаются в регистрах процессора. Конечно, большие объекты (например, массивы) не могут поместиться в регистры процессора, однако компилятор получает указание “позаботиться” о быстродействии операций с ними. В зависимости от конкретной реа- лизации компилятора и операционной системы переменные register обрабатываются по-разному. Иногда спецификатор register попросту игнорируется, а переменная об- рабатывается как обычная, однако на практике это бывает редко. Спецификатор register можно применить только к локальным переменным и формальным параметрам функций. В объявлении глобальных переменных примене- ние спецификатора register не допускается. Ниже приведен пример использования переменной, в объявлении которой применен спецификатор register; эта перемен- ная используется в функции возведения целого числа m в степень. (Степень — нату- ральное число — представлена идентификатором е.) int int_pwr(register int m, register int e) { register int temp; temp = 1; for(; e; e—) temp = temp * m; return temp; } В этом примере в объявлениях к переменным е, m и temp применен спецификатор register потому, что они используются внутри цикла. Переменные register иде- ально подходят для оптимизации скорости работы цикла. Как правило, переменные register используются там, где от них больше всего пользы, а именно, когда про- цесс многократно обращается к одной и той же переменной. Это существенно пото- му, что в объявлении можно применить спецификатор register к любой перемен- ной, но средства оптимизации быстродействия могут быть применены далеко не ко всем переменным в равной степени. Максимальное количество переменных register, оптимизирующихся по быстро- действию, зависит от среды программирования и конкретной реализации компилято- ра. Если таких переменных окажется слишком много, то компилятор автоматически Глава 2. Выражения 59
преобразует регистровые переменные в нерегистровые. Этим обеспечивается перено- симость программы в широком диапазоне процессоров. Обычно в регистры процессора можно поместить как минимум две переменные типа char или int. Однако в различных средах программирования режимы оптими- зации могут очень отличаться, поэтому выбор режима оптимизации необходимо осу- ществлять с учетом особенностей конкретного компилятора. В языке С с помощью оператора & (рассматривается в этой главе далее) нельзя по- лучить адрес регистровой переменной, потому что она может храниться в регистре процессора, который обычно не имеет адреса. Хотя в настоящее время применение спецификатора register в значительной ме- ре вышло за его традиционные рамки, практически ощутимый эффект от его приме- нения по-прежнему может быть получен только для переменных целого и символь- ного типа. Не следует ожидать заметного повышения скорости от объявления регист- ровыми переменных других типов. Э1 Инициализация переменных При объявлении переменной она может быть инициализирована. Для этого нужно после ее объявления поставить знак равенства и константу, т.е. общая форма инициа- лизации имеет следующий вид: тип имя_переменной = константа; Приведем несколько примеров инициализации переменных: Ichar ch = ’ а ’ ; int first = 0; double balance = 123.23; Глобальные и статические локальные переменные инициализируются только один раз в начале работы программы. А локальные переменные (исключая статические ло- кальные) инициализируются каждый раз при входе в блок, в котором они объявлены. Неинициализированные локальные переменные до первого присвоения имеют произ- вольное значение. Неинициализированные глобальные и статические локальные пе- ременные в начале работы программы автоматически обнуляются. 31 Константы Константа — это фиксированное значение, которое не может быть изменено программой. Константа может относиться к любому базовому типу. Способ представ- ления константы определяется ее типом. Константы также называются литералами. Символьные константы заключаются в одинарные кавычки. Например, 'а' и — это символьные константы. В языке С определены многобайтовые (состоящие из од- ного или более байт) и широкие (обычно длиной 16 бит) символы. Они используются для представления символов языков, имеющих в своем алфавите много букв. Много- байтовый символ записывается в одинарных кавычках, например, 'ху', а широкий — с предшествующим символом L, например: |wchar_t wc; wc = L ’ А ’ ; Здесь переменной wc присвоено значение константы А, рассматриваемой как ши- рокий символ. Тип широкого символа wchar_t определен в заголовочном файле <stddef . h>, этот тип не является встроенным. 60 Часть I. Основы языка С
Целые константы определяются как числа без дробной части. Например, 10 и - 100 — это целые константы. Константы в плавающем формате записываются как числа с десятичной точкой, например, 11.123. Допускается также экспоненциаль- ное представление чисел (в виде мантиссы и порядка): 111.23е—1. По умолчанию компилятор приписывает константе тип наименьшего размера, в ячейку которого может уместиться константа. Таким образом, если целые числа обычно являются 16-разрядными, то константа 10 по умолчанию имеет тип int, а 103000 — тип long int. Число 10 может поместиться в типе char, однако ком- пилятор не нарушит границы типов и поместит ее в int. Но это правило имеет исключение: всем константам в плавающем формате, даже самым маленьким, приписывается тип double (если, конечно, они сюда помещаются). Определение типов констант по умолчанию является вполне удовлетворитель- ным при разработке большинства программ. Однако, используя суффикс, можно явно указать тип числовой константы. Если после числа в плавающем формате стоит суффикс F, то считается, что константа имеет тип float, а если L, то long double. Для целых типов суффикс U означает unsigned, a L — long. Тип суф- фикса не зависит от регистра, например, как F, так и f определяют константы типа float. Приведем несколько примеров: Тип данных Примеры констант int 1 123 21000 -243 long int 35000L -34L unsigned int 10000U 987u 40000U float 123.23F 4.34e-4f double 123.23 1.0 -0.9876324 long double 1001.2L Стандарт С99 определяет также целые константы типа long long , их суффикс — LL или И. Шестнадцатеричные и восьмеричные константы Иногда удобнее использовать не десятичную, а восьмеричную или шестнадца- теричную систему. Позиционную систему счисления с основанием 8 называют восьмеричной. В ней используются цифры от 0 до 7. Число 10 в восьмеричной сис- теме представляет то же число, что и 8 в десятичной. Позиционная система счис- ления с основанием 16 называется шестнадцатеричной. В ней используются 16 символов: цифры от 0 до 9 и символы от А до F, обозначающие цифры от 10 до 15. Например, запись 10 в шестнадцатеричной системе обозначает то же число, что и 16 в десятичной системе. Эти системы счисления используются довольно часто, поэтому в С целые константы можно определять не только в десятичной, но и в восьмеричной и шестнадцатеричной системах счисления. Шестнадцате- ричная константа начинается с Ох, а восьмеричная — с 0, например: Iint hex = 0x80; /* 128 в десятичной системе */ int oct = 012; /* 10 в десятичной системе */ Строковые константы Язык С поддерживает еще один тип констант, а именно — строковые. Строка — это последовательность символов, заключенных в двойные кавычки. Например, "тест" — это строка. В этой книге ранее уже встречались примеры строк в функции printf (). В тер- мине “строковая константа” слово “строковая” не означает строковый предопределенный тип данных, такого в С нет, здесь это всего лишь прилагательное. Глава 2. Выражения 61
Не следует путать понятия строки и символа. Символьная константа заключается в одинарные кавычки, например, 'а'. Соответственно запись "а" означает строку, со- стоящую из одного символа. Специальные символьные константы Чтобы представить большинство символьных констант, достаточно заключить соответствующий символ в одинарные кавычки. Но некоторые символы, напри- мер, символ возврата каретки, требуют специального представления. В языке С определены специальные символьные константы, приведенные в табл. 2.2. Иногда их называют ESC-последовательностями, управляющими последовательностями и символами с обратным слэшем. Управляющие последовательности можно исполь- зовать вместо ASCII-кодов для обеспечения лучшей переносимости программы. В следующем примере программа выводит символ новой строки (т.е. переходит на новую строку), выводит символ табуляции (т.е. переходит на первую позицию табуля- ции) и, наконец, выводит строку Простой тест. #include <stdio.h> int main(void) { printf("пЪПростой тест."); return 0; } Таблица 2.2. Специальные символьные константы Код Назначение ь f п г t " V \ v а ? N xN Удаление предыдущего символа Подача бумаги Новая строка Возврат каретки Горизонтальная табуляция Двойная кавычка Одинарная кавычка Обратный слэш Вертикальная табуляция Сигнал Знак вопроса Восьмеричная константа (N — восьмеричное представление) Шестнадцатеричная константа (N — шестнадцатеричное представление) Н Операции Язык С содержит большое количество встроенных операций. Их роль в С значительно больше, чем в других языках программирования. Существует четыре основных класса опе- раций: арифметические, логические, поразрядные и операции сравнения. Кроме них, есть так- же некоторые специальные операторы, например, оператор присваивания. 62 Часть I. Основы языка С
Оператор присваивания Оператор присваивания может присутствовать в любом выражении языка С1. Этим С отличается от большинства других языков программирования (Pascal, BASIC и FORTRAN), в которых присваивание возможно только в отдельном операторе. Общая форма оператора присваивания: имя_п временно и ^выражение; Выражение может быть просто константой или сколь угодно сложным выражени- ем. В отличие от Pascal или Modula-2, в которых для присваивания используется знак в языке С оператором присваивания служит единственный знак присваивания “=”. Адресатом (получателем), т.е. левой частью оператора присваивания должен быть объект, способный получить значение, например, переменная. В книгах по С и в сообщениях компилятора часто встречаются термины lvalue1 (left side value) и rvalue*1 (right side value). Попросту говоря, lvalue — это объект. Если этот объект мо- жет стоять в левой части присваивания, то он называется также модифицируемым (modifiable) lvalue. Подытожим сказанное: lvalue — это объект в левой части оператора при- сваивания, получающий значение, чаще всего этим объектом является переменная. Тер- мин rvalue означает значение выражения в правой части оператора присваивания. Преобразование типов при присваиваниях Если в операции встречаются переменные разных типов, происходит преобразова- ние типов. В операторе присваивания действует простое правило: значение выражения в правой части преобразуется к типу объекта в левой части. int х; char ch; float f; void func(void) { ch = x; /* 1-я строка */ x = f; /* 2-я строка */ f = ch; /* 3-я строка */ f = x; /* 4-я строка */ } В 1-й строке этого примера старшие двоичные разряды целой переменной х отбра- сываются, а в ch заносятся младшие 8 бит. Если значение х лежит в интервале от 0 до 255, то ch и х будут идентичны и потери информации не произойдет. В противном случае в ch будут занесены только младшие разряды переменной х. Во 2-й строке в х будет записана целая часть числа f. В 3-й строке произойдет преобразование целого 8-разрядного числа, хранящегося в ch, в число в плавающем формате. В 4-й строке произойдет то же самое, только с 16-разрядным целым. 1 2 3 1 В данном случае под оператором имеется в виду, конечно, знак операции. По этому пово- ду см. сделанное ранее примечание редактора о переводе термина operator. — Прим. ред. 2 lvalue — именующее выражение, т.е. выражение, которое может стоять в левой части опе- ратора присваивания. Под lvalue также часто подразумевается адрес переменной. (С идентифи- катором переменной в программе связано две величины: адрес переменной и ее значение. Ад- рес используется, когда переменная стоит в левой части присваивания, значение — в правой части присваивания.) Иногда встречается и термин 1-значение. Как бы то ни было, этим терми- ном обозначается выражение, которое может находиться в левой части оператора присваивания. Семантически оно представляет собой адрес, по которому размещена переменная, массив, эле- мент структуры и т.п. — Прим. ред. 3 rvalue — значение переменной; иногда переводится, как r-значение, т.е. значение в правой части оператора присваивания. — Прим. ред. Глава 2. Выражения 63
Преобразование целых в символы и длинных целых в целые удаляет соответст- вующее количество старших двоичных разрядов. В 16-разрядной среде теряются 8 би- тов при преобразовании целого в символ и 16 битов при преобразовании длинного целого в целое. В 32-разрядной среде теряются 24 бита при преобразовании целого в символ и 16 битов при преобразовании целого в короткое целое. В табл. 2.3. приведены варианты потери информации при некоторых преобразова- ниях. Необходимо помнить, что преобразование int во float или float в double не повышает точность вычислений. При таком преобразовании только изменяется форма представления числа. Некоторые компиляторы при преобразовании char в int счи- тают переменную char положительной независимо от ее значения. Другие компиля- торы считают переменную char отрицательной, если она больше 127. Поэтому для обеспечения переносимости программы необходимо использовать переменные типа char для хранения символов, а переменные типа signed char и int (целый) — для хранения чисел. Таблица 2.3. Результат некоторых преобразований типов Тип адресата Тип выражения Потеря информации signed char char Если значение > 127, то результат отрица- тельный char short int Старшие 8 бит char int (16- разрядный) Старшие 8 бит char int (32- разрядный) Старшие 24 бита char long int Старшие 24 бита short int int (16- разрядный) Нет short int int (32- разрядный) Старшие 16 бит int (16-разрядный) long int Старшие 16 бит int (32- разрядный) long int Нет long int (32- разрядный) long long int (64-разряд- ный) Старшие 32 бита (это относится только к С99) int float Дробная часть float double Результат округляется double long double Результат округляется Если какое-либо преобразование не приведено в табл. 2.3, то, чтобы определить, что именно теряется в результате этого преобразования, нужно представить его в виде композиции (суперпозиции, произведения) указанных в таблице преобразований и затем провести последовательные преобразования. Например, преобразование double в int эквивалентно последовательному выполнению двух преобразований: сначала double в float, а затем float в int. Множественное присваивание В одном операторе присваивания можно присвоить одно и то же значение многим пе- ременным. Для этого используется оператор множественного присваивания1, например: |x = y= z = 0; Следует отметить, что в практике программирования этот прием используется очень часто. 1 Множественное присваивание — присваивание одного и того же значения нескольким пе- ременным. Под множественным присваиванием также подразумевается конструкция языка программирования, позволяющая присвоить одно и то же значение нескольким переменным одновременно. — Прим. ред. 64 Часть I. Основы языка С
Составное присваивание Составное присваивание — это разновидность оператора присваивания, в которой запись сокращается и становится более удобной в написании1. Например, оператор | х = х+10; можно записать как | х += Юг- Оператор “+=” сообщает компилятору, что к переменной х нужно прибавить 10. “Составные” операторы1 2 присваивания существуют для всех бинарных операций (то есть операций, имеющих два операнда). Любой оператор вида переменная = переменная оператор выражение; можно записать как переменная оператора выражение; Еще один пример: | х = х-100; означает то же самое, что и | х -= 100; Составное присваивание значительно компактнее, чем соответствующее простое присваивание, поэтому его иногда называют стенографическим (shorthand) присваива- нием. В программах на С этот оператор широко используется, поэтому необходимо хорошо его усвоить. Арифметические операции В табл. 2.4 приведены арифметические операции С. Операции +, —, * и / работают так же, как и в большинстве других языков программирования. Их можно применять почти ко всем встроенным типам данных. Если операция / применяется к целому или символьному типам, то остаток от деления отбрасывается. Например, результатом операции 5/2 является 2. Оператор деления по модулю % в С работает так же, как и в других языках, его ре- зультатом является остаток от целочисленного деления. Этот оператор, однако, нельзя применять к типам данных с плавающей точкой. Применение оператора % иллюстри- руется следующим примером: int х, у; х = 5; у = 2; printf("%d ”, х/у); /★ напечатает 2 ★/ printf("%d ”, х%у); /* напечатает 1, остаток от целочисленного деления */ 1 По этой причине варианты оператора присваивания, в которых используется такая запись, называются “сокращенными” или “укороченными”. Что касается терминологии, то необходимо отметить также следующее обстоятельство. Хотя термины присваивание и оператор присваивания часто могут рассматриваться как синонимы, составное присваивание не является составным оператором! (Под составным оператором в языке С подразумевают блок.) — Прим. ред. 2 Под “составными” операторами в данном случае, конечно, подразумеваются составные знаки операций, т.е. знаки операций, состоящие из нескольких (обычно двух) символов. Со- ставные операторы-блоки не имеют к этому никакого отношения. — Прим. ред. Глава 2. Выражения 65
I х = 1; I У = 2; |printf("%d %d "z x/y, x%y) ; /★ напечатает 0 1*/ Последняя строка программы напечатает 0 1 потому, что при целочисленном деле- нии остаток отбрасывается и здесь результат будет 0, а сам остаток равен 1. Таблица 2.4. Арифметические операции Оператор Операция - Вычитание, также унарный минус + * Сложение Умножение / % Деление Остаток от деления ++ Декремент1, или уменьшение Инкремент1 2, или увеличение Унарный минус умножает операнд на —1, то есть меняет его знак на противо- положный. Операции увеличения (инкремента) и уменьшения (декремента) В языке С есть два полезных оператора, значительно упрощающие широко рас- пространенные операции. Это инкремент ++ и декремент —. Оператор ++ увеличи- вает значение операнда на 1, а — уменьшает на 1. Иными словами, | х = х+1; можно записать как | ++х; Аналогично оператор | х = х-1; равносилен оператору | х—; Как инкремент, так и декремент могут предшествовать операнду (префиксная форма) или следовать за ним (постфиксная форма). Например | х = х+1; можно записать как в виде | ++х; так и в виде | х++; Однако префиксная и постфиксная формы отличаются при использовании их в выражениях. Если оператор инкремента или декремента предшествует операнду, то сама операция выполняется до использования результата в выражении. Если же опе- ратор следует за операндом, то в выражении значение операнда используется до вы- 1 На жаргоне программистов: декрементация. — Прим. ред. 2 На жаргоне программистов: инкрементация. — Прим. ред. 66 Часть I. Основы языка С
полнения операции инкремента или декремента. То есть для выражения эта операция как бы не существует, она выполняется только для операнда. Например, |х = 10; у = 4-4-х; присваивает у значение 11. Однако если написать |х = 10; у = Х4-4-; то переменной у будет присвоено значение 10. В обоих случаях х присвоено значение 11, разница только в том, когда именно это случилось, до или после присваивания значения переменной у. Большинство компиляторов С генерируют для инкремента и декремента очень бы- стрый, эффективный объектный код, значительно лучший, чем для соответствующих операторов присваивания. Поэтому везде, где это возможно, рекомендуется использо- вать инкремент и декремент. Приоритет выполнения арифметических операторов следующий: Наивысший 4-4- — - (унарный минус) * / % Наиниэшии 4- - Операции с одинаковым приоритетом выполняются слева направо. Используя круглые скобки, можно изменить порядок вычислений. В языке С круглые скобки интерпретиру- ются компилятором так же, как и в любом другом языке программирования: они как бы придают операции (или последовательности операций) наивысший приоритет. Операции сравнения и логические операции Операции сравнения — это операции, в которых значения двух переменных срав- ниваются друг с другом. Логические же операции реализуют средствами языка С опе- рации формальной логики. Между логическими операциями и операциями сравнения существует тесная связь: результаты операций сравнения часто являются операндами логических операций. В операциях сравнения и логических операциях в качестве операндов и результа- тов операций используются значения ИСТИНА (true) и ЛОЖЬ (false). В языке С зна- чение ИСТИНА представляется любым числом, отличным от нуля. Значение ЛОЖЬ представляется нулем. Результатом операции сравнения или логической операции яв- ляются ИСТИНА (true, 1) или ЛОЖЬ (false, 0). На заметку Как в С89, так и в С99 значение ИСТИНА представлено любым отличным от нуля числом, а ЛОЖЬ— нулем. В стандарте С99 дополнительно определен тип данных _воо1, переменные которого могут принимать значение только 0 или 1. Подробнее см. часть II. В табл. 2.5 приведен полный список операций сравнения и логических операций. Таблица истинности логических операций имеет следующий вид: р я p&&q Р II я !р 0 0 0 0 1 0 1 0 1 1 1 1 1 1 0 1 0 0 1 0 Глава 2. Выражения 67
Как операции сравнения, так и логические операции имеют низший приоритет по сравнению с арифметическими. То есть, выражение 10>1+12 интерпретируется как 10>(1 + 12). Результат, конечно, равен ЛОЖЬ. В одном выражении можно использовать несколько операций: 10>5 && !(10<9) Н 3<4 В этом случае результатом будет ИСТИНА. В языке С не определена операция “исключающего ИЛИ” (exclusive OR, или XOR). Однако с помощью логических операторов несложно написать функцию, выполняющую эту операцию. Результатом операции “исключающее ИЛИ” явля- ется ИСТИНА, если и только если один из операндов (но не оба) имеют значение ИСТИНА. В следующем примере функция хог() возвращает результат операции “исключающее ИЛИ”, а операндами служат аргументы функции: #include <stdio.h> int xor(int a, int b); int main(void) { printf("%d", xor(l, 0) ) ; printf("%d", xor(l, 1)); printf("%d", xor(0, 1)); printf("%d", xor(0, 0)); return 0; } /* Выполнение логической операции исключающее ИЛИ над двумя аргументами */ int xor(int a, int b) { return (a || b) && !(a && b) ; } Таблица 2.5. Операции сравнения и логические операции Операторы сравнения Оператор Операция > Больше чем >= Больше или равно < Меньше чем <= Меньше или равно == Равно != Не равно Логические операторы Оператор Операция && И II ИЛИ НЕ, отрицание 68 Часть I. Основы языка С
Ниже приведен приоритет логических операций: Наивысший 1 == । = && Наинивший | | Как и в арифметических выражениях, для изменения порядка выполнения опера- ций сравнения и логических операций можно использовать круглые скобки. Напри- мер, выражение: | 10 && 0 || О равно ЛОЖЬ. Однако, если добавить скобки как показано ниже, то результатом будет ИСТИНА: | 1 (0 && 0) 110 Необходимо помнить, что результатом любой операции сравнения или логической операции есть 0 или 1. Поэтому следующий фрагмент программы является правиль- ным и в результате его выполнения будет напечатано 1. Iint х; х = 100; printf("%d", х>10); Поразрядные операции В отличие от многих других языков программирования, в С определен полный на- бор поразрядных операций1. Это обусловлено тем, что С был задуман как язык, при- званный во многих приложениях заменить ассемблер, который способен оперировать битами данных. Поразрядные операции — это тестирование (проверка), сдвиг или при- своение значений отдельным битам данных. Эти операции осуществляются над ячей- ками памяти, содержащими данные типа char или int. Данные типа float, double, long double, void или другие более сложные не могут участвовать в поразрядных операциях. В табл. 2.6 приведен полный список знаков поразрядных операций, вы- полняемых над отдельными разрядами (битами) операндов. Таблицы истинности логических операций и поразрядных операций И, ИЛИ, НЕ совпадают. Отличие лишь в том, что поразрядные операции выполняются над отдель- ными разрядами (битами) операндов. Операция “исключающее ИЛИ” имеет следую- щую таблицу истинности: Р________________________Q________________________PJ_____________________ 0 0 о 1 0 1 1 1 о 0 1 1 Как показано в таблице, результат операции “исключающее ИЛИ” равен ИСТИНА ес- ли и только если один из операндов равен 1, иначе результат будет равен ЛОЖЬ. Наиболее часто поразрядные операции применяются при программировании драй- веров устройств, таких как модемы, а также процедур, выполняющих операции над файлами, и стандартных программ обслуживания принтера. В них поразрядные опе- рации используются для маскирования определенных битов, например, бита контроля 1 Называются также битовыми, побитовыми и логическими операциями. — Прим. ред. Глава 2. Выражения 69
четности1. (Этот бит служит для проверки правильности остальных битов в байте. Чаще всего это бит старшего разряда в каждом байте.) Таблица 2.6. Поразрядные операции Оператор Операция & И 1 или Л исключающее ИЛИ — НЕ (отрицание, дополнение к 1) » Сдвиг вправо « Сдвиг влево Операция И может быть использована для очищения бита1. Иными словами, для гашения бита используется следующее свойство операции И: если бит одного из опе- рандов равен 0, то соответствующий бит результата будет равен 0 независимо от со- стояния этого бита во втором операнде. Например, следующая функция читает сим- вол из порта модема и обнуляет бит контроля четности: char get_char_from_modem (void) { char ch; ch = read_modem(); /* чтение символа из порта модема */ return (ch & 127); } Бит контроля четности, находящийся в 8-м разряде байта, обнуляется с помощью операции И. При этом в качестве второго операнда выбирается число, имеющее 1 в разрядах от 1 до 7, и 0 в 8-м разряде. Именно таким числом и является 127, поскольку все биты двоичного представления числа 127, кроме старшего, равны 1. В силу ука- занного свойства операции И операция ch & 127 оставляет все биты, кроме старше- го, без изменения, а старший обнуляет: Бит контроля четности г 11000001 переменная ch содержит символ 'А' с битом четности 01111111 двоичное представление числа 127 & --------- поразрядная операция И 01000001 символ 'А' с обнуленным битом контроля четности 1 Бит контроля четности называется также контрольным двоичным разрядом четности, контрольным разрядом четности, проверочным двоичным разрядом четности, проверочным разрядом четности, битом четности, разрядом четности, контрольным битом и битом контроля на четность. Это дополнительный бит, который добавляется к группе (обычно из семи) битов. Передающее устройство устанавливает значение бита четности равным нулю или единице так, чтобы сумма битов в каждом байте всегда была четной или нечетной в зависимости от выбора типа проверки — на четность или нечетность. Невыполнение условия такой проверки на при- емном конце линии означает искажение по крайней мере одного бита при передаче. При обна- ружении ошибки принимающее устройство делает запрос на повтор данных. Иными словами, это бит, добавляемый к данным для контроля их верности таким образом, чтобы сумма двоич- ных единиц, составляющих данное, включая единицу контрольного бита, всегда была четной (либо всегда нечетной). — Прим. ред. 1 Очищение бита — гашение, т.е. занесение нуля. — Прим. ред. 70 Часть I. Основы языка С
Поразрядная операция ИЛИ, являющаяся двойственной операции И, применяется для установки необходимых битов в 1. В следующем примере выполняется операция 128 | 3: 10000000 двоичное представление числа 128 00000011 двоичное представление числа 3 | --------- поразрядная операция ИЛИ 10000011 результат Операция исключающего ИЛИ (XOR) устанавливает бит результата в 1, если соот- ветствующие биты операндов различны. В следующем примере выполняется операция 127А120: 01111111 01111000 двоичное представление числа 127 двоичное представление числа 120 поразрядная операция XOR результат 00000111 Необходимо помнить, что результат логической операции всегда равен 0 или 1. В то же время результатом поразрядной операции может быть любое значение, которое, как видно из предыдущих примеров, не обязательно равно 0 или 1. Поразрядные операторы сдвига » и « сдвигают все биты переменной вправо или влево. Общая форма оператора сдвига вправо: переменная » количество_разрядов Общая форма оператора сдвига влево: переменная « количество_разрядов Во время сдвига битов в один конец числа, другой конец заполняется нулями. Но если число типа signed int отрицательно, то при сдвиге вправо левый конец запол- няется единицами, так что знак числа сохраняется. Необходимо отметить различие между сдвигом и циклическим сдвигом. При циклическом сдвиге биты, сдвигаемые за пределы операнда, появляются на другом конце операнда. А при сдвиге вышедшие за границу биты теряются. Поразрядные операции сдвига очень полезны при декодировании выходов внеш- них устройств, например таких, как цифро-аналоговые преобразователи, а также при считывании информации о статусе устройств. Побитовые операторы сдвига могут бы- стро умножать и делить целые числа. Как показано в табл. 2.7, сдвиг на один бит вправо делит число на 2, а на один бит влево — умножает на 2. Следующая программа иллюстрирует применение операторов сдвига: /* Пример применения операторов сдвига */ #include <stdio.h> int main(void) { unsigned int i; int j; i = 1; /* сдвиг влево */ for(j=0; j<4; j++) { i = i << 1; /* сдвиг i влево на 1 разряд, что равносильно умножению на 2 */ printf (’’Сдвиг влево на %d разр. : %dn”, j, i) ; } /* сдвиг вправо */ Глава 2. Выражения 71
for(j=0; j<4; j++) { i = i » 1; /* сдвиг i вправо на 1 разряд, что равносильно делению на 2 */ printf("Сдвиг вправо на %d разр.: %dn", j, i) ; } return 0; } Поразрядная операция отрицания (дополнения) ~ инвертирует состояние каждого бита операнда. То есть, 0 преобразует в 1, а 1 — в 0. Поразрядные операции часто используются в процедурах кодирования. Проде- лав с дисковым файлом некоторые поразрядные операции, его можно сделать не- читаемым. Простейший способ сделать это — применить операцию отрицания к каждому биту: Исходный байт 0010100 После 1-го отрицания 1101011 После 2-го отрицания 0010100 Обратите внимание, при последовательном применении 2-х отрицаний результа- том всегда будет исходное число. Таким образом, 1-е отрицание кодирует состояние байта, а 2-е — декодирует. Таблица 2.7. Умножение и деление операторами сдвига unsigned char х х после операции значениех х = 7; 00000111 1 х = х«1; 00001110 14 х = х«3; 01110000 112 х = х«2; 11000000 192 х = х»1; 01100000 96 х = х»2; 00011000 24 Каждый сдвиг влево умножает на 2. Потеря информации произошла после операции х«2 в ре- зультате сдвига за левую границу. Каждый сдвиг вправо делит на 2. Сдвиг вправо потерянную информацию не восстановил. В следующем примере оператор отрицания используется в функции шифрования символа: I /* Простейшая процедура шифрования */ I char encode(char ch) I { I return(-ch); /* операция отрицания */ I I Конечно, взломать такой шифр не представляет труда. Операция ? В языке С определен мощный и удобный оператор, который часто можно исполь- зовать вместо оператора вида if-then-else. Речь идет о тернарном операторе ?, общий вид которого следующий: Выражение! ? Выражение2: ВыражениеЗ; 72 Часть I. Основы языка С
Обратите внимание на использование двоеточия. Оператор ? работает следующим образом: сначала вычисляется Выражение!, если оно истинно, то вычисляется Выра- жение2 и его значение присваивается всему выражению; если Выражение! ложно, то вычисляется ВыражениеЗ и всему выражению присваивается его значение. В примере |х = 10; у = х>9 ? 100 : 200; переменной у будет присвоено значение 100. Если бы х было меньше 9, то перемен- ной у было бы присвоено значение 200. Эту же процедуру можно написать, используя оператор if-else: |х = 10; if(x>9) у = 100; else у = 200; Более подробно оператор ? обсуждается в главе 3 в связи с условными операторами. Операции получения адреса (&) и раскрытия ссылки (*) Указатель — это адрес объекта в памяти. Переменная типа “указатель” (или просто переменная-указатель) — это специально объявленная переменная, в которой хранится указатель на переменную определенного типа. В языке С указатели служат мощней- шим средством создания программ и широко используются для самых разных целей. Например, с их помощью можно быстро обратиться к элементам массива или дать функции возможность модифицировать свои аргументы. Указатели широко исполь- зуются для связи элементов в списках, в двоичных деревьях и в других динамических структурах данных. Глава 5 полностью посвящена указателям. В данной главе коротко рассматриваются два оператора, использующиеся для работы с указателями. Первый из них — оператор &, это унарный оператор, возвращающий адрес опе- ранда в памяти1. (Унарной операцией называется операция, имеющая только один операнд.) Например, оператор | m = &count; записывает в переменную m адрес переменной count. Этот адрес представляет собой адрес ячейки памяти компьютера, в которой размещена переменная. Адрес и значе- ние переменной — совершенно разные понятия. Выражение “&переменная” означает “адрес переменной”. Следовательно, инструкция m = &count; означает: “Переменной m присвоить адрес, по которому расположена переменная count; ”. Допустим, переменная count расположена в памяти в ячейке с адресом 2000, а ее значение равно 100. Тогда в предыдущем примере переменной m будет при- своено значение 2000. Второй рассматриваемый оператор * является двойственным (дополняющим) по отношению к &1 2. Оператор * является унарным оператором, он возвращает значение объекта, расположенного по указанному адресу. Операндом для * слу- жит адрес объекта (переменной). Например, если переменная m содержит адрес переменной count, то оператор | q = *m; записывает значение переменной count в переменную q. В нашем примере переменная q получит значение 100, потому что по адресу 2000 записано число 100, причем этот адрес 1 Оператор & называется также оператором получения (взятия) адреса. — Прим. ред. 2 Оператор * называется также оператором косвенности, оператором раскрытия ссылки и опе- ратором разыменования адреса. — Прим. ред. Глава 2. Выражения 73
записан в переменной т. Выражение “* адрес” означает “по адресу”. Наш фрагмент про- граммы можно прочесть как “q получает значение, расположенное по адресу т”. К сожалению, символ операции раскрытия ссылки совпадает с символом операции умножения, а символ операции получения адреса — с символом операции поразряд- ного И. Необходимо помнить, что эти операторы не имеют никакого отношения друг к другу. Операторы * и & имеют более высокий приоритет, чем любая арифметиче- ская операция, кроме унарного минуса, имеющего такой же приоритет. Если переменная является указателем, то в объявлении перед ее именем нужно поста- вить символ *, он сообщит компилятору о том, что это указатель на переменную данного типа. Например, объявление указателя на переменную типа char записывается так: | char *ch; Необходимо понимать, что ch — это не переменная типа char, а указатель на пе- ременную данного типа, это совершенно разные вещи. Тип данных, на который ука- зывает указатель (в данном случае это char), называется базовым типом указателя1. Сам указатель является переменной, содержащей адрес объекта базового типа. Ком- пилятор учтет размер указателя в архитектуре компьютера и выделит для него необхо- димое количество байтов, чтобы в указатель поместился адрес. Базовый тип указателя определяет тип объекта, хранящегося по этому адресу. В одном операторе объявления можно одновременно объявить и указатель, и пе- ременную, не являющуюся указателем. Например, оператор | int х, *у, count; объявляет х и count как переменные целого типа, а у — как указатель на перемен- ную целого типа. В следующей программе операторы * и & используются для записи значения 10 в переменную target. Программа выведет значение 10 на экран. ttinclude <stdio.h> int main(void) { int target, source; int *m; source = 10; m = &source; target = *m; printf("%d", target); return 0; Операция определения размера sizeof Унарная операция sizeof, выполняемая во время компиляции программы, позволяет определить длину операнда в байтах. Например, если компилятор для чисел типа int от- водит 4 байта, а для чисел типа double — 8, то следующая программа напечатает 8 4. | double f; I printf("%d ”, sizeof f) ; I printf(”%d ”, sizeof(int)); 1 Иногда называется также основным или исходным типом. — Прим. ред. 74 Часть I. Основы языка С
Необходимо помнить, что для вычисления размера типа переменной имя типа должно быть заключено в круглые скобки. Имя переменной заключать в скобки не обязательно, но ошибки в этом не будет. В языке С определяется (с помощью спецификатора класса памяти typedef) спе- циальный тип size t, приблизительно соответствующий целому числу без знака. Ре- зультат операции sizeof имеет тип size t. Но практически его можно использовать везде, где допустимо использование целого числа без знака. Оператор sizeof очень полезен для улучшения переносимости программ, так как пе- реносимость существенно зависит от размеров встроенных типов данных. Для примера рассмотрим программу, работающую с базой данных, в которой необходимо хранить шесть целых чисел в одной записи. Если эта программа предназначена для работы на многих компьютерах, ни в коем случае нельзя полагаться на то, что размер целого числа на всех компьютерах будет один и тот же. В программе следует определять размер целого, исполь- зуя оператор sizeof. Соответствующая программа имеет следующий вид: /* Запись шести целых чисел в дисковый файл */ void piit_rec(int rec [6], FILE *fp) { int len; len = fwrite(rec, sizeof(int)*6, 1, fp); if(len != 1) printf("Ошибка при записи"); } Приведенная функция put rec () компилируется и выполняется правильно в лю- бой среде, в том числе на 16- и 32-разрядных компьютерах. И в заключение: оператор sizeof выполняется во время трансляции, его результат в программе рассматривается как константа. Оператор последовательного вычисления: оператор “запятая” Оператор “запятая’* связывает воедино несколько выражений. При вычислении левой части оператора “запятая” всегда подразумевается, что она имеет тип void. Это значит, что выражение, стоящее справа после оператора “запятая”, является значени- ем всего разделенного запятыми выражения. Например, оператор | х = (у=3, у+1); сначала присваивает у значение 3, а затем присваивает х значение 4. Скобки здесь обязательны, потому что приоритет оператора “запятая” меньший, чем оператора присваивания. В операторе “запятая” выполняется последовательность операций. Если этот опе- ратор стоит в правой части оператора присваивания, то его результатом всегда являет- ся выражение, стоящее последним в списке. Оператор доступа к члену структуры (оператор . (точка)) и оператор доступа через указатель -> (оператор стрелка) В языке С операторы . (точка) и -> (стрелка) обеспечивают доступ к элементам структур и объединений. Структуры и объединения — это составные типы данных, в которых под одним именем хранятся многие объекты. (Структуры и объединения подробно рассматриваются в главе 7.) 1 Чаще встречается написание без кавычек: оператор запятая. Мы пишем кавычки лишь для того, чтобы новичкам было легче воспринимать несколько непривычное для них название опе- ратора. — Прим. ред. Глава 2. Выражения 75
Оператор точка используется для прямой ссылки на элемент структуры или объе- динения, т.е. перед точкой стоит имя структуры, а после — имя элемента структуры. Оператор стрелка используется с указателем на структуру или объединение, т.е. перед стрелкой стоит указатель на структуру. Например, во фрагменте программы struct employee { char name[80] ; int age; float wage; } emp; struct employee *p = &emp; /* адрес emp заносится в p */ для присвоения члену wage значения 123.33 необходимо записать | emp.wage = 123.33; То же самое можно сделать, использовав указатель на структуру: | p->wage = 123.33; Операторы [ ] и () Круглые скобки являются оператором, повышающим приоритет выполнения опе- раций, которые в них заключены. Квадратные скобки служат для индексации массива (массивы подробно рассматриваются в главе 4). Если в программе определен массив, то выражение в квадратных скобках представляет собой индекс массива. Например, в программе #include <stdio.h> char s [ 8 0 ] ; int main(void) { s[3] = ’X’; printf("%c", s[3]); return 0; } значение ’X’ сначала присваивается четвертому элементу массива (в С элементы мас- сива нумеруются с нуля), затем этот элемент выводится на экран. Сводка приоритетов операций В табл. 2.8 приведены приоритеты всех операций, определенных в С. Необходимо помнить, что все операторы, кроме унарных и “?”, связывают (присоединяют, ассо- циируют) свои операнды слева направо. Унарные операторы (♦, &, -) и “?” связывают (присоединяют, ассоциируют) свои операнды справа налево. Таблица 2.8. Приоритеты операций в языке С Наивысший_____________________________()[]->._____________________________ ! * ++ — (type) * & sizeof * / % + - « » <<=>>= 76 Часть I. Основы языка С
Окончание табл. 2.8 == != & А && II ?: = += -= *= /= и Т.Д. Наинизший S Выражения Выражения состоят из операторов, констант, функций и переменных. В языке С выражением является любая правильная последовательность этих элементов. Боль- шинство выражений в языке С по форме очень похожи на алгебраические, часто их и пишут, руководствуясь правилами алгебры. Однако здесь необходимо быть внима- тельным и учитывать специфику выражений в языке С. Порядок вычислений Порядок вычисления подвыражений в выражениях языка С не определен. Компилятор может самостоятельно перестроить выражение с целью создания оптимального объектного кода. Это значит, что программист не может полагаться на определенную последователь- ность вычисления подвыражений. Например, при вычислении выражения | х = fl () + f2() ; нет никаких гарантий того, что функция f 1 () будет вызвана перед вызовом f 2 (). Преобразование типов в выражениях Если в выражении встречаются переменные и константы разных типов, они преобра- зуются к одному типу. Компилятор преобразует “меньший” тип в “больший”. Этот про- цесс называется продвижением типов (type promotion). Сначала все переменные типов char и short int автоматически продвигаются в int. Это называется целочисленным расшире- нием. (В С99 целочисленное расширение может также завершиться преобразованием в un- signed int.) После этого все остальные операции выполняются одна за другой, как опи- сано в приведенном ниже алгоритме преобразования типов: IF операнд имеет тип long double THEN второй операнд преобразуется в long double ELSE IF операнд имеет тип double THEN второй операнд преобразуется в double ELSE IF операнд имеет тип float THEN второй операнд преобразуется в float ELSE IF операнд имеет тип unsigned long THEN второй операнд преобразуется в unsigned long ELSE IF операнд имеет тип long THEN второй операнд преобразуется в long ELSE IF операнд имеет тип unsigned int THEN второй операнд преобразуется в unsigned int Глава 2. Выражения 77
Для тех, кто еще не знаком с общей формой оператора IF, приводим более руси- фицированную запись алгоритма1: ЕСЛИ операнд имеет тип long double ТО второй операнд преобразуется в long double ИНАЧЕ ЕСЛИ операнд имеет тип double ТО второй операнд преобразуется в double ИНАЧЕ ЕСЛИ операнд имеет тип float ТО второй операнд преобразуется в float ИНАЧЕ ЕСЛИ операнд имеет тип unsigned long ТО второй операнд преобразуется в unsigned long ИНАЧЕ ЕСЛИ операнд имеет тип long ТО второй операнд преобразуется в long ИНАЧЕ ЕСЛИ операнд имеет тип unsigned int ТО второй операнд преобразуется в unsigned int Кроме того, действует следующее правило: если один из операндов имеет тип long, а второй — unsigned int, притом значение unsigned int не может быть представлено типом long, то оба операнда преобразуются в unsigned long. На заметку Описание правил целочисленного расширения в С99 см. в части II. После выполнения приведенных выше преобразований оба операнда относятся к одному и тому же типу, к этому типу относится и результат операции. Рассмотрим пример преобразования типов, приведенный на рис. 2.2. Сначала символ ch преобразуется в целое число. Результат операции ch/i преобразуется в double, потому что результат f*d имеет тип double. Результат операции f+i имеет тип float, потому что f имеет тип float. Окончательный результат имеет тип double. char ch; int i; Float f; double d; result® (ch/i) + (f* d) - (f + i); int double float ~ I int double float double Puc. 2.2. Пример преобразования типов Явное преобразование типов: операция приведения типов Программист может “принудительно” преобразовать значение выражения к нуж- ному ему типу, используя операцию приведения типов. Общая форма оператора явного приведения типа: 1 Добавлено редактором русского перевода. — Прим. ред. 78 Часть I. Основы языка С
(тип) выражение Здесь тип — это любой поддерживаемый тип данных. Например, следующая за- пись преобразует значение выражения х/2 к типу float: | (float) х/2 Явное преобразование типа — это операция. Оператор приведения типа является унарным и имеет тот же приоритет, что и остальные унарные операторы. Иногда приведение типов может быть весьма полезным. Допустим, целую пере- менную нужно использовать как параметр цикла, притом в вычислении участвует и дробная часть числа. В следующем примере показано, как с помощью приведения можно сохранить точность: tfinclude <stdio.h> int main(void) /* печать i и i/2 с дробной частью */ { int i; for(i=l; i<=100; ++i) printf("%d / 2 равно: %fn", i, (float) i /2); return 0; } Без операции приведения (float) выполнялось бы целочисленное деление. Дробная часть результата выводится благодаря приведению типа переменной i. Пробелы и круглые скобки Для повышения удобочитаемости программы при записи выражений можно ис- пользовать пробелы и символы табуляции. Например, следующие два оператора экви- валентны: |х=10/у~(127/х)/ х = 10 / у ~(127/х) ; Лишние скобки, если они не изменяют приоритет операций, не приводят к ошиб- ке и не замедляют вычисление выражения. Дополнительные скобки часто используют для прояснения порядка вычислений. В следующем примере 2-я строка читается зна- чительно легче: |х = у/3-34*temp+127; х= (у/3) - (34*temp) + 127; Глава 2. Выражения 79
Полный справочник по ГлаваЗ Операторы
Оператор — это часть программы, которая может быть выполнена отдельно1. Это означает, что оператор определяет некоторое действие. В языке С существуют следующие группы операторов: Условные операторы Операторы цикла Операторы безусловного перехода Метки Операторы-выражения Блоки К условным относятся операторы if и switch. Иногда их также называют опера- торами условного перехода. Операторы цикла — это while, for и do-while. К опера- торам безусловного перехода относятся break, continue, goto и return. К меткам относятся операторы case, default (рассматриваются в разделе “Оператор выбора — switch”) и собственно метки (рассматриваются в разделе “Оператор goto”). Операто- ры-выражения — это операторы, состоящие из допустимых выражений. Блок пред- ставляет собой фрагмент текста программы, обрамленный фигурными скобками {}. Блок иногда называют составным оператором. Так как во многих операторах применяются условные выражения, их рассмотрение начнем с краткой характеристики значений ИСТИНА и ЛОЖЬ. В Логические значения ИСТИНА (True) и ЛОЖЬ (False) в языке С При выполнении многих операторов языка С вычисляются значения условных вы- ражений и в зависимости от полученного значения выбирается та или иная ветвь вы- числительного процесса. Условное выражение может принимать одно из двух значе- ний: ИСТИНА или ЛОЖЬ. В языке С значение ИСТИНА представлено любым нену- левым значением, включая отрицательные числа. Значение ЛОЖЬ всегда представлено нулем. Такое представление логических значений ИСТИНА и ЛОЖЬ позволяет весьма эффективно программировать многие процедуры. О Условные операторы В языке С существуют два условных оператора: if и switch. При определенных обстоятельствах оператор ? является альтернативой оператора if. Оператор if Общая форма оператора if следующая: if (выражение) оператор; else оператор; Здесь оператор может быть только одним оператором, блоком операторов или от- сутствовать (пустой оператор). Фраза else может вообще отсутствовать. 1 Операторы в указанном смысле в языке С называются также инструкциями, а иногда и коман- дами. В других языках операторы могут называться также предложениями (КОБОЛ). — Прим. ред. 82 Часть I. Основы языка С
Если выражение истинно1 (т.е. принимает любое значение, отличное от нуля), то выполняется оператор или блок операторов, следующий за if. В противном случае выполняется оператор (или блок операторов), следующий за else (если эта фраза присутствует). Необходимо помнить, что выполняется или оператор, связанный с if, или с else, но оба — никогда! Условное выражение, входящее в if, должно иметь скалярный результат. Это зна- чит, что результатом должно быть целое число, символ, указатель или число с пла- вающей точкой, но им не может быть массив или структура. (В Стандарте С99 тип _Воо1 также является скалярным, поэтому значение этого типа может использоваться в условии оператора if.) В выражении-условии оператора if результат плавающего типа используется редко, потому что это существенно замедляет вычислительный процесс. Объясняется это тем, что для выполнения операций над плавающими операн- дами1 2 необходимо выполнить больше команд процессора, чем для выполнения опера- ций над целыми числами или символами. В следующей программе иллюстрируется использование оператора if. В ней запро- граммирована очень простая игра “угадай магическое число”. Если играющий угадал чис- ло, на экран выводится сообщение **Верно**. Программа генерирует “магическое число” с помощью стандартного генератора случайных чисел rand (). Генератор возвращает слу- чайное число в диапазоне между 0 и rand_max (обычно это число не меньше 32767). Функция rand () объявлена в заголовочном файле stdlib. h. /* Магическое число, программа N1 */ #include <stdio.h> ttinclude <stdlib.h> int main(void) { int magic; /* магическое число */ int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf (’’Угадай магическое число: ”) ; scanf(’’%d”, &guess); if(guess == magic) printf("**Верно**"); return 0; } В следующей версии программы для игры в “магическое число” иллюстрируется использование оператора else. В этой версии выводится дополнительное сообщение в случае ложного ответа. /* Магическое число, программа N2 */ #include <stdio.h> #include <stdlib.h> int main(void) { int magic; /* магическое число */ 1 Иногда в этом случае говорят, что выражение принимает значение ИСТИНА, которое в языке С может быть представлено, как мы помним, любым целым числом, отличным от ну- ля. — Прим. ред. 2 Плавающие операнды называются также операндами в формате с плавающей точкой, вещест- венными операндами. — Прим. ред. Глава 3. Операторы 83
int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: ”); scanf("%d", &guess); if(guess == magic) printf("**Верно** "); else printf("**Неверно** ”); return 0; H Вложенные условные операторы if Оператор if является вложенным, если он вложен, т.е. находится внутри другого оператора if или else. В практике программирования вложенные условные операто- ры используются довольно часто. Во вложенном условном операторе фраза else все- гда ассоциирована с ближайшим if в том же блоке, если этот if не ассоциирован с другой фразой else. Например: if (i) { if (j) dosomethingl () ; if(k) dosomething2(); /* этот if */ else dosomething3(); /* ассоциирован с этим else */ } else dosomething4(); /* ассоциирован c if(i) */ Последняя фраза else не ассоциирована c if (j) потому, что она находится в другом блоке. Эта фраза else ассоциирована с if (i). Внутренняя фраза else ассо- циирована с if (к), потому что этот if — ближайший. Стандарт С89 допускает 15 уровней вложенности условных операторов, С99 — 127 уровней. В настоящее время большинство компиляторов допускают значительно боль- шее количество уровней вложенности. Однако на практике необходимость в глубине вложенности, большей, чем несколько уровней, возникает довольно редко, так как уве- личение глубины вложенности быстро запутывает программу и делает ее нечитаемой. В следующем примере вложенный оператор if используется в модернизированной программе для игры в магическое число. С его помощью играющий получает сообще- ние о характере ошибки: /* Магическое число, программа N3 */ #include <stdio.h> #include <stdlib.h> int main(void) { int magic; /* магическое число */ int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: ”); scanf(”%d”, &guess); if(guess == magic){ 84 Часть I. Основы языка С
printf("**Верно**"); printf("магическое число равно %dn", magic); } else { printf("**Неверно, ") ; if(guess > magic) printf("слишком большое.n"); /* вложенный if */ else printf("слишком малое.n"); return 0; } В программах часто используется конструкция, которую называют лестницей if-else-if1. Общая форма лестницы имеет вид if (выражение) оператор; else if (выражение) оператор; else if (выражение) оператор; else оператор; Работает эта конструкция следующим образом. Условные выражения операторов if вычисляются сверху вниз. После выполнения некоторого условия, т.е. когда встре- тится выражение, принимающее значение ИСТИНА, выполняется ассоциированный с этим выражением оператор, а оставшаяся часть лестницы пропускается. Если все условия ложны, то выполняется оператор в последней фразе else, а если последняя фраза else отсутствует, то в этом случае не выполняется ни один оператор. Недостаток предыдущей записи лестницы состоит в том, что с ростом глубины вложенности увеличивается количество отступов в строке. Это становится неудобным с технической точки зрения. Поэтому лестницу if-else-if обычно записывают так: if(выражение) оператор; else if(выражение) оператор; else if(выражение) оператор; else оператор; Используя лестницу if-else-if, программу для игры в “магическое число” мож- но записать так: 1 Называется также структурой выбора или конструкцией условного перехода. — Прим. ред. Глава 3. Операторы 85
/★ Магическое число, программа N4 */ #include <stdio.h> ^include <stdlib.h> int main(void) { int magic; /★ магическое число */ int guess; /★ попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: "); scant("%d", &guess); if(guess == magic){ printf("**Верно** "); printf("Магическое число равно %dn", magic); } else if(guess > magic) printf("Неверно, слишком большое"); else printf("Неверно, слишком малое"); return 0; Оператор альтернативный условному Оператор ? можно использовать вместо оператора if-else, записанного в форме И(условие) переменная = выражение; else переменная = выражение; Оператор ? является тернарным, потому что он имеет три операнда. Его общая форма следующая: Выражение! ? Выражение2 : ВыражениеЗ; Обратите внимание на использование и расположение двоеточия. Результат операции ? определяется следующим образом. Сначала вычисляется Вы- ражение!. Если оно имеет значение ИСТИНА, вычисляется ВыражениеЗ и его значение становится результатом операции ?. Если Выражение! имеет значение ЛОЖЬ, вычисля- ется ВыражениеЗ и его значение становится результатом операции ?. Например: |х = 10; у = х>9 ? 100 : 200; В этом примере переменной у присваивается значение 100. Если бы х было мень- ше 9, то переменная у получила бы значение 200. То же самое можно записать, ис- пользуя оператор if-else: |х = 10; if (х<9) у = 100; else у = 200; В следующем примере оператор ? используется для присвоения квадрату числа знака числа. (Само число вводится пользователем.) В этой программе при возведении в квадрат фактически сохраняется знак числа. Например, если пользователь введет 10, это число будет возведено в квадрат и в результате программа напечатает 100, а если 86 Часть I. Основы языка С
пользователь введет число -10, то оно будет возведено в квадрат и результату будет приписан знак числа; в этом случае будет напечатано -100. #include <stdio.h> int main(void) { int isqrd, i; printf("Введите число: "); scanf("%d", &i); isqrd = i > 0 ? i*i : -(i*i); printf(" %d в квадрате равно %d ", i, isqrd); return 0; } (Обратите внимание, что в результате выполнения данной программы могут быть напечатаны не только верные утверждения. Не всегда компьютеры печатают только правильные результаты, если даже они работают без сбоев! — Прим, ред.) Оператор ? можно использовать вместо if-else не только в операторе присваи- вания. Как известно, все функции (за исключением имеющих тип результата void) возвращают значение. Следовательно, в операторе ? можно использовать вызовы функций. Когда в выражении встречается вызов функции, она выполняется, а воз- вращаемое ею значение используется при вычислении выражения. Это значит, что можно выполнить одну или несколько функций путем размещения их вызовов в вы- ражениях оператора ? в качестве операндов. Например: #include <stdio.h> int fl(int n); int f2(void); int main(void) { int t ; printf("Введите число: "); scanf("%d", &t); /* печать соответствующего сообщения */ t ? fl(t) + f2() : printf("Введен нуль."); printf("n"); return 0; } int fl(int n) { printf("%d ", n); return 0; } int f2(void) { printf(" введено "); return 0; } Глава 3. Операторы 87
Эта программа сначала запрашивает число. При вводе нуля вызывается функция printf (), выводящая на экран сообщение введен нуль. При вводе отличного от ну- ля числа выполняются как f 1 (), так и f 2 (). Обратите внимание на то, что значение выражения ? в этом примере не присваивается никакой переменной, оно просто от- брасывается. Следует помнить, что компилятор, пытаясь оптимизировать объектный код, может установить любой порядок вычисления значений операндов. В данном примере это значит, что функции f 1 () и f 2 () выполняются в произвольном порядке и сообщение введено может появиться как до, так и после числа. Используя оператор ?, программу для игры в “магическое число” можно перепи- сать следующим образом: /* Магическое число, программа N5 */ #include <stdio.h> #include <stdlib.h> int main(void) { int magic; /* магическое число */ int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: "); scanf("%d", &guess); if(guess == magic){ printf("**Верно ** "); printf("Магическое число равно %dn", magic); } else guess > magic ? printf("Слишком большое ") : printf("Слишком малое "); return 0; } В этой программе оператор ? печатает соответствующее сообщение на основе про- верки условия guess > magic. Условное выражение У начинающих программистов иногда возникают трудности в связи с тем, что в услов- ном (управляющем) выражении операторов if или ? могут стоять любые операторы, при- чем это не обязательно операторы отношения или логические (как в языках Basic или Pas- cal). В языке С значением результата управляющего выражения являются ИСТИНА или ЛОЖЬ, однако тип результата может быть любым скалярным типом. Считается, что любой ненулевой результат представляет значение ИСТИНА, а нулевой — ЛОЖЬ. В следующем примере программа считывает с клавиатуры два числа, вычисляет их отношение и выводит его на экран. Оператор if используется для того, чтобы избе- жать деления на нуль, если второе число равно нулю. 1 /* Деление первого числа на второе. */ I #include <stdio.h> 88 Часть I. Основы языка С
int main(void) { int a, b; printf("Введите два числа: "); scanf("%d%d", &a, &b); if(b) printf("%dn", a/b); else printf("Делить на нуль нельзя.n"); return 0; } Если управляющее выражение Ь равно 0, то его результат представляет значение ЛОЖЬ и выполняется оператор else. В противном случае (Ь не равно нулю) резуль- тат представляет значение ИСТИНА и выполняется деление чисел. В последнем примере оператор if можно записать так: | if (b != 0) printf("%dn", a/b); Но следует отметить, что такая форма записи избыточна, она может привести к гене- рации неоптимального кода, кроме того, это считается признаком плохого стиля. Пе- ременная b сама по себе представляет значение ИСТИНА или ЛОЖЬ, поэтому срав- нивать ее с нулем нет необходимости. Оператор выбора - switch Оператор выбора switch (часто его называют переключателем) предназначен для вы- бора ветви вычислительного процесса исходя из значения управляющего выражения. (При этом значение управляющего выражения сравнивается со значениями в списке целых или символьных констант. Если будет найдено совпадение, то выполнится ассоциированный с совпавшей константой оператор.) Общая форма оператора switch следующая: switch (выражение) { case постоянная!: последовательность операторов break; case постоянная2: последовательность операторов break; case постояннаяЗ: последовательность операторов break; default: последовательность операторов; } Значение выражения оператора switch должно быть таким, чтобы его можно было вы- разить целым числом. Это означает, что в управляющем выражении можно использовать переменные целого или символьного типа, но только не с плавающей точкой. Значение управляющего выражения по очереди сравнивается с постоянными в операторах case. Если значение управляющего выражения совпадет с какой-то из постоянных, управление пере- дается на соответствующую метку case и выполняется последовательность операторов до оператора break. Если оператор break отсутствует, выполнение последовательности опе- Глава 3. Операторы 89
раторов продолжается до тех пор, пока не встретится break (в другой метке — Прим, ред.) или не кончится тело оператора switch (т.е. блок, следующий за switch). Оператор default выполняется в том случае, когда значение управляющего выражения не совпало ни с одной постоянной. Оператор default также может отсутствовать. В этом случае при отсутствии совпадений не выполняется ни один оператор. Согласно Стандарту С89, оператор switch может иметь как минимум 257 операто- ров case. Стандарт С99 требует поддержки как минимум 1023 операторов case. Ели вы пишете программы вручную, такое большое количество операторов вам никогда не понадобится1. Оператор case — это метка, однако он не может быть использован сам по себе, вне оператора switch. Оператор break — это один из операторов безусловного перехода. Он может приме- няться не только в операторе switch, но и в циклах, (см. раздел “Операторы цикла”). Когда в теле оператора switch встречается оператор break, программа выходит из опера- тора switch и выполняет оператор, следующий за фигурной скобкой } оператора switch. Об операторе switch очень важно помнить следующее: Оператор switch отличается от if тем, что в нем управляющее выражение проверяется только на равенство с постоянными, в то время как в if проверя- ется любой вид отношения или логического выражения. В одном и том же операторе switch никакие два оператора case не могут иметь равных постоянных. Конечно, если один switch вложен в другой, в их операторах case могут быть совпадающие постоянные. Если в управляющем выражении оператора switch встречаются символьные константы, они автоматически преобразуются к целому типу по принятым в языке С правилам приведения типов. Оператор switch часто используется для обработки команд с клавиатуры, напри- мер, при выборе пунктов меню. В следующем примере программа выводит на экран меню проверки правописания и вызывает соответствующую процедуру: void menu(void) { char ch; printf("1. Проверка правописанияп"); printf("2. Коррекция ошибокп"); printf("3. Вывод ошибокп"); printf("Для пропуска нажмите любую клавишуп"); printf(" Введите ваш выбор: "); ch = getchar(); /* чтение клавиши */ switch(ch) { case '1’: check_spelling(); break; case ’2': correct_errors(); break; case 13’: display_errors(); break; default: 1 Если же для генерации программ вы используете макрогенераторы или генераторы компилято- ров, например, уасс или lex, то на данное ограничение следует обратить внимание. — Прим. ред. 90 Часть I. Основы языка С
printf("Не выбрана ни одна опция."); I } I } С точки зрения синтаксиса, присутствие операторов break внутри switch не обя- зательно. Они прерывают выполнение последовательности операторов, ассоциирован- ных с данной константой. Если оператор break отсутствует, то выполняется следую- щий оператор case, пока не встретится очередной break, или не будет достигнут ко- нец тела оператора switch. Например, в функции inp_handler() (обработчик ввода драйвера) для упрощения программы несколько операторов break опущено, поэтому выполняются сразу несколько операторов case: /* Обработка значения i */ void inp_handler(int i) { int flag; flag = -1; switch(i) { case 1: /* Эти case имеют общую */ case 2: /* последовательность операторов */ case 3: flag = 0; break; case 4: flag = 1; case 5: error(flag); break; default: process(i) ; Приведенный пример иллюстрирует следующие две особенности оператора switch(). Во-первых, оператор case может не иметь ассоциированной с ним последовательности операторов. Тогда управление переходит к следующему case. В этом примере три первых case вызывают выполнение одной и той же последовательности операторов, а именно: Iflag = 0; break; Во-вторых, если оператор break отсутствует, то выполняется последовательность операторов следующего case. Если i равно 4, то переменной flag присваивается значение 1 и, поскольку break отсутствует, выполнение продолжается и вызывается error (flag). Если i равно 5, то error () будет вызвана со значением переменной flag, равным —1, а не 1. То, что при отсутствии break операторы case выполняются вместе, позволяет из- бежать ненужного дублирования операторов1. 1 Но представляет собой опасность для забывчивых программистов. — Прим. ред. Глава 3. Операторы
Вложенные операторы switch Оператор switch может находиться в теле внешнего по отношению к нему опера- тора switch. Операторы case внутреннего и внешнего switch могут иметь одинако- вые константы, в этом случае они не конфликтуют между собой. Например, следую- щий фрагмент программы вполне работоспособен: switch(х) { case 1: switch(у) { case 0: printf("Деление на нуль.п"); break; case 1: process(х, у); break; } break; case 2: Операторы цикла В языке С, как и в других языках программирования, операторы цикла служат для многократного выполнения последовательности операторов до тех пор, пока выпол- няется некоторое условие. Условие может быть установленным заранее (как в опера- торе for) или меняться при выполнении тела цикла (как в while или do-while). Цикл for Во всех процедурных языках программирования циклы for очень похожи. Однако в С этот цикл особенно гибкий и мощный. Общая форма оператора for следующая: for (инициализация; условие; приращение) оператор; Цикл for может иметь большое количество вариаций. В наиболее общем виде принцип его работы следующий. Инициализация — это присваивание начального зна- чения переменной, которая называется параметром цикла. Условие представляет собой условное выражение, определяющее, следует ли выполнять оператор цикла (часто его называют телом цикла) в очередной раз. Оператор приращение осуществляет измене- ние параметра цикла при каждой итерации. Эти три оператора (они называются также секциями оператора for) обязательно разделяются точкой с запятой. Цикл for выпол- няется, если выражение условие принимает значение ИСТИНА. Если оно хотя бы один раз примет значение ЛОЖЬ, то программа выходит из цикла и выполняется оператор, следующий за телом цикла for. В следующем примере в цикле for выводятся на экран числа от 1 до 100: tfinclude <stdio.h> int main(void) { int x; for(x=l; x <= 100; x++) printf("%d ", x); return 0; 92 Часть I. Основы языка С
В этом примере параметр цикла х инициализирован числом 1, а затем при каждой итерации сравнивается с числом 100. Пока переменная х меньше 100, вызывается функция printf () и цикл повторяется. При этом х увеличивается на 1 и опять про- веряется условие цикла х <= 100. Процесс повторяется, пока переменная х не станет больше 100. После этого процесс выходит из цикла, а управление передается операто- ру, следующему за ним. В этом примере параметром цикла является переменная х, при каждой итерации она изменяется и проверяется в секции условия цикла. В следующем примере в цикле for выполняется блок операторов: I for(x=100; х != 65; х -= 5) { I z = х*х; I printf("Квадрат %d равен %dn", х, z); I } Операции возведения переменной х в квадрат и вызова функции printf () повторяют- ся, пока х не примет значение 65. Обратите внимание на то, что здесь параметр цикла уменьшается, он инициализирован числом 100 и уменьшается на 5 при каждой итерации. В операторе for условие цикла всегда проверяется перед началом итерации. Это значит, что операторы цикла могут не выполняться ни разу, если перед первой итера- цией условие примет значение ЛОЖЬ. Например, в следующем фрагменте программы I х = 10; I for(y=10; у != х; ++у) printf("%d", у); I printf("%d", у); /★ Это единственный printf(), I который будет выполнен */ цикл не выполнится ни разу, потому что при входе в цикл значения переменных х и у равны. Поэтому условие цикла принимает значение ЛОЖЬ, а тело цикла и оператор приращение не выполняются. Переменная у остается равной 10, единственный резуль- тат работы этой программы — вывод на экран числа 10 в результате вызова функции printf (), расположенной вне цикла. Варианты цикла for В предыдущем разделе рассмотрена наиболее общая форма цикла for. Однако в языке С допускаются некоторые его варианты, позволяющие во многих случаях уве- личить мощность и гибкость программы. Один из распространенных способов усиления мощности цикла for — примене- ние оператора “запятая” для создания двух параметров цикла. Оператор “запятая” связывает несколько выражений, заставляя их выполняться вместе (см. главу 2). В следующем примере обе переменные (х и у) являются параметрами цикла for и обе инициализируются в этом цикле: for(x=0, у=0; х'+у < 10; ++х) { у = getchar(); у = у - 'О'; /* Вычитание из у ASCII-кода нуля */ } Здесь запятая разделяет два оператора инициализации. При каждой итерации зна- чение переменной х увеличивается, а значение у вводится с клавиатуры. Для выпол- нения итерации как х, так и у должны иметь определенное значение. Несмотря на то что значение у вводится с клавиатуры, оно должно быть инициализировано таким об- разом, чтобы выполнилось условие цикла при первой итерации. Если у не инициали- Глава 3. Операторы 93
зировать, то оно может случайно оказаться таким, что условие цикла примет значение ЛОЖЬ, тело цикла не будет выполнено ни разу. Следующий пример демонстрирует использование двух параметров цикла. Функ- ция converge () копирует содержимое одной строки в другую, начиная с обоих кон- цов строки и кончая в ее середине. /* Демонстрация использования 2-х параметров цикла */ #include <stdio.h> #include <string.h> void convergence(char *targ, char *src); int main(void) { char target[80] ="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; convergence(target, • "Это проверка функции converge()."); printf("Строка-результат: %sn", target); return 0; } /* Эта функция копирует содержимое одной строки в другую, начиная с обоих концов и сходясь посередине */ void convergence(char *targ, char *src) { int i, j; printf("%sn", targ); for(i=0, j=strlen(src); i<=j; i++, j—) { targ[i] = src[i]; targ[j] = src[j]; printf("%sn", targ); } } Программа выводит на экран следующее: хххххххххххххххххххххххххххххххх эххххххххххххххххххххххххххххххх ЭтХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ. ЭтоХХХХХХХХХХХХХХХХХХХХХХХХХХХ). Это ХХХХХХХХХХХХХХХХХХХХХХХХХ(). Это пХХХХХХХХХХХХХХХХХХХХХХХе(). Это прХХХХХХХХХХХХХХХХХХХХХде(). Это проХХХХХХХХХХХХХХХХХХХгде(). Это провХХХХХХХХХХХХХХХХХегдео. Это nposeXXXXXXXXXXXXXXXverge(). Это nposepXXXXXXXXXXXXXnverge(). Это nposepKXXXXXXXXXXXonverge () . Это nposepKaXXXXXXXXXconverge(). Это проверка ХХХХХХХ converge (). Это проверка фХХХХХи converge (). Это проверка фуХХХии converge (). Это проверка фунХции converge(). Это проверка функции converge (). Строка-результат: Это проверка функции converge(). 94 Часть I. Основы языка С
В функции convergence () цикл for использует два параметра цикла (i и j) для индексации строки с противоположных концов. Параметр i в цикле увеличивается, а j — уменьшается. Итерации прекращаются, когда i становится больше j. Это обеспе- чивает копирование всех символов. Проверка параметра цикла на соответствие некоторому условию не обязательна. Условие может быть любым логическим оператором или оператором отношения. Это значит, что условие выполнения цикла может состоять из нескольких условий, или операторов отношения. Следующий пример демонстрирует применение составного условия цикла для проверки пароля, вводимого пользователем. Пользователю предос- тавляются три попытки ввода пароля. Программа выходит из цикла, когда использо- ваны все три попытки или когда введен верный пароль. void sign_on(void) { char str[20] ; int x; for(x=0; x<3 && strcmp(str, "password")); { printf("Пожалуйста, введите пароль: "); gets(str); } if(x == 3) return; /* Иначе пользователь допускается. */ } Функция sign_on() использует стандартную библиотечную функцию strcmpO, которая сравнивает две строки и возвращает 0, если они совпадают. Следует помнить, что каждая из трех секций оператора for может быть любым синтаксически правильным выражением. Эти выражения не всегда каким-либо обра- зом отображают назначение секции. Рассмотрим следующий пример: #include <stdio.h> int sqrnum(int num); int readnum(void); int prompt(void); int main(void) { int t; for(prompt(); t=readnum(); prompt()) sqrnum(t); return 0; } int prompt(void) { printf("Введите число "); return 0; } int readnum(void) { int t; Глава 3. Операторы 95
scanf(”%d", &t); return t; } int sqrnum(int num) { printf("%dn”, num* num) ; return num*num; } Здесь в main () каждая секция цикла for состоит из вызовов функций, которые предлагают пользователю ввести число и считывают его. Если пользователь ввел 0, то цикл прекращается, потому что тогда условие цикла принимает значение ЛОЖЬ. В противном случае число возводится в квадрат. Таким образом, в этом примере цикла for секции инициализации и приращения используются весьма необычно, но совер- шенно правильно. Другая интересная особенность цикла for состоит в том, что его секции могут быть вообще пустыми, присутствие в них какого-либо выражения не обязательно. В следующем примере цикл выполняется, пока пользователь не введет число 123: | for(x=0; х!= 123; ) scanf(”%d”, &х); Секция приращения оператора for здесь оставлена пустой. Это значит, что перед каждой итерацией значение переменной х проверяется на неравенство числу 123, а приращения не происходит, оно здесь ненужно. Если с клавиатуры ввести число 123, то условие принимает значение ЛОЖЬ и программа выходит из цикла. Инициализацию параметра цикла for можно сделать за пределами этого цикла, но, конечно, до него. Это особенно уместно, если начальное значение параметра цик- ла вычисляется достаточно сложно, например: gets(s); /* читает строку в s */ if(*s) х = strlen(s); /* вычисление длины строки */ else х = 10; for ( ; х < 10; ) { printf(”%d”, x); ++x; } В этом примере секция инициализации оставлена пустой, а переменная х инициа- лизируется до входа в цикл. Бесконечный цикл Для создания бесконечного цикла можно использовать любой оператор цикла, но чаще всего для этого выбирают оператор for. Так как в операторе for может отсутст- вовать любая секция, бесконечный цикл проще всего сделать, оставив пустыми все секции. Это хорошо показано в следующем примере: | for( ; ; ) printf(” Этот цикл крутится бесконечно "); Если условие цикла for отсутствует, то предполагается, что его значение — ИСТИНА. В оператор for можно добавить выражения инициализации и приращения, хотя обычно для создания бесконечного цикла используют конструкцию for ( ; ; ). Фактически конструкция for ( ; ; ) не гарантирует бесконечность итераций, по- тому что в нем может встретиться оператор break, вызывающий немедленный выход из цикла. (Подробно оператор break рассмотрен в этой главе далее.) В этом случае 96 Часть I. Основы языка С
выполнение программы продолжается с оператора, следующего за закрывающейся фигурной скобкой цикла for: ch = •1; for( ; ; ) { ch = getcharO; /* считывание символа */ if (ch == ’A1) break; /* выход из цикла */ } printf(" Вы напечатали ’А,п); В данном примере цикл выполняется до тех пор, пока пользователь не введет с клавиатуры символ А. Цикл for без тела цикла Следует учесть, что оператор может быть пустым. Это значит, что тело цикла for (или любого другого цикла) также может быть пустым. Такую особенность цикла for можно использовать для упрощения некоторых программ, а также в циклах, предна- значенных для того, чтобы отложить выполнение последующей части программы на некоторое время. Программисту иногда приходится решать задачу удаления пробелов из входного потока. Допустим, программа, работающая с базой данных, обрабатывает запрос “показать все балансы меньше 400”. База данных требует представления каждого сло- ва отдельно, без пробелов, т.е. обработчик распознает слово “показать”, но не “ по- казать”. В следующем примере цикл for удаляет начальные пробелы в строке str: | for ( ; *str == 1 1; str++) ; В этом примере указатель str переставляется на первый символ, не являющийся пробелом. Цикл не имеет тела, так как в нем нет необходимости.1 Иногда возникает необходимость отложить выполнение последующей части программы на определенное время. Это можно сделать с помощью цикла for следующим образом: | for(t=0; t < SOME_VALUE; t++) ; Единственное назначение этого цикла — задержка выполнения последующей час- ти программы. Однако следует иметь в виду, что компилятор может оптимизировать объектный код таким образом, что пропустит этот цикл вообще, поскольку он не вы- полняет никаких действий, тогда желаемой задержки выполнения последующей части программы не произойдет. Объявление переменных внутри цикла В стандартах С99 и C++ (но не С89!) допускается объявление переменных в сек- ции инициализации цикла for. Объявленная таким образом переменная является ло- кальной переменной цикла и ее область действия распространяется на тело цикла. Рассмотрим следующий пример: 1 Этот пример, конечно, учебный. На практике так поступать со строкой не рекомендуется, потому что начало строки str, “напрасно висящее” в памяти, впоследствии может создать не- которые трудности. Например, если вы захотите освободить память, занимаемую данной стро- кой, вам потребуется указать на начало строки, а не на первый отличный от пробела символ в этой строке. — Прим, перев. Глава 3. Операторы 97
/★ Здесь переменная i является локальной переменной цикла, a j видима вне цикла. *** Этот пример в С89 неправильный! ***/ int j ; for (int i = 0; i<10; i++) j = i * i; /* i = 10; Это ошибка, переменная i здесь недоступна! */ В данном примере переменная i объявлена в секции инициализации цикла for и служит параметром цикла. Вне цикла переменная i невидима. Поскольку параметр цикла чаще всего необходим только внутри цикла, его объяв- ление в секции инициализации очень удобно и входит в широкую практику1. Однако необходимо помнить, что это не поддерживается стандартом С89. Цикл while Общая форма цикла while имеет следующий вид: while (условие) оператор; Здесь оператор (тело цикла) может быть пустым оператором, единственным опера- тором или блоком. Условие (управляющее выражение) может быть любым допустимым в языке выражением. Условие считается истинным, если значение выражения не рав- но нулю, а оператор выполняется, если условие принимает значение ИСТИНА. Если условие принимает значение ЛОЖЬ, программа выходит из цикла и выполняется сле- дующий за циклом оператор. В следующем примере ввод с клавиатуры происходит до тех пор, пока пользова- тель не введет символ А: char wait_for_char(void) { char ch; ch = ’ ’; /* инициализация ch */ while(ch != ’A’) ch = getchar(); return ch; Переменная ch является локальной, ее значение при входе в функцию произвольно, поэтому сначала значение ch инициализируется нулем. Условие цикла while истинно, ес- ли ch не равно А. Поскольку ch инициализировано нулем, условие истинно и цикл начи- нает выполняться. Условие проверяется при каждом нажатии клавиши пользователем. При вводе символа А условие становится ложным и выполнение цикла прекращается. Как и в цикле for, в цикле while условие проверяется перед началом итерации. Это значит, что если условие ложно, тело цикла не будет выполнено. Благодаря этому нет необходимости вводить в программу отдельное условие перед циклом. Рассмотрим это на примере функции pad (), которая добавляет пробелы в конец строки и делает ее длину равной предварительно заданной величине. Если строка уже имеет необхо- димую длину, то пробелы не добавляются: |#include <stdio.h> #include <string.h> void pad(char *s, int length); 1 В некоторых языках (например АЛГОЛ 68) локализация параметра цикла выполняется ав- томатически. — Прим. ред. 98 Часть I. Основы языка С
int main(void) { char str[80]; strcpy(str, ’’это проверка’’); pad(str, 40); printf("%d”, strlen(str)); return 0; } /* Добавление пробелов в конец строки. */ void pad(char *s, int length) { int 1; 1 = strlen(s); /* определение длины строки */ while(1 < length) { s[l] = ’ /* вставка пробела */ 1++; } s[l]= ’’; /* строка должна заканчиваться нулем */ } Аргументами функции pad () являются s (указатель на исходную строку) и length (требуемое количество символов в строке). Если длина строки s при входе в функцию равна или больше length, то цикл while не выполняется. В противном случае pad() добавляет требуемое количество пробелов, а библиотечная функция strlen () воз- вращает длину строки. Если выполнение цикла должно зависеть от нескольких условий, можно создать так называемую управляющую переменную, значения которой присваиваются разны- ми операторами тела цикла. Рассмотрим следующий пример: void fund (void) { int working; working = 1;/* то есть ИСТИНА */ while(working) { working = processlO; if(working) working = process2(); if(working) working = process3(); } } В этом примере переменная working является управляющей. Любая из трех функ- ций может возвратить значение 0 и этим прервать выполнение цикла. Тело цикла while может быть пустым. Например, цикл | while((ch=getchar()) != ’А’) ; выполняется до тех пор, пока пользователь не введет символ А'. Напоминаем, что оператор присваивания выполняет две задачи: присваивает значение выражения спра- ва переменной слева и возвращает это значение как свое собственное. Глава 3. Операторы 99
Цикл do-while В отличие от циклов for и while, которые проверяют свое условие перед итера- цией, do-while делает это после нее. Поэтому цикл do-while всегда выполняется как минимум один раз. Общая форма цикла do-while следующая: do { оператор; } while (условие); Если оператор не является блоком, фигурные скобки не обязательны, но их почти всегда ставят, чтобы оператор достаточно наглядно отделялся от условия. Итерации оператора do-while выполняются, пока условие не примет значение ЛОЖЬ. В следующем примере в цикле do-while числа считываются с клавиатуры, пока не встретится число, меньшее или равное 100: I do { I scanf(”%d”, &num) ; I } while(num > 100); Цикл do-while часто используется в функциях выбора пунктов меню. Если пользователь вводит допустимое значение, оно возвращается в качестве значения функции. В противном случае цикл требует повторить ввод. Следующий пример демонстрирует усовершенствованную версию программы для выбора пункта меню проверки грамматики: void menu(void) { char ch; printf(”1. Проверка правописанияп"); printf(”2. Коррекция ошибокп"); printf(”3. Вывод ошибокп"); printf(” Введите ваш выбор: ”); do { ch = getchar(); /* чтение выбора с клавиатуры */ switch(ch) { case ’1’: check_spelling(); break; case ’2’: correct_errors (); break; case ’3’; display_errors () ; break; } } while(ch!=’1’ && ch’='2’ && ch!=’3’); } В этом примере применение цикла do-while весьма уместно, потому что итерация, как уже упоминалось, всегда должна выполниться как минимум один раз. Цикл по- вторяется, пока его условие не станет ложным, т.е. пока пользователь не введет один из допустимых ответов. 100 Часть I. Основы языка С
S Операторы перехода В языке С определены четыре оператора перехода: return, goto, break и continue. Операторы return и goto можно использовать в любом месте внутри функции. Операторы break и continue можно использовать в любом из операторов цикла. Как указывалось ранее в этой главе, break можно также использовать в опера- торе switch. Оператор return Оператор return используется для выхода из функции. Отнесение его к категории операторов перехода обусловлено тем, что он заставляет программу перейти в точку вызова функции. Оператор return может иметь ассоциированное с ним значение, тогда при выполнении данного оператора это значение возвращается в качестве зна- чения функции. В функциях типа void используется оператор return без значения. Стандарт С89 допускает наличие оператора return без значения, даже если тип функции отличен от void. В этом случае функция возвращает неопределенное значе- ние. Но что касается языков С99 и C++, если тип функции отличен от void, то ее оператор return обязательно должен иметь значение. Конечно, и в программе на С89 отсутствие возвращаемого значения в функции, тип которой отличен от void, являет- ся признаком плохого стиля! Общая форма оператора return следующая: return выражение; Выражение присутствует только в том случае, если функция возвращает значение. Это значение выражения становится возвращаемым значением функции. Внутри функции может присутствовать произвольное количество операторов return. Выход из функции происходит тогда, когда встречается один из них. Закры- вающаяся фигурная скобка } также вызывает выход из функции. Выход программы на нее эквивалентен оператору return без значения. В этом случае функция, тип кото- рой отличен от void, возвращает неопределенное значение. Функция, определенная со спецификатором void, не может содержать return со зна- чением. Так как эта функция не возвращает значения, в ней не может быть оператора return, возвращающего значение. Более подробно return рассматривается в главе 6. Оператор goto Кроме goto, в языке С есть другие операторы управления (например break, continue), поэтому необходимости в применении goto практически нет. В результа- те чрезмерного использования операторов goto программа плохо читается, она стано- вится “похожей на спагетти”. Чаще всего такими программами недовольна админист- рация фирм, производящих программный продукт. То есть оператор goto весьма не- популярен, более того, считается, что в программировании не существует ситуаций, в которых нельзя обойтись без оператора goto. Но в некоторых случаях его применение все же уместно. Иногда, при умелом использовании, этот оператор может оказаться весьма полезным, например, если нужно покинуть глубоко вложенные циклы1. В данной книге оператор goto рассматривается только в этом разделе. 1 Уже одно это (чрезмерная вложенность и неожиданный выход сразу из нескольких цик- лов) может свидетельствовать о плохой структуре программы. — Прим. ред. Глава 3. Операторы 101
Для оператора goto всегда необходима метка. Метка — это идентификатор с по- следующим двоеточием. Метка должна находится в той же функции, что и goto, пе- реход в другую функцию невозможен. Общая форма оператора goto следующая: goto метка; метка: Метка может находиться как до, так и после оператора goto. Например, используя оператор goto, можно выполнить цикл от 1 до 100: I х = 1; I loopl: I х++; I if(x <= 100) goto loopl; Оператор break Оператор break применяется в двух случаях. Во-первых, в операторе switch с его помощью прерывается выполнение последовательности case (см. раздел “Оператор выбора — switch” ранее в этой главе). В этом случае оператор break не передает управление за пределы блока. Во-вторых, оператор break используется для немедлен- ного прекращения выполнения цикла без проверки его условия, в этом случае опера- тор break передает управление оператору, следующему после оператора цикла. Когда внутри цикла встречается оператор break, выполнение цикла безусловно (т.е. без проверки каких-либо условий. — Прим, ред.) прекращается и управление пе- редается оператору, следующему за ним. Например, программа ttinclude <stdio.h> int main(void) { int t; for(t=0; t < 100; t++) { printf("%d ", t); if(t == 10) break; } return 0; выводит на экран числа от 0 до 10. После этого выполнение цикла прекращается опе- ратором break, условие t < 100 при этом игнорируется. Оператор break часто используется в циклах, в которых некоторое событие долж- но вызвать немедленное прекращение выполнения цикла. В следующем примере на- жатие клавиши прекращает выполнение функции look_up (): void look_up(char *name) { do { /* поиск имени 'name’ */ if(kbhit()) break; } while(!found); 102 Часть I. Основы языка С
Библиотечная функция kbhit () возвращает 0, если клавиша не нажата (то есть, буфер клавиатуры пуст), в противном случае она возвращает ненулевое значение. В стандарте С функция kbhit () не определена, однако практически она поставляется почти с каждым компилятором (возможно, под несколько другим именем). Оператор break вызывает выход только из внутреннего цикла. Например, программа for(t=0; t < 100; ++t) { count = 1; f or (; ; ) { printf("%d ", count); count++; if(count == 10) break; } } 100 раз выводит на экран числа от 1 до 9. Оператор break передает управление внеш- нему циклу for. Если оператор break присутствует внутри оператора switch, который вложен в какие- либо циклы, то break относится только к switch, выход из цикла не происходит. Функция exit () Функция exit () не является оператором языка, однако рассмотрим возможность ее применения. Аналогично прекращению выполнения цикла оператором break, можно прекратить работу программы и с помощью вызова стандартной библиотечной функции exit (). Эта функция вызывает немедленное прекращение работы всей программы и передает управление операционной системе. Общая форма функции exit () следующая: void exit (int код__возврата) Значение переменной код_возврата передается вызвавшему программу процессу, обычно в качестве этого процесса выступает операционная система. Нулевое значение кода возврата обычно используется для указания нормального завершения работы программы. Другие значения указывают на характер ошибки. В качестве кода возврата можно использовать макросы exit_success и EXIT_FAILURE (выход успешный и выход с ошибкой). Функция exit () объявлена в заголовочном файле <stdlib.h>. Функция exit () часто используется, когда обязательное условие работы програм- мы не выполняется. Рассмотрим, например, компьютерную игру в виртуальной ре- альности, использующую специальный графический адаптер. Главная функция main () этой игры выглядит так: #include <stdlib.h> int main(void) { if(!virtual—graphics() ) exit(l); play () ; /* */ } Здесь virtual_graphics () возвращает значение ИСТИНА, если присутствует нужный графический адаптер. Если требуемого адаптера нет, вызов функции exit (1) прекращает работу программы. В следующем примере в новой версии ранее рассмотренной функции menu () вызов exit () используется для выхода из программы и возврата в операционную систему: Глава 3. Операторы 103
void menu(void) { char ch; printf("1. Проверка правописанияп”); printf("2. Коррекция ошибокп”); printf("3. Вывод ошибокп"); printf("4. Выход из программып"); printf (’’ Введите ваш выбор: ") ; do { ch = getcharO; /* чтение клавиши */ switch(ch) { case '1' : check_spelling(); break; case ' 2 ' : correct_errors (); break; case ’3 ’ : display_errors (); break; case ' 4 ’ : exit(O); /* Возвращение в ОС */ } } while(ch!='1’ && ch!='2' && ch!='3’); } Оператор continue Можно сказать, что оператор continue немного похож на break. Оператор break вы- зывает прерывание цикла, a continue — прерывание текущей итерации цикла и осущест- вляет переход к следующей итерации. При этом все операторы до конца тела цикла про- пускаются. В цикле for оператор continue вызывает выполнение операторов прираще- ния и проверки условия цикла. В циклах while и do-while оператор continue передает управление операторам проверки условий цикла. В следующем примере программа под- считывает количество пробелов в строке, введенной пользователем: /* Подсчет количества пробелов */ #include <stdio.h> int main(void) { char s [80], *str; int space; printf(" Введите строку: "); gets (s) ; str = s; for(space=0; *str; str++) { if(*str != ’ ') continue; space++; } printf(”%d пробеловп", space); return 0; } 104 Часть I. Основы языка С
Каждый символ строки сравнивается с пробелом. Если сравниваемый символ не является пробелом, оператор continue передает управление в конец цикла for и вы- полняется следующая итерация. Если символ является пробелом, значение перемен- ной space увеличивается на 1. В следующем примере оператор continue применяется для выхода из цикла while путем передачи управления на условие цикла: void code(void) { char done, ch; done = 0; while(!done) { ch = getchar(); if(ch == ’$’) { done = 1; continue; } putchar(ch+1);/* печать следующего в алфавитном порядке символа */ } Функция code предназначена для кодирования сообщения путем замены каждого символа символом, код которого на 1 больше кода исходного символа в коде ASCII. Например, символ А заменяется символом В (если это латинские символы. — Прим, перев.). Функция прекращает работу при вводе символа $. При этом переменной done присваивается значение 1 и оператор continue передает управление на условие цик- ла, что и прекращает выполнение цикла. ® Оператор-выражение Выражения были подробно рассмотрены в главе 2. Но говоря об операторах, будет уместно добавить несколько слов и о выражениях. Любое выражение, которое закан- чивается точкой с запятой, является оператором. Рассмотрим следующие примеры: /* вызов функции */ /* оператор присваивания */ /* правильный, но ’’странный” оператор */ /* пустой оператор */ func(); а = Ь+с; b+f () ; Первый оператор выполняет вызов функции, второй — присваивание. Третий оператор выглядит странно, но транслятор все же не укажет на ошибку (возможно, даст предупреж- дение). В этом операторе необходимые действия, видимо, выполняются функцией f (). Последний пример — пустой оператор, не выполняющий никакого действия. В Блок операторов Блок — это последовательность операторов, заключенных в фигурные скобки и рассматриваются как одна программная единица. Операторы, составляющие блок, логически связаны друг с другом. Иногда блок называют составным оператором. Блок всегда начинается открывающейся фигурной скобкой { и заканчивается закрываю- щейся }. Чаще всего блок используется как составная часть какого-либо оператора, Глава 3. Операторы 105
выполняющего действие над группой операторов, например, if или for. Однако блок можно поставить в любом месте, где может находиться оператор, как это показано в следующем примере: #include <stdio.h> int main(void) { int i; { /* блок операторов */ i = 120; printf("%d", i) ; } return 0; } 106 Часть I. Основы языка С
Полный справо - ник по Глава 4 Массивы и строки
Массив — это набор переменных одного типа, имеющих одно и то же имя. Доступ к конкретному элементу массива осуществляется с помощью индекса. В языке С все массивы располагаются в отдельной непрерывной области памяти. Первый элемент массива располагается по самому меньшему адресу, а последний — по само- му большому. Массивы могут быть одномерными и многомерными. Строка представ- ляет собой массив символьных переменных, заканчивающийся специальным нулевым символом, это наиболее распространенный тип массива. Массивы и указатели тесно связаны. То, что может быть сказано о массивах, чаще всего непосредственно относится и к указателям, и наоборот. В этой главе рассматри- ваются массивы, указатели будут подробно рассмотрены в главе 5. Для полного пони- мания приемов работы с массивами и указателями читателю следует хорошо усвоить обе эти главы. В Одномерные массивы Общая форма объявления одномерного массива имеет следующий вид: тип имя переменной [размер]', Как и другие переменные, массив должен быть объявлен явно, чтобы компилятор выделил для него определенную область памяти (т.е. разместил массив). Здесь тип обозначает базовый тип массива, являющийся типом каждого элемента. Размер задает количество элементов массива. Например, следующий оператор объявляет массив из 100 элементов типа double под именем balance: (double balance[100]; Согласно стандарту С89 размер массива должен быть указан явно с помощью вы- ражения-константы. Таким образом, в программе на С89 размер массива определяет- ся во время компиляции и впоследствии остается неизменным. (В С99 определены массивы, размер которых определяется во время выполнения. О них еще будет идти речь далее в этой главе, а также более подробно в части II). Доступ к элементу массива осуществляется с помощью имени массива и индекса. Индекс элемента массива помещается в квадратных скобках после имени. Например, оператор | balance[3] = 12.23; присваивает 3-му элементу массива balance значение 12.23. Индекс первого элемента любого массива в языке С равен нулю. Поэтому оператор | char р[10]; объявляет массив символов из 10 элементов — от р[0] до р[9]. В следующей программе вычисляются значения элементов массива целого типа с индексами от 0 до 99: #include <stdio.h> int main(void) { int x[100]; /* объявление массива 100 целых */ int t; /* присваивание массиву значений от 0 до 99 */ for(t=0; t<100; t++) x[t] = t; /* вывод на экран содержимого х */ 108 Часть I. Основы языка С
|for(t=0; t<100; t++)'printf("%d ”, x[t]); return 0; } Объем памяти, необходимый для хранения массива, непосредственно определяется его типом и размером. Для одномерного массива количество байтов памяти вычисля- ется следующим образом: количество_байтов = sizeof(6a3OBbW_Tnn) х длина_массива Во время выполнения программы на С не проверяется ни соблюдение границ мас- сивов, ни их содержимое. В область памяти, занятую массивом, может быть записано что угодно, даже программный код. Программист должен сам, где это необходимо, ввести проверку границ индексов. Следующий пример программы компилируется без ошибки, однако при выполнении происходит нарушение границы массива count и разрушение соседних участков памяти: Iint count[10], i; /* здесь нарушена граница массива count */ for(i=0; i<100; i++ count[i] = i; Можно сказать, что одномерный массив — это список, хранящийся в непрерывной области памяти в порядке индексации. На рис. 4.1 показано, как хранится в памяти массив а, начинающийся по адресу 1000 и объявленный как | char а[7]; Элемент а[0] а[1] а[2] а[3] а[4] а[5] а[6] Адрес 1000 1001 1002 1003 1004 1005 1006 Рис. 4.1. Массив из семи символов, начинающийся по адресу 1000 И Создание указателя на массив Указатель на 1-й элемент массива можно создать путем присваивания ему имени массива без индекса. Например, если есть объявление | int sample[10]; то в качестве указателя на 1-й элемент массива можно использовать имя sample. В следующем фрагменте программы адрес 1-го элемента массива sample присваивается указателю р: Iint *р; int sample[10]; р = sample; В обеих переменных (р и sample) хранится адрес 1-го элемента, отличаются эти переменные только тем, что значение sample в программе изменять нельзя. Адрес первого элемента можно также получить, используя оператор получения адреса &. Например, выражения sample и &sample[0] имеют одно и то же значение. Тем не менее, в профессионально написанных программах вы не встретите выражения &sample [0]. Глава 4. Массивы и строки 109
В Передача одномерного массива в функцию В языке С нельзя передать весь массив как аргумент функции. Однако можно пе- редать указатель на массив, т.е. имя массива без индекса. Например, в представлен- ной программе в fund () передается указатель на массив i: int main(void) { int i[10]; fund (i) ; /* */ Если в функцию передается указатель на одномерный массив, то в самой функции его можно объявить одним из трех вариантов: как указатель, как массив определенного разме- ра и как массив без определенного размера. Например, чтобы функция fund () получила доступ к значениям, хранящимся в массиве i, она может быть объявлена как Ivoid fund (int *х) /* указатель */ { /* */ } или как Ivoid fund (int х[10]) /* массив определенного размера */ { /* */ } и наконец как Ivoid fund (int х[]) /* массив без определенного размера */ { /* */ } Эти три объявления тождественны, потому что каждое из них сообщает компиля- тору одно и то же: в функцию будет передан указатель на переменную целого типа. В первом объявлении используется указатель, во втором — стандартное объявление мас- сива. В последнем примере измененная форма объявления массива сообщает компи- лятору, что в функцию будет передан массив неопределенной длины. Как видно, дли- на массива не имеет для функции никакого значения, потому что в С проверка гра- ниц массива не выполняется. Эту функцию можно объявить даже так: Ivoid fund (int х[32]) ; /- ... ./ И при этом программа будет выполнена правильно, потому что компилятор не созда- ет массив из 32 элементов, а только подготавливает функцию к приему указателя. В Строки Одномерный массив наиболее часто применяется в виде строки символов. Стро- ка — это одномерный массив символов, заканчивающийся нулевым символом. В язы- ке С признаком окончания строки (нулевым символом) служит символ ' '. Таким 110 Часть I. Основы языка С
На заметку образом, строка содержит символы, составляющие строку, а также нулевой символ. Это единственный вид строки, определенный в С. В C++ дополнительно определен специальный класс строк, называющийся string1, который позволяет обрабатывать строки объектно-ориентированными метода- ми. Стандарт С не поддерживает string. Объявляя массив символов, предназначенный для хранения строки, необходимо предусмотреть место для нуля, т.е. указать его размер в объявлении на один символ больше, чем наибольшее предполагаемое количество символов. Например, объявле- ние массива str, предназначенного для хранения строки из 10 символов, должно выглядеть так: | char str[11]; Последний, 11-й байт предназначен для нулевого символа. Записанная в тексте программы строка символов, заключенных в двойные кавыч- ки, является строковой константой, например, ’’некоторая строка” В конец строковой константы компилятор автоматически добавляет нулевой символ. Для обработки строк в С определено много различных библиотечных функций. Чаще всего используются следующие функции: Имя функции Выполняемое действие strcpy(s 1, s2) Копирование s2 в s1 strcat(s7,s2) Конкатенация (присоединение) s2 в конец s1 strlen(sf) Возвращает длину строки s1 strcmp(s7,s2) Возвращает 0, если s1 и s2 совпадают, отрицательное значение, если s1<s2 и положительное значение, если s1>s2 strchr(s7 ,ch) Возвращает указатель на первое вхождение символа ch в строку s1 strstr(s7,s2) Возвращает указатель на первое вхождение строки s2 в строку s1 Эти функции объявлены в заголовочном файле <string.h>. Применение библиотеч- ных функций обработки строк иллюстрируется следующим примером: #include <stdio.h> #include <string.h> int main(void) { char si [80], s2 [80]; gets (si); gets (s2); printf("Длина: %d %dn", strlen(sl), strlen(s2)); if(!strcmp(si, s2)) printf("Строки равнып"); strcat(si, s2); printf(”%sn”, si); strcpy(sl, "Проверка.n”); printf(si); if(strchr("Алло!",’л’)) printf(" л есть в Аллоп”); if(strstr("Привет!”,"ив”)) printf(" найдено ив ”); 1 CString в среде Visual C++. — Прим, перев. Глава 4. Массивы и строки 111
return 0; | } Если эту программу выполнить и ввести в si и в s2 одну и ту же строку “Алло! ”, то на экран будет выведено следующее: Длина: 5 5 Строки равны Алло!Алло 1 Проверка, л есть в Алло найдено ив Следует помнить, что strcmpO принимает значение ЛОЖЬ, если строки совпа- дают (хоть это и несколько нелогично). Поэтому в тесте на совпадение нужно исполь- зовать логический оператор отрицания ! как в предыдущем примере. И Двухмерные массивы Стандартом С определены многомерные массивы. Простейшая форма многомерного массива — двухмерный массив. Двухмерный массив — это массив одномерных массивов. Объявление двухмерного массива d с размерами 10 на 20 выглядит следующим образом: | int d[10][20]; Во многих языках измерения массива отделяются друг от друга запятой. В языке С каждое измерение заключено в свои квадратные скобки. Аналогично обращению к элементу одномерного массива, обращение к элементу с индексами 1 и 2 двухмерного массива d выглядит так: | d[l][2] В следующем примере элементам двухмерного массива присваиваются числа от 1 до 12 и значения элементов выводятся на экран построчно: #include <stdio.h> int main(void) { int t, i, num[3][4]; for(t=0; t<3; t++) for(i=0; i<4; i++) num[t][i] = (t*4)+i+l; /* вывод на экран */ for(t=0; t<3; t++) { for(i=0; i<4; i++) printf("%3d ”, num[t][i]); printf(”n"); } return 0; } В этом примере num[0] [0] имеет значение 1, num[0] [1] — значение 2, num[0] [2] — значение 3 и так далее. Наглядно двухмерный массив num можно пред- ставить так: 112 Часть I. Основы языка С
num[t][i] 0 12 3 0 1 2 3 4 2 5 6 7 8 3 9 10 11 12 Двухмерные массивы размещаются в матрице, состоящей из строк и столбцов. Первый индекс указывает номер строки, а второй — номер столбца. Это значит, что когда к элементам массива обращаются в том порядке, в котором они размещены в памяти, правый индекс изменяется быстрее, чем левый. На рис. 4.2 показано графи- ческое представление двухмерного массива в памяти. Объявление массива: char ch [3] [4] Правый индекс определяет номер столбца Рис. 4.2. Двухмерный массив Объем памяти в байтах, занимаемый двухмерным массивом, вычисляется по сле- дующей формуле: количество_байтов = = размер_1-го_измерения х размер_2-го_измерения х 81геоГ(базовый_тип) Например, двухмерный массив 4-байтовых целых чисел размерностью 10x5 зани- мает участок памяти объемом 10x5x4 то есть 200 байтов. Если двухмерный массив используется в качестве аргумента функции, то в нее переда- ется только указатель на начальный элемент массива. В соответствующем параметре функции, получающем двухмерный массив, обязательно должен быть указан размер пра- вого измерения1, который равен длине строки массива. Размер левого измерения указы- вать не обязательно. Размер правого измерения необходим компилятору для того, чтобы 1 Размер правого измерения указывать не нужно, если в вызывающей функции массив объ- явлен как **х и размещен динамически (см. главу 5). — Прим, перев. Глава 4. Массивы и строки 113
внутри функции правильно вычислить адрес элемента массива, так как для этого компиля- тор должен знать длину строки массива. Например, функция, получающая двухмерный массив целых размерностью 10x10, должна быть объявлена так: Ivoid fund (int х[][10] { /* ... */ } Компилятор должен знать длину строки массива, чтобы внутри функции правильно вычислить адрес элемента массива. Если при компиляции функции это неизвестно, то невозможно определить, где начинается следующая строка, и вычислить, напри- мер, адрес элемента | х[2][4] В следующем примере двухмерные массивы используются для хранения оценок сту- дентов. Предполагается, что преподаватель ведет три класса, в каждом из которых учится не более 30 студентов. Обратите внимание на то, как происходит обращение к массиву grade в каждой функции. /* Простая база данных оценок студентов */ #include <stdio.h> #include <ctype.h> #include <stdlib.h> #define CLASSES 3 #define GRADES 30 int grade[CLASSES][GRADES]; void enter_grades(void); int get_grade(int num); void disp_grades(int g[][GRADES]); int main(void) { char ch, str[80]; for(;;) { do { printf("(В)вод оценок студентовп"); printf("В(ы)вод оценок студентовп"); printf("Вы(х)од из программып"); gets(str); ch = toupper(*str); } while (ch! = ’B’ && ch! = ’bi’ && ch! = ’x’); switch(ch) { case ’B’: enter__grades () ; break; case ’ы’: disp_grades(grade) ; break; case ’x *: exit (0); } } 114 Часть I. Основы языка С
return 0; } /* Занесение оценок студентов в массив */ void enter_grades(void) { int t, i; for(t=0; t<CLASSES; t++) { printf (’’Класс № %d:n”, t+1); for(i=0; KGRADES; i++) grade[t][i] = get_grade(i) ; } } /* Ввод оценок */ int get_grade(int num) { char s [80]; printf (’’Введите оценку студента № %d:n”, num+1) ; gets (s); return(atoi(s)); } /* Вывод оценок */ void disp_grades(int g[][GRADES]) { int t, i; for(t=0; t<CLASSES; t++) { printf (’’Класс N’ %d:n", t+1); for(i=0; KGRADES; i++) printf (’’Студент N’ %d имеет оценку %dn", i+1, g[t][i]); } } Массивы строк В программах на языке С часто используются массивы строк. Например, сервер базы данных сверяет команды пользователей с массивом допустимых команд. В каче- стве массива строк в языке С служит двухмерный символьный массив. Размер левого измерения определяет количество строк, а правого — максимальную длину каждой строки. Например, в следующем операторе объявлен массив из 30 строк с максималь- ной длиной 79 символов: (char str_array[30] [80] ; Чтобы обратиться к отдельной строке массива, нужно указать только левый ин- декс. Например, вызов функции gets () с третьей строкой массива str_array в ка- честве аргумента можно записать так: | gets(str_array[2]); Этот оператор эквивалентен следующему: | gets(&str_array[2][0]); Из этих двух форм записи предпочтительной является первая. Глава 4. Массивы и строки 115
Для лучшего понимания свойств массива строк рассмотрим следующую корот- кую программу, в которой на основе применения массива строк создан простой текстовый редактор: /* Очень простой текстовый редактор */ ^include <stdio.h> #define MAX 100 tfdefine LEN 80 char text[MAX][LEN]; int main(void) { register int t, i, j; printf("Для выхода введите пустую строку.n”); for(t=0; t<MAX; t++) { printf("%d: ", t); gets(text[t]); if(!*text[t]) break; /* выход по пустой строке */ } for(i=0; i<t; i++) { for(j=0; text[i][j]; j++) putchar(text[i][j]); putchar(’n'); } return 0; } Пользователь вводит в программу строки текста, заканчивая ввод пустой строкой. Затем программа выводит текст посимвольно. Н Многомерные массивы В языке С можно пользоваться массивами, размерность которых больше двух. Об- щая форма объявления многомерного массива следующая: тип имя_массива [Размер 1][Размер2]...[РазмерМ]; Массивы, у которых число измерений больше трех, используются довольно редко, потому что они занимают большой объем памяти. Например, четырехмерный массив символов размерностью 10x6x9x4 занимает 2160 байтов. Если бы массив содержал 2- байтовые целые, потребовалось бы 4320 байтов. Если бы элементы массива имели тип double, причем каждый элемент (вещественное число двойной точности) занимал бы 8 байтов, то для хранения массива потребовалось бы 17 280 байтов. Объем требуемой памяти с ростом числа измерений растет экспоненциально. Например, если к преды- дущему массиву добавить пятое измерение, причем его толщину по этому измерению сделать равной всего 10, то его объем возрастет до 172 800 байтов. При обращении к многомерным массивам компьютер много времени затрачивает на вычисление адреса, так как при этом приходится учитывать значение каждого ин- декса. Поэтому доступ к элементам многомерного массива происходит значительно медленнее, чем к элементам одномерного. 116 Часть I. Основы языка С
Передавая многомерный массив в функцию, в объявлении параметров функции необходимо указать все размеры измерений, кроме самого левого. Например, если массив m объявлен как | int m[4][3][6][5]; то функция, принимающая этот массив, должна быть объявлена примерно так: Ivoid fund (int d [ ] [3] [ 6] [5]) : - - - Конечно, можно включить в объявление и размер 1-го измерения, но это излишне. Н Индексация указателей Указатели и массивы тесно связаны друг с другом. Имя массива без индекса — это указатель на первый (начальный) элемент массива. Рассмотрим, например, следую- щий массив: | char р[10]; Следующие два выражения идентичны: I Р I &Р[0] Выражение | р == &р[0] принимает значение ИСТИНА, потому что адрес 1-го элемента массива — это то же самое, что и адрес массива. Как уже указывалось, имя массива без индекса представляет собой указатель. И наоборот, указатель можно индексировать как массив. Рассмотрим следующий фраг- мент программы: Iint *р, i[10] ; р = i; р[5] = 100; /* в присваивании используется индекс */ *(р+5) = 100; /* в присваивании используется адресная арифметика */ Оба оператора присваивания заносят число 100 в 6-й элемент массива i. Первый из них индексирует указатель р, во втором применяются правила адресной арифмети- ки. В обоих случаях получается один и тот же результат. (Подробно указатели и ад- ресная арифметика рассматриваются в главе 5.) Можно также индексировать указатели на многомерные массивы. Например, если а — это указатель на двухмерный массив целых размерностью 10x10, то следующие два выражения эквивалентны: | &а[0] [0] Более того, к элементу (0, 4)1 можно обратиться двумя способами: либо указав ин- дексы массива: а [0] [4], либо с помощью указателя: * ((int*) а+4). Аналогично для элемента (1, 2): а [1] [2] или * ( (int*) а+12). В общем виде для двухмерного массива справедлива следующая формула: a[j][k] эквивалентно *((^озовый_/пмл*)а+()*(Элм«о_с?прокм)+к) 1 Так обозначается элемент, у которого первая координата равна 0, а вторая — 4. — Прим. ред. Глава 4. Массивы и строки 117
Правила адресной арифметики требуют явного преобразования указателя на мас- сив в указатель на базовый тип (см. главу 5). Указатели используются для обращения к элементам массива потому, что часто операции адресной арифметики выполняются быстрее, чем индексация массива. Двухмерный массив может быть представлен как указатель на массив одномерных массивов. Добавив еще один указатель, можно с его помощью обращаться к элемен- там отдельной строки массива. Этот прием демонстрируется в функции pr_row (), ко- торая печатает содержимое конкретной строки двухмерного глобального массива num: int num[10][10]; /* ... */ void pr_row(int j) { int *p, t; p = (int *) &num[j][0]; /* вычисление адреса 1-го элемента строки номер j */ for(t=0; t<10; t++) printf("%d ", *(p+t)); } Эту функцию можно обобщить, включив в список аргументов номер строки, длину строки и указатель на 1-й элемент: void row(int j, int row_dimension, int *p) { int t; p = p + (j * row_dimension); for(t=0; t<row_dimension; t++) printf("%d ", *(p+t)); } /* ... */ void f(void) { int num[10][10]; pr_row(0, 10, (int *) num); /* печать 1-й строки */ } Такой прием “понижения размерности” годится не только для двухмерных массивов, но и для любых многомерных. Например, вместо того, чтобы работать с трехмерным мас- сивом, можно использовать указатель на двухмерный массив, причем вместо него в свою очередь можно использовать указатель на одномерный массив. В общем случае вместо то- го, чтобы обращаться к «-мерному массиву, можно работать с указателем на (п-1)-мерный массив. Причем этот процесс понижения размерности кончается на одномерном массиве. Щ Инициализация массивов В языке С массивы при объявлении можно инициализировать. Общая форма ини- циализации массива аналогична инициализации переменной: тип имя_массива[размер!]...[размер^ = {список_значений} 118 Часть I. Основы языка С
Список значений представляет собой список констант, разделенных запятыми. Ти- пы констант должны быть совместимыми с типом массива. Первая константа при- сваивается первому элементу массива, вторая — второму и так далее. После закры- вающейся фигурной скобки точка с запятой обязательна. В С99 локальные массивы можно инициализировать не константами, а пере- менными, однако в С89 все массивы инициализируются только константами. На заметку В следующем примере массив целых из 10 элементов инициализируется числами от 1 до 10: | int i[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; Здесь элементу i [0] присваивается 1, a i [9] — 10. Символьные массивы, содержащие строки, можно инициализировать строковыми константами: char имя_массива[размер] = "строка"; В следующем примере массив str инициализируется фразой "Язык С": | char str[7] = "Язык С"; Это объявление можно записать так: | char str[7] = {’Я’, ’з', 'ы', ’ к’t ’ ’С’, * ’ }; Строка кончается нулевым символом, поэтому при объявлении необходимо зада- вать размер массива, достаточный для того, чтобы этот символ поместился в нем. В предыдущем примере размер строки задан равным 7, хотя во фразе "Язык С" содер- жится 6 символов. Если строка инициализируется строковой константой, компилятор автоматически добавляет нулевой символ в конец строки. Многомерные массивы инициализируются так же, как и одномерные. В следую- щем примере массив sqrs инициализируется числами от 1 до 10 и их квадратами: int sqrs[10][2] = { 1, 1, 2, 4, 3, 9, 4, 16, 5, 25, 6, 36, 7, 49, 8, 64, 9, 81, 10, 100 }; Инициализируя многомерный массив, для улучшения наглядности элементы ини- циализации каждого измерения можно заключать в фигурные скобки. Этот способ называется группированием подагрегатов (subaggregate grouping). С использованием этого приема предыдущий пример может быть записан так: int sqrs[10][2] = { {lz 1}, {2, 4}, {3, 9}, {4, 16}, {5, 25}, {6, 36}, {7, 49}, {8, 64}, Глава 4. Массивы и строки 119
|{9, 81}, {10, 100} При такой записи, если внутри группы недостаточно констант инициализации, то оставшиеся элементы группы автоматически заполняются нулями. Инициализация безразмерных массивов Предположим, что необходимо создать таблицу сообщений об ошибках, используя инициализацию массивов: Ichar el [15] = ’’Ошибка чтенияп”; char е2 [15] = ’’Ошибка записип"; char еЗ[21] = ’’Нельзя открыть файлп”; Для задания размера массива пришлось бы вручную подсчитывать количество симво- лов в каждом сообщении. Однако в языке С есть конструкция, благодаря которой компи- лятор автоматически определяет необходимую длину строки. Если в операторе инициали- зации массива не указан размер массива, компилятор создает массив такого размера, что в нем умещаются все инициализирующие элементы. Таким образом создается безразмерный массив. Используя этот метод, предыдущий пример можно записать так: Ichar el[] = ’’Ошибка чтенияп”; char е2[] = "Ошибка записип"; char еЗ[] = "Нельзя открыть файлп"; Тогда оператор | printf("%s имеет длину %dn", е2, sizeof е2); выведет на экран следующее: (Ошибка записи имеет длину 15 Кроме уменьшения трудоемкости, инициализация безразмерных массивов полезна тем, что позволяет изменять длину любого сообщения, не заботясь о соблюдении гра- ниц массивов. Инициализация безразмерных массивов поддерживается не только для одномер- ных массивов. В многомерном массиве размер самого левого измерения также можно не указывать. (Размеры по остальным измерениям обязательно должны быть указаны, так как это нужно компилятору для определения длины подмассивов, составляющих массив). Таким способом можно создавать таблицы переменного размера, компилятор автоматически выделит требуемую для них память. Например, объявление sqrs как безразмерного массива выглядит так: int sqrs[][2] = { {1, 1}, {2, 4}, {3, 9}, {4, 16}, {5, 25}, {6, 36}, {7, 49}, {8, 64}, {9, 81}, {10, 100} }; Преимущество безразмерного объявления массива состоит в том, что можно изме- нять длину таблицы, не заботясь о размере массива. 120 Часть I. Основы языка С
SI Массивы переменной длины Как уже упоминалось, в С89 размер массива должен быть объявлен с помощью константных выражений. Поэтому компилятор С89 устанавливает фиксированный размер массива, не изменяющийся в процессе выполнения программы. Однако это не относится к С99, в котором определено новое мощное средство: массивы переменной длины. Стандарт С99 позволяет в объявлении размера массива использовать любые выражения, в том числе такие, значение которых становится известным только во время выполнения. Объявленный таким образом массив называется массивом перемен- ной длины. Однако переменную длину могут иметь только локальные массивы (т.е. ви- димые в блоке или в прототипе). Приведем пример массива переменной длины: Ivoid f(int dim) { char str[dim]; /* символьный массив переменной длины */ /* ... */ } Здесь размер массива str определяется значением переменной dim, которая пере- дается в функцию f () как параметр. Таким образом, при каждом вызове f () создает- ся массив str разной длины. Массивы переменной длины добавлены в С99 главным образом для поддержки численных методов обработки данных. В программировании это средство распростра- нено достаточно широко. Однако следует помнить, что стандарт С89 (и некоторые компиляторы C++) не поддерживает массивы переменной длины. Более подробно этот вопрос рассматривается в части II. IS Приемы использования массивов и строк на примере игры в крестики-нолики Представленный длинный пример иллюстрирует большое количество приемов исполь- зования строк. Рассматривается простая программа игры в крестики-нолики. Двухмерный массив используется в качестве матрицы, изображающей игральную доску. Компьютер играет в очень простую игру. Когда наступает очередь хода компьюте- ра, функция get_computer_move () просматривает матрицу в поиске незанятых яче- ек. Если функция находит незанятую ячейку, она помещает туда символ О. Если не- занятой ячейки нет, то функция выводит сообщение об окончании игры и прекраща- ет работу программы. Функция get_player__move () спрашивает играющего, где он хочет поместить символ X. Верхний левый угол имеет координаты (1, 1), а нижний правый — (3, 3). Массив matrix, содержащий матрицу игры, инициализирован символами пробела. Каждый ход, сделанный игроком или компьютером, заменяет символ пробела симво- лом X или о. Это позволяет легко отобразить матрицу на экране. После каждого хода вызывается функция check (), которая возвращает пробел, ес- ли победителя еще нет, или X, если победил игрок, или о, когда победил компьютер. Эта функция просматривает строки, столбцы и диагонали в поиске трех одинаковых символов (х или О) подряд. Глава 4. Массивы и строки 121
Функция disp_matrix () отображает на экране текущее состояние игры. Обратите внимание на то, как существенно упрощает эту функцию инициализация матрицы пробелами. Функции получают доступ к массиву matrix различными способами. Их стоит внимательно изучить для лучшего понимания приемов работы с массивами. /* Простая игра в крестики-нолики. */ #include <stdio.h> #include <stdlib.h> char matrix[3][3]; /* матрица игры */ char check(void); void init_matrix(void); void get_player_move(void); void get_computer_move(void); void disp_matrix(void); int main(void) { char done; printf (’’ Это игра в крестики-нолики.n"); printf (’’ Вы будете играть против компьютера. п") ; done = ’ •; init_matrix(); do { disp_matrix(); get_player_move(); done = check(); /* проверка, есть ли победитель */ if(done!= ’ ’) break; /* есть победитель */ get_computer_move(); done = check(); /* проверка, есть ли победитель */ } while(done== ’ '); if(done == ’X’) printf("Вы победили!n"); else printf("Победил компьютер!!!n"); disp_matrix(); /* показ финальной позиции */ return 0; } /* Инициализация матрицы игры */ void init_matrix(void) { int i, j; for(i=0; i<3; i++) for(j=0; j<3; j++) matrix[i][j] = ’ ’; } /* Ход игрока. */ void get_player_move(void) { int x, y; printf("Введите координаты X,Y вашего хода: "); 122 Часть I. Основы языка С
scanf("%d%*c%d", &x, &y); x—; y—; if(matrix[x][y]!= ’ ’){ printf (’’Неверный ход, попытайтесь еще.п”); get_player_move () ; } else matrix[x][у] = ’X’; } /* Ход компьютера. */ void get_computer_move(void) { int i, j; for(i=0; i<3; i++){ for(j=0; j<3; j++) if(matrix[i][j]==’ ’) break; if(matrix[i][j]==’ •) break; /* Второй break нужен для выхода из цикла по i */ } if(i*j == 9) { printf (’’Конец игры.п’’); exit(0); } else matrix[i][j] = ’O’ ; } /* Вывод матрицы на экран. */ void disp_matrix(void) { int t ; for(t=0; t<3; t++) { printf(" %c | %c | %c ”, matrix[t] [0] , matrix[t][1], matrix[t][2]); if (t 1=2) printf (”n-|---I---n”) ; } printf(”n”); /* Определение победителя. */ char check(void) { int i; for(i=0; i<3;i++) /* проверка строк */ if(matrix[i][0] == matrix[i][1] && matrix[i][0] == matrix[i][2]) return matrix[i] [0] ; for(i=0; i<3; i++) /* проверка столбцов */ if(matrix[0][i] == matrix[l][i] && matrix[0][i] == matrix[2][i]) return matrix[0][i]; /* проверка диагоналей */ Глава 4. Массивы и строки 123
if(matrix[0][0] == matrix[1][1] && matrix[1][1] ~ matrix[2][2]) return matrix[0][0]; if(matrix[0][2] == matrix[l][1] && matrix[1][1] == matrix[2][0]) return matrix[0][2]; return ’ ’; Пояснение к программе. В функции get_player_move () с помощью библиотечной функции scanf () считываются с клавиатуры два целых числа х и у. Функция scanf () при считывании чисел предполагает, что во входном потоке они разделены пробелами (или пробельными символами), другие разделительные символы не допус- каются. Однако многие пользователи привыкли к тому, что числа можно разделять, например, запятыми. (Собственно говоря, именно так и предлагается в подсказке, выдаваемой программой.) В приведенном примере символ, следующий непосредст- венно после первого числа, просто игнорируется, именно для этого в функции scanf () используется спецификатор формата %*с. Звездочка означает, что символ считывается из потока, но в память не записывается1. 1 Пояснение добавлено редактором. — Прим. ред. 124 Часть I. Основы языка С
Полный справочник по Глава Указатели
Правильное понимание и использование указателей особенно необходимо для со- ставления хороших программ на языке С. И вот почему. Во-первых, указатели являются средством, с помощью которого функция может изменять значения переда- ваемых в нее аргументов. Во-вторых, с помощью указателей выполняется динамиче- ское распределение памяти. В-третьих, указатели позволяют повысить эффективность многих процедур. И наконец, они обеспечивают поддержку динамических структур данных, таких, например, как двоичные деревья и связные списки. Таким образом, указатели являются весьма мощным средством языка С. Но и весьма опасным. Например, если указатель содержит неправильное значение, про- грамма может потерпеть крах. Указатели весьма опасны еще и потому, что легко ошибиться при их использовании. К тому же ошибки, связанные с неправильными значениями указателей, найти очень трудно. Что такое указатели Указатель — это переменная, значением которой является адрес некоторого объек- та (обычно другой переменной) в памяти компьютера. Например, если одна перемен- ная содержит адрес другой переменной, то говорят, что первая переменная указывает (ссылается) на вторую. Это иллюстрируется с помощью рис. 5.1. Адрес ячейки Значение переменной в памяти Память Рис. 5.1. Одна переменная ссылается на другую Указательные переменные Как известно, переменную, являющуюся указателем, нужно соответствующим об- разом объявить. Объявление указателя состоит из имени базового типа, символа * и имени переменной. Общая форма объявления указателя следующая: тип *имя; Здесь тип — это базовый тип указателя, им может быть любой правильный тип. Имя определяет имя переменной-указателя. Базовый тип указателя определяет тип объекта, на который указатель будет ссы- латься. Фактически указатель любого типа может ссылаться на любое место в памяти. Однако выполняемые с указателем операции существенно зависят от его типа. На- пример, если объявлен указатель типа int *, компилятор предполагает, что любой 126 Часть I. Основы языка С
адрес, на который он ссылается, содержит переменную типа int, хоть это может быть и не так. Следовательно, объявляя указатель, необходимо убедиться, что его тип со- вместим с типом объекта, на который он будет ссылаться. S Операции для работы с указателями Операции для работы с указателями рассматривались в главе 2. Приведем их об- зор. В языке С определены две операции для работы с указателями: ★ и &. Оператор & — это унарный оператор, возвращающий адрес своего операнда. (Напомним, что унарный оператор имеет один операнд). Например, оператор | m = &count; присваивает переменной m адрес переменной count. Можно сказать, что адрес — это номер первого байта участка памяти, в котором хранится переменная. Адрес и значе- ние переменной — это совершенно разные понятия. Оператор & можно представить себе как оператор, возвращающий адрес объекта. Следовательно, предыдущий пример можно прочесть так: “переменной m присваивается адрес переменной count”. Предположим, переменная count хранится в ячейке памяти под номером 2000, а ее значение равно 100. Тогда переменной m будет присвоено значение 2000. Вторая операция для работы с указателями (ее знак, т.е. оператор, *) выполняет действие, обратное по отношению к &. Оператор * — это унарный оператор, возвра- щающий значение переменной, расположенной по указанному адресу. Например, ес- ли m содержит адрес переменной count, то оператор | q = *m; присваивает переменной q значение переменной count. Таким образом, q получит значение 100, потому что по адресу 2000 расположена переменная count, которая имеет значение 100. Действие оператора * можно выразить словами “значение по ад- ресу”, тогда предыдущий оператор может быть прочитан так: “q получает значение переменной, расположенной по адресу т”. В Указательные выражения В общем случае выражения с указателями подчиняются тем же правилам, что и обычные выражения. В этом разделе рассматривается применение указательных вы- ражений в операциях присваивания, преобразования типов, а также в операциях “указательной” арифметики. Присваивание указателей Указатель можно использовать в правой части оператора присваивания для присваива- ния его значения другому указателю. Если оба указателя имеют один и тот же тип, то вы- полняется простое присваивание, без преобразования типа. В следующем примере #include <stdio.h> int main(void) { int x = 99; int *pl, *p2; pl = &x; Глава 5. Указатели 127
р2 = pl; /* печать значения х дважды */ printf("Значения по адресу pl и р2 : %d %dn", *pl, *р2); /* печать адреса х дважды */ printf("Значения указателей pl и р2: %р %р", pl, р2); return 0; после присваивания Ipl = &х; р2 = pl; оба указателя (pl и р2) ссылаются на х. То есть, оба указателя ссылаются на один и тот же объект. Программа выводит на экран следующее: Значения по адресу pl и р2 : 99 99 Значения указателей pl и р2: 0063FDF0 0063FDF0 Обратите внимание, для вывода значений указателей в функции printf () исполь- зуется спецификатор формата %р, который выводит адреса в формате, используемом компилятором. Допускается присваивание указателя одного типа указателю другого типа. Однако для этого необходимо выполнить явное преобразование типа указателя (операция приведения типов), которая рассматривается в следующем разделе. Преобразование типа указателя Указатель можно преобразовать к другому типу. Эти преобразования бывают двух видов: с использованием указателя типа void * и без его использования. В языке С допускается присваивание указателя типа void * указателю любого другого типа (и наоборот) без явного преобразования типа указателя. Тип указателя void * используется, если тип объекта неизвестен. Например, использование типа void * в качестве параметра функции позволяет передавать в функцию указатель на объект любого типа, при этом сообщение об ошибке не генерируется. Также он поле- зен для ссылки на произвольный участок памяти, независимо от размещенных там объектов. Например, функция размещения mallocO (рассматривается далее в этой главе) возвращает значение типа void *, что позволяет использовать ее для размеще- ния в памяти объектов любого типа. В отличие от void *, преобразования всех остальных типов указателей должны быть всегда явными (т.е. должна быть указана операция приведения типов). Однако следует учитывать, что преобразование одного типа указателя к другому может вы- звать непредсказуемое поведение программы. Например, в следующей программе де- лается попытка присвоить значение х переменной у посредством указателя р. При компиляции программы сообщение об ошибке не генерируется, однако результат ра- боты программы неверен. #include <stdio.h> int main(void) { double x = 100.1, y; int *p; /* В следующем операторе указателю на целое р присваивается 128 Часть I. Основы языка С
значение, ссылающееся на double */ р = (int *) &х; /* Следующий оператор работает не так, как ожидается */ у = *р; /* Следующий оператор не выведет число 100.1 */ printf (’’значение х равно %f (Это не так!)”,у); return 0; } Обратите внимание на то, что операция приведения типов применяется в операто- ре присваивания адреса переменной х (он имеет тип double *) указателю р, тип ко- торого int *. Преобразование типа выполнено корректно, однако программа работа- ет не так, как ожидается (по крайней мере, в большинстве оболочек). Для разъясне- ния проблемы предположим, что переменная int занимает в памяти 4 байта, а double — 8 байтов. Указатель р объявлен как указатель на целую переменную (т.е. типа int), поэтому оператор присваивания I у = *р; передаст переменной у только 4 байта информации, а не 8 байтов, необходимых для double. Несмотря на то, что р ссылается на объект double, оператор присваивания выполнит действие с объектом типа int, потому что р объявлен как указатель на int. Поэтому такое использование указателя р неправильное. Приведенный пример подтверждает то, что операции с указателями выполняются в зависимости от базового типа указателей. Синтаксически допускается ссылка на объект с типом, отличным от типа указателя, однако при этом указатель будет “думать”, что он ссылается на объект своего типа. Таким образом, операции с указа- телями управляются типом указателя, а не типом объекта, на который он ссылается. Разрешен еще один тип преобразований: преобразование целого в указатель и на- оборот. В этом случае необходимо применить операцию приведения типов (явное пре- образование типа). Однако пользоваться этим средством нужно очень осторожно, пото- му что при этом легко получить непредсказуемое поведение программы. Явное преобра- зование типа не обязательно, если преобразуется нуль, то есть нулевой указатель. В языке C++ требуется явно указывать преобразование типа указателей, в том числе указателей типа void ★. Поэтому многие программисты исполь- зуют в языке С явное преобразование для совместимости с C++. На заметку Адресная арифметика В языке С допустимы только две арифметические операции над указателями: сум- мирование и вычитание. Предположим, текущее значение указателя pl типа int * равно 2000. Предположим также, что переменная типа int занимает в памяти 2 байта. Тогда после операции увеличения I р1++; указатель pl принимает значение 2002, а не 2001. То есть, при увеличении на 1 указа- тель pl будет ссылаться на следующее целое число. Это же справедливо и для опера- ции уменьшения. Например, если pl равно 2000, то после выполнения оператора I pi—; значение pl будет равно 1998. Глава 5. Указатели 129
Операции адресной арифметики подчиняются следующим правилам. После вы- полнения операции увеличения над указателем, данный указатель будет ссылаться на следующий объект своего базового типа. После выполнения операции уменьшения — на предыдущий объект. Применительно к указателям на char, операции адресной арифметики выполняются как обычные арифметические операции, потому что длина объекта char всегда равна 1. Для всех указателей адрес увеличивается или уменьшает- ся на величину, равную размеру объекта того типа, на который они указывают. По- этому указатель всегда ссылается на объект с типом, тождественным базовому типу указателя. Эта концепция иллюстрируется с помощью рис. 5.2. Операции адресной арифметики не ограничены увеличением (инкрементом) и уменьшением (декрементом). Например, к указателям можно добавлять целые числа или вычитать из них целые числа. Выполнение оператора | pl = pl + 12; “передвигает” указатель pl на 12 объектов в сторону увеличения адресов. char *ch = int *i = ( : (char int *) 3000 *) 3000; 3000; 3001 3002 ———- СП+1 w 14-1 3003 3004 1 ' -L ch+3 ► СП + 4 r < 14-2 3005 Память -L СПтЗ ж Рис. 5.2. Пример размещения в па- мяти переменных char (слева) и int (справа) Кроме суммирования и вычитания указателя и целого, разрешена еще только одна операция адресной арифметики: можно вычитать два указателя. Благодаря этому можно определить количество объектов, расположенных между адресами, на которые указывают данные два указателя; правда, при этом считается, что тип объектов совпа- дает с базовым типом указателей. Все остальные арифметические операции запреще- ны. А именно: нельзя делить и умножать указатели, суммировать два указателя, вы- полнять над указателями побитовые операции, суммировать указатель со значениями, имеющими тип float или double и т.д. Сравнение указателей Стандартом С допускается сравнение двух указателей. Например, если объявлены два указателя р и q, то следующий оператор является правильным: | if (р < q) printf("р ссылается на меньший адрес, чем q”); Как правило, сравнение указателей может оказаться полезным, только тогда, когда два указателя ссылаются на общий объект, например, на массив. В качестве примера рассмотрим программу с двумя стековыми функциями, предназначенными для записи и считывания целых чисел. Стек — это список, использующий систему доступа “первым вошел — последним вышел”. Иногда стек сравнивают со стопкой тарелок на 130 Часть I. Основы языка С
столе: первая, поставленная на стол, будет взята последней. Стеки часто используются в компиляторах, интерпретаторах, программах обработки крупноформатных таблиц и в других системных программах. Для создания стека необходимы две функции: push () и pop (). Функция push () заносит числа в стек, a pop () — извлекает их. В данном примере эти функции используются в main (). При вводе числа с клавиатуры, программа помещает его в стек. Если ввести 0, то число извлекается из стека. Про- грамма завершает работу при вводе -1. #include <stdio.h> #include <stdlib.h> #define SIZE 50 void push(int i); int pop(void); int *tos, *pl, stack[SIZE]; int main(void) { int value; tos = stack; /* tos ссылается на основание стека */ pl = stack; /* инициализация pl */ do { printf("Введите значение: "); scanf("%d”, &value); if(value != 0) push(value); else printf("значение на вершине равно %dn", рор()); } while(value != -1); return 0; } void push(int i) { pl++; if(pl == (tos+SIZE)) { printf("Переполнение стека.n"); exit(1); } *pl = i; } int pop(void) { if(pl == tos) { printf("Стек пуст.Хп"); exit(1); } pl—; return *(pl+l); } Стек хранится в массиве stack. Сначала указатели pl и tos устанавливаются на первый элемент массива stack. В дальнейшем pl ссылается на верхний элемент сте- Глава 5. Указатели 131
ка, a tos продолжает хранить адрес основания стека. После инициализации стека ис- пользуются функции push () и pop (). Они выполняют запись в стек и считывание из него, проверяя каждый раз соблюдение границы стека. В функции push () проверяет- ся, что указатель pl не превышает верхней границы стека tos+SlZE. Это предотвра- щает переполнение стека. В функции pop () проверяется, что указатель pl не выходит за нижнюю границу стека. В операторе return функции pop () скобки необходимы потому, что без них оператор | return *pl+l; вернул бы значение, расположенное по адресу pl, увеличенное на 1, а не значение по адресу pl+1. S Указатели и массивы Понятия указателей и массивов тесно связаны. Рассмотрим следующий фрагмент программы: Ichar str[80], *pl; pl = str; Здесь pl указывает на первый элемент массива str. Обратиться к пятому элементу массива str можно с помощью любого из двух выражений: Istr[4] *(pl+4) Массив начинается с нуля. Поэтому для пятого элемента массива str нужно ис- пользовать индекс 4. Можно также увеличить pl на 4, тогда он будет указывать на пя- тый элемент. (Напомним, что имя массива без индекса возвращает адрес первого эле- мента массива.) В языке С существуют два метода обращения к элементу массива: адресная ариф- метика и индексация массива. Стандартная запись массивов с индексами наглядна и удобна в использовании, однако с помощью адресной арифметики иногда удается со- кратить время доступа к элементам массива. Поэтому адресная арифметика часто ис- пользуется в программах, где существенную роль играет быстродействие. В следующем фрагменте программы приведены две версии функции putstrO, выводящей строку на экран. В первой версии используется индексация массива, а во второй — адресная арифметика: /* Индексация указателя s как массива */ void putstr(char *s) { register int t; for(t=0; s[t] ; ++t) putchar(s[t]); } /* Использование адресной арифметики */ void putstr(char *s) { while(*s) putchar(*s++); } Большинство профессиональных программистов сочтут вторую версию более на- глядной и удобной. Для большинства компиляторов она также более быстродейст- вующая. Поэтому в процедурах такого типа приемы адресной арифметики использу- ются довольно часто. 132 Часть I. Основы языка С
Массивы указателей Как и объекты любых других типов, указатели могут быть собраны в массив. В следующем операторе объявлен массив из 10 указателей на объекты типа int: | int *х[10]; Для присвоения, например, адреса переменной var третьему элементу массива указателей, необходимо написать: | х[2] = &var; В результате этой операции, следующее выражение принимает то же значение, что и var: | *х [2] Для передачи массива указателей в функцию используется тот же метод, что и для любого другого массива: имя массива без индекса записывается как формальный па^- раметр функции. Например, следующая функция может принять массив х в качестве аргумента: void display_array(int *q[]) { int t; for(t=0; t<10; t++ ) printf("%d ", *q[t]); } Необходимо помнить, что q — это не указатель на целые, а указатель на массив указателей на целые. Поэтому параметр q нужно объявить как массив указателей на целые. Нельзя объявить q просто как указатель на целые, потому что он представляет собой указатель на указатель. Массивы указателей часто используются при работе со строками. Например, можно написать функцию, выводящую нужную строку с сообщением об ошибке по индексу num: void syntax__error (int num) { static char *err[] = { "Нельзя открыть файлп", "Ошибка при чтениип", "Ошибка при записип", "Некачественный носительп" }; printf("%s", err[num]); } Массив err содержит указатели на строки с сообщениями об ошибках. Здесь стро- ковые константы в выражении инициализации создают указатели на строки. Аргумен- том функции printf () служит один из указателей массива err, который в соответст- вии с индексом num указывает на нужную строку с сообщением об ошибке. Напри- мер, если в функцию syntax_error () передается num со значением 2, то выводится сообщение Ошибка при записи. Отметим, что аргумент командной строки argv (см. главу 6) также является мас- сивом указателей на строковые константы. Глава 5. Указатели 133
В Многоуровневая адресация Иногда указатель может ссылаться на указатель, который ссылается на число. Это называется многоуровневой адресацией. Иногда применение таких указателей сущест- венно усложняет программу, делает ее плохо читаемой и подверженной ошибкам. Рис. 5.3 иллюстрирует концепцию многоуровневой адресации. На рисунке видно, что значением “нормального” указателя является адрес объекта, содержащего нужное значение. В случае двухуровневой адресации первый указатель содержит адрес второго указателя, который содержит адрес объекта с нужным значением. Многоуровневая адресация может иметь сколько угодно уровней, однако уровни глуб- же второго, т.е. указатели более глубокие, чем “указатели на указатели” применяются крайне редко. Дело в том, что при использовании таких указателей часто встречаются кон- цептуальные ошибки из-за того, что смысл таких указателей представить трудно. Не следует путать многоуровневую адресацию с многоуровневыми структу- рами данных, использующими указатели, такими, например, как связные спи- ски. Это фундаментально различные концепции. Переменная, являющаяся указателем на указатель, должна быть соответствующим образом объявлена. Это делается с помощью двух звездочек перед именем перемен- ной. Например, в следующем операторе newbalance объявлена как указатель на ука- затель на переменную типа float: | float **newbalance; Следует хорошо понимать, что newbalance — это не указатель на число типа float, а указатель на указатель на число типа float. Указатель На заметку Переменная Адрес -----------------► Значение Одноуровневая адресация Указатель Указатель Переменная Многоуровневая адресация Рис. 5.3. Одноуровневая и многоуровневая адресации При двухуровневой адресации для доступа к значению объекта нужно поставить перед идентификатором две звездочки: #include <stdio.h> int main(void) { int x, *p, **q; x = 20; 134 Часть I. Основы языка С
р = &х; q = &р; printf("%d", **q); /* печать значения х */ return 0; } Здесь р объявлена как указатель на целое, a q - как указатель на указатель на це- лое. Функция printf () выводит на экран число 10. В Инициализация указателей После объявления нестатического локального указателя до первого присвоения он содержит неопределенное значение. (Глобальные и статические локальные указатели при объявлении неявно инициализируются нулем.) Если попытаться использовать указатель перед присвоением ему нужного значения, то скорее всего он мгновенно разрушит программу или всю операционную систему. Это очень досадная ошибка. При работе с указателями большинство программистов придерживаются следую- щего важного соглашения: указатель, не ссылающийся в текущий момент времени должным образом на конкретный объект, должен содержать нулевое значение. Нуль используется потому, что С гарантирует отсутствие чего-либо по нулевому адресу. Следовательно, если указатель равен нулю, то это значит, во-первых, что он ни на что не ссылается, а во-вторых — что его сейчас нельзя использовать. Указателю можно задать нулевое значение, присвоив ему 0. Например, следующий оператор инициализирует р нулем: | char *р = 0; Дополнительно к этому во многих заголовочных файлах языка С, например, в <stdio.h> определен макрос NULL, являющийся нулевой указательной константой. Поэтому в программах на С часто можно увидеть следующее присваивание: | р = NULL; Однако равенство указателя нулю не делает его абсолютно “безопасным”. Исполь- зование нуля в качестве признака неподготовленности указателя — это только согла- шение программистов, но не правило языка С. В следующем примере компиляция пройдет без ошибки, а результат, тем не менее, будет неправильным: Iint *р = 0; *р = 10; /* ошибка! */ В этом случае присваивание посредством р будет присваиванием по нулевому ад- ресу, что обычно вызывает разрушение программы. Во многих процедурах для повышения эффективности программы можно исполь- зовать то, что нулевой указатель заведомо считается неподготовленным для использо- вания. Например, можно использовать нулевой указатель как признак конца массива указателей (по аналогии с нулевым терминатором строки). Процедура, использующая массив указателей, таким образом узнает о конце массива. Такой подход иллюстриру- ется в таком примере. Просматривая список имен, функция search () определяет, есть ли в этом списке заданное имя. |#include <stdio.h> #include <string.h> int search(char *p[], char *name); Глава 5. Указатели 135
char *names[] = { "Сергей", "Юрий", "Ольга", "Игорь", NULL}; /* Нулевая константа кончает список */ int main(void){ if(search(names, "Ольга") != -1) printf("Ольга есть в списке.n"); if(search(names, "Павел") == -1) printf("Павел в списке не найден.n"); return 0; } /* Просмотр имен */ int search(char *p[], char *name) { register int t; for(t=0; p[t]; ++t) if(!strcmp(p[t], name)) return t; return -1; /* имя не найдено */ } В функцию search () передаются два параметра. Первый из них, р — массив ука- зателей на строки, представляющие собой имена из списка. Второй параметр name является указателем на строку с заданным именем. Функция search () просматривает массив указателей, пока не найдет строку, совпадающую со строкой, на которую ука- зывает name. Итерации цикла for повторяются до тех пор, пока не произойдет совпа- дение имен, или не встретится нулевой указатель. Конец массива отмечен нулевым указателем, поэтому при достижении конца массива управляющее условие цикла примет значение ЛОЖЬ. Иными словами, р[t] имеет значение ЛОЖЬ, когда р[t] является нулевым указателем. В рассмотренном примере именно это и происходит, когда идет поиск имени “Павел”, которого в списке нет. В программах на С указатель типа char * часто инициализируют строковой кон- стантой (как в предыдущем примере). Рассмотрим следующий пример: | char *р = "тестовая строка"; Переменная р является указателем, а не массивом. Поэтому возникает логичный вопрос: где хранится строковая константа “тестовая строка”? Так как р не является массивом, она не может храниться в р, тем не менее, она где-то записана. Чтобы от- ветить на этот вопрос, нужно знать, что происходит, когда компилятор встречает строковую константу. Компилятор создает так называемую таблицу строк, в ней он сохраняет строковые константы, которые встречаются ему по ходу чтения текста программы. Следовательно, когда встречается объявление с инициализацией, компи- лятор сохраняет строку “тестовая строка” в таблице строк, а в указатель р записывает ее адрес. Дальше в программе указатель р может быть использован как любая другая строка. Это иллюстрируется следующим примером: |#include <stdio.h> #include <string.h> char *p = "тестовая строка"; 136 Часть I. Основы языка С
int main(void) { register int t; /* печать строки слева направо и справа налево */ printf(р); for(t=strlen(р)-1; t>-l; t—) printf("%c”, p[t]); return 0; } 111 Указатели на функции Указатели на функции1 — очень мощное средство языка С. Хотя нельзя не отме- тить, что это весьма трудный для понимания термин. Функция располагается в памя- ти по определенному адресу, который можно присвоить указателю в качестве его зна- чения. Адресом функции является ее точка входа. Именно этот адрес используется при вызове функции. Так как указатель хранит адрес функции, то она может быть вы- звана с помощью этого указателя. Он позволяет также передавать ее другим функци- ям в качестве аргумента. В программе на С адресом функции служит ее имя без скобок и аргументов (это похоже на адрес массива, который равен имени массива без индексов). Рассмотрим следующую программу, в которой сравниваются две строки, введенные пользователем. Обратите внимание на объявление функции check () и указатель р внутри main(). Указатель р, как вы увидите, является указателем на функцию. #include <stdio.h> #include <string.h> void check(char *a, char *b, int (*cmp)(const char *, const char *)); int main(void) { char si [80], s2[80]; int (*p)(const char *, const char *); /* указатель на функцию */ p = strcmp; /* присваивает адрес функции strcmp указателю р */ printf (’’Введите две строки. п”) ; gets (si); gets(s2); check(sl, s2, p); /* передает адрес функции strcmp посредством указателя р */ return 0; } 1 Иногда их называют просто указателями функций. Но следует помнить, что в языках про- граммирования под этим термином подразумевается также средство обращения к подпрограм- ме-функции или встроенной функции, имеющее конструкцию <имя-функции> (<список- аргументов>). — Прим. ред. Глава 5. Указатели 137
void check(char *a, char *b, int (*cmp)(const char ★, const char *)) { printf("Проверка на совпадение.n"); if(!(*cmp)(a, b)) printf("Равны"); else printf("He равны"); } Проанализируем эту программу подробно. В первую очередь рассмотрим объявле- ние указателя р в main (): | int (*р)(const char *,const char *); Это объявление сообщает компилятору, что р — это указатель на функцию, имеющую два параметра типа const char * и возвращающую значение типа int. Скобки вокруг р необходимы для правильной интерпретации объявления компилято- ром. Подобная форма объявления используется также для указателей на любые другие функции, нужно лишь внести изменения в зависимости от возвращаемого типа и па- раметров функции. Теперь рассмотрим функцию check (). В ней объявлены три параметра: два указа- теля на символьный тип (а и Ь) и указатель на функцию стр. Обратите внимание на то, что указатель функции стр объявлен в том же формате, что и р. Поэтому в стр можно хранить значение указателя на функцию, имеющую два параметра типа const char * и возвращающую значение int. Как и в объявлении р, круглые скобки вокруг *стр необходимы для правильной интерпретации этого объявления компилятором. Вначале в программе указателю р присваивается адрес стандартной библиотечной функции strcmp (), которая сравнивает строки. Потом программа просит пользовате- ля ввести две строки и передает указатели на них функции check (), которая их срав- нивает. Внутри check () выражение | (*стр) (а, Ь) вызывает функцию strcmp (), на которую указывает стр, с аргументами а и Ь. Скоб- ки вокруг *стр обязательны. Существует и другой, более простой, способ вызова функции с помощью указателя: | cmp(a f b); Однако первый способ используется чаще (и мы рекомендуем использовать имен- но его), потому что при втором способе вызова указатель стр очень похож на имя функции, что может сбить с толку читающего программу. В то же время у первого способа записи есть свои преимущества, например, хорошо видно, что функция вы- зывается с помощью указателя на функцию, а не имени функции. Следует отметить, что первоначально в С был определен именно первый способ вызова. Вызов функции check () можно записать, используя непосредственно имя strcmp(): | check(sl, s2, strcmp); В этом случае вводить в программу дополнительный указатель р нет необходимости. У читателя может возникнуть вопрос: какая польза от вызова функции с помощью указателя на функцию? Ведь в данном случае никаких преимуществ не достигнуто, этим мы только усложнили программу. Тем не менее, во многих случаях оказывается более выгодным передать имя функции как параметр или даже создать массив функ- ций. Например, в программе интерпретатора синтаксический анализатор (программа, анализирующая выражения) часто вызывает различные вспомогательные функции, такие как вычисление математических функций, процедуры ввода-вывода и т.п. В та- ких случаях чаще всего создают список функций и вызывают их с помощью индексов. 138 Часть I. Основы языка С
Альтернативный подход — использование оператора switch с длинным списком ме- ток case — делает программу более громоздкой и подверженной ошибкам. В следующем примере рассматривается расширенная версия предыдущей про- граммы. В этой версии функция check () устроена так, что может выполнять разные операции над строками si и s2 (например, сравнивать каждый символ с соответст- вующим символом другой строки или сравнивать числа, записанные в строках) в за- висимости от того, какая функция указана в списке аргументов. Например, строки “0123” и “123” отличаются, однако представляют одно и то же числовое значение. #include <stdio.h> tfinclude <ctype.h> tfinclude <stdlib.h> tfinclude <string.h> void check(char *a, char *b, int (*cmp)(const char *r const char *)); int compvalues(const char *a, const char *b); int main(void) { char si [80]f s2[80]; printf("Введите два значения или две строки.п"); gets (si); gets (s2); if(isdigit(*sl)) { printf("Проверка значений на равенство.n"); check(sl, s2, compvalues); } else { printf("Проверка строк на равенство.n"); check(sl, s2, strcmp); } return 0; } void check(char *a, char *b, int (*cmp)(const char *, const char *)) { if(!(*cmp)(a, b)) printf("Равны"); else printf("He равны"); } int compvalues(const char *a, const char *b) { if(atoi(a) == atoi(b)) return 0; else return 1; } Если в этом примере ввести первый символ первой строки как цифру, то check () использует compvalues (), в противном случае — strcmp (). Функция check () вызы- вает ту функцию, имя которой указано в списке аргументов при вызове check (), по- этому она в разных ситуациях может вызывать разные функции. Ниже приведены ре- зультаты работы этой программы в двух случаях: Глава 5. Указатели 139
Введите два значения или две строки, тест тест Проверка строк на равенство. Равны Введите два значения или две строки. 0123 123 Проверка значений на равенство. Равны Сравнение строк 01231 и 123 показывает равенство их значений. И функции динамического распределения Указатели используются для динамического выделения памяти компьютера для хранения данных. Динамическое распределение означает, что программа выделяет па- мять для данных во время своего выполнения. Память для глобальных переменных выделяется во время компиляции, а для нестатических локальных переменных — в стеке. Во время выполнения программы ни глобальным, ни локальным переменным не может быть выделена дополнительная память. Но довольно часто такая необходи- мость возникает, причем объем требуемой памяти заранее неизвестен. Такое случает- ся, например, при использовании динамических структур данных, таких как связные списки или двоичные деревья. Такие структуры данных при выполнении программы расширяются или сокращаются по мере необходимости. Для реализации таких струк- тур в программе нужны средства, способные по мере необходимости выделять и осво- бождать для них память. Память, выделяемая в С функциями динамического распределения данных, нахо- дится в т.н. динамически распределяемой области памяти (heap)1 2. Динамически распре- деляемая область памяти — это свободная область памяти, не используемая програм- мой, операционной системой или другими программами. Размер динамически рас- пределяемой области памяти заранее неизвестен, но как правило в ней достаточно памяти для размещения данных программы. Большинство компиляторов поддержи- вают библиотечные функции, позволяющие получить текущий размер динамически распределяемой области памяти, однако эти функции не определены в Стандарте С. Хотя размер динамически распределяемой области памяти очень большой, все же она конечна и может быть исчерпана. Основу системы динамического распределения в С составляют функции malloc () и free (). Эти функции работают совместно. Функция malloc () выделяет память, а free () — освобождает ее. Это значит, что при каждом запросе функция malloc () выделяет требуемый участок свободной памяти, a free() освобождает его, то есть возвращает системе. В программу, использующую эти функции, должен быть включен заголовочный файл <stdlib.h>. Прототип функции malloc () следующий: void *malloc(size_t количество_байпюв); 1 Обратите внимание, что в языке С нулем начинаются восьмеричные константы. Если бы эта запись была в выражении, то 0123 не было бы равно 123. Однако здесь функция atoi () об- рабатывает это число как десятичное. — Прим, перев. 2 Применяются и другие названия: динамическая область, динамически распределяемая об- ласть, куча, неупорядоченный массив (данных). — Прим. ред. ио Часть I. Основы языка С
Здесь количествО-байтов — размер памяти, необходимой для размещения данных. (Тип size_t определен в <stdlib.h> как некоторый целый без знака.) Функция mallocO возвращает указатель типа void *, поэтому его можно присвоить указателю любого типа. При успешном выполнении та 11 ос () возвращает указатель на первый байт непрерывного участка памяти, выделенного в динамически распределяемой области памяти. Если в ди- намически распределяемой области памяти недостаточно свободной памяти для выполне- ния запроса, то память не выделяется и malloc () возвращает нуль. При выполнении следующего фрагмента программы выделяется непрерывный уча- сток памяти объемом 1000 байтов: Ichar *р; р = malloc(1000); /* выделение 1000 байтов */ После присвоения указатель р ссылается на первый из 1000 байтов выделенного участка памяти. В следующем примере выделяется память для 50 целых. Для повышения мобильности (переносимости программы с одной машины на другую) используется оператор sizeof. Iint *р; р = malloc(50 * sizeof (int) ); Поскольку динамически распределяемая область памяти не бесконечна, при каждом размещении данных необходимо проверять, состоялось ли оно. Если malloc () не смогла по какой-либо причине выделить требуемый участок памяти, то она возвращает нуль. В следующем примере показано, как выполняется проверка успешности размещения: р = malloc(100); if(Ip) { printf("Нехватка памяти.") ; exit(1); } Конечно, вместо выхода из программы exit () можно поставить какой-либо обра- ботчик ошибки. Обязательным здесь можно назвать лишь требование не использовать указатель р, если он равен нулю. Функция free () противоположна функции mallocO в том смысле, что она воз- вращает системе участок памяти, выделенный ранее с помощью функции mallocO. Иными словами, она освобождает участок памяти, который может быть вновь исполь- зован функцией malloc (). Функция free () имеет следующий прототип: void free(void *р); Здесь р — указатель на участок памяти, выделенный перед этим функцией malloc (). Функцию free () ни в коем случае нельзя вызывать с неправильным аргу- ментом, это мгновенно разрушит всю систему распределения памяти. Подсистема динамического распределения в С используется совместно с указате- лями для создания различных программных конструкций, таких как связные списки и двоичные деревья. Несколько примеров использования таких конструкций приведены в части IV. Здесь рассматривается другое важное применение динамического разме- щения: размещение массивов. Динамическое выделение памяти для массивов Довольно часто возникает необходимость выделить память динамически, исполь- зуя mallocO, но работать с этой памятью удобнее так, будто это массив, который можно индексировать. В этом случае нужно создать динамический массив. Сделать это несложно, потому что каждый указатель можно индексировать как массив. В следую- щем примере одномерный динамический массив содержит строку: Глава 5. Указатели 141
/* Динамическое размещение строки, строка вводится пользователем, а затем распечатывается справа налево. */ #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { char *s; register int t; s = malloc(80) ; if(!s) { printf("Требуемая память не выделена.n"); exit(1); } gets (s); for(t=strlen(s)-1; t>=0; t—) putchar(s[t]); free (s); return 0; } Перед первым использованием s программа проверяет, успешно ли прошло выделение памяти. Эта проверка необходима для предотвращения случайного использования нуле- вого указателя. Обратите внимание на то, что указатель s используется в функции gets (), а также при выводе на экран (но на этот раз уже как,обыкновенный массив). Можно также динамически выделить память для многомерного массива. Для этого нужно объявить указатель, определяющий все, кроме самого левого измерения масси- ва. В следующем примере1 двухмерный динамический массив содержит таблицу чисел от 1 до 10 в степенях 1, 2, 3 и 4. #include <stdio.h> #include <stdlib.h> int pwr(int a, int b); int main(void) { /* Объявление указателя на массив из 10 строк в которых хранятся целые числа (int). */ int (*р)[10]; register int i, j; /* выделение памяти для массива 4x10 */ р = malloc(40*sizeof(int)); if(!р) { printf("Требуемая память не выделена.п"); exit(1); } 1 В примере динамически размещается только левое измерение массива. Однако это нетруд- но сделать и для всех измерений, объявив указатель **р и разместив каждое измерение отдель- но. Такой прием особенно удобен при написании функции, один из аргументов которой — двухмерный массив с неизвестными заранее размерами измерений. — Прим, перев. 142 Часть I. Основы языка С
for(j=l; jell; j++) for(i=l; i<5; i++) = pwr(j , p[i-l] [j-1] i) ; for(j=l; jell; j++) { for(i=l; i<5; i++) printf(”%10d ”, p [ i-1] [ j-1]); printf(”n”); } return 0; } /* Возведение чисел в степень. */ pwr(int a, int b) { register int t=l; for(; b; b—) t = t*a; return t; } Программа выводит на экран следующее: 1 1 1 1 2 4 8 16 3 9 27 81 4 16 64 256 5 25 125 625 6 36 216 1296 7 4 9 343 2401 8 64 512 4096 9 81 729 6561 10 100 1000 10000 Указатель р в главной программе (main ()) объявлен как | int (*р)[10]; Следует отметить, что скобки вокруг *р обязательны. Такое объявление означает, что р указывает на массив из 10 целых. Если увеличить указатель р на 1, то он будет указывать на следующие 10 целых чисел. Таким образом, р — это указатель на двух- мерный массив с 10 числами в каждой строке. Поэтому р можно индексировать как обычный двухмерный массив. Разница только в том, что здесь память выделена с по- мощью malloc (), а для обыкновенного массива память выделяет компилятор. Как упоминалось ранее, в C++ нужно преобразовывать типы указателей явно. Поэтому чтобы данная программа была правильной и в С, и в C++, необходимо выполнить явное приведение типа значения, возвращаемого функцией malloc (). Для этого строчку, в ко- торой указателю р присваивается это значение, нужно переписать следующим образом: | р = (int (*) [10]) malloc(40*sizeof(int)); Многие программисты используют явное преобразование типов указателей для обеспечения совместимости с C++. Ш Указатели с квалификатором restrict Стандарт С99 дополнительно вводит новый квалификатор типа restrict, приме- нимый только для указателей. Подробно этот спецификатор обсуждается в части II, здесь приведено только его краткое описание. Глава 5. Указатели 143
Если указатель объявлен с квалификатором restrict, то к объекту, на который он ссылается, можно обратиться только с помощью этого указателя. Обращение к объек- ту с помощью другого указателя возможно только в том случае, если другой указатель основан на первом. Таким образом, доступ к объекту можно получить только с помо- щью выражений, основанных на указателе с квалификатором restrict. Указатели restrict используются главным образом как параметры функции или совместно с malloc (). Если указатель объявлен с квалификатором restrict, компилятор спосо- бен лучше оптимизировать некоторые процедуры. Например, если два параметра функции определены как указатели с квалификатором restrict, то это сообщает компилятору о том, что они указывают на два разных (не пересекающихся) объекта. Квалификатор restrict не изменяет семантику программы. 110 Трудности при работе с указателями Ничто не может доставить больше неприятностей, чем “дикий” указатель! Указа- тели похожи на обоюдоострое оружие: их возможности огромны, однако обезвредить ошибки в них особенно трудно. Ошибочный указатель трудно найти потому, что ошибка в самом указателе никак себя не проявляет. Проблемы возникают при попытке обратиться к объекту с помо- щью этого указателя. Если значение указателя неправильное, то программа с его по- мощью обращается к произвольной ячейке памяти. При чтении в программу попада- ют неправильные данные, а при записи искажаются другие данные, хранящиеся в па- мяти, или портится участок программы, не имеющий никакого отношения к ошибочному указателю. В обоих случаях ошибка может не проявиться вовсе или про- явиться позже в форме, никак не указывающей на ее причину. Поскольку ошибки, связанные с указателями, особенно трудно обезвредить, при работе с указателями следует соблюдать особую осторожность. Рассмотрим некоторые ошибки, наиболее часто возникающие при работе с указателями. Классический при- мер — неинициализированный указатель'. /★ Эта программа содержит ошибку. ★/ int main(void) { int x, *p; x = 10; *p = x; /ошибка, p не инициализирован */ return 0; } Эта программа присваивает значение 10 некоторой неизвестной области памяти. Рассмотрим, почему это происходит. Хотя указателю р не было присвоено никакого значения, но в момент выполнения операции *р = х он имел некоторое (совершенно произвольное!) значение. Поэтому здесь имела место попытка выполнить операцию записи в область памяти, на которую указывал данный указатель. В небольших про- граммах такая ошибка часто остается незамеченной, потому что если программа и данные занимают немного места, то “выстрел наугад” скорее всего будет “промахом”. С увеличением размера программы вероятность “попасть” в нее возрастает. В таком простом случае большинство компиляторов выводят предупреждение о том, что используется неинициализированный указатель. Однако подобная ошибка может про- изойти и в более завуалированном виде, тогда компилятор не сможет распознать ее. Вторая распространенная ошибка заключается в простом недоразумении при ис- пользовании указателя: 144 Часть I. Основы языка С
/★ Эта программа содержит ошибку */ #include <stdio.h> int main(void) { int xr *p; x = 10; P = x; printf(”%d", *p); return 0; } Вызов printf () не выводит на экран значение х, равное 10. Выводится произ- вольная величина, потому что оператор | р = х; записан неправильно. Он присваивает значение 10 указателю, однако указатель дол- жен содержать адрес, а не значение. Правильный оператор выглядит так: | Р = &х; Большинство компиляторов при попытке присвоить указателю р значение х выве- дут предупреждающее сообщение, но, как и в предыдущем примере, компилятор не сможет распознать эту ошибку в более завуалированном виде. Еще одна типичная ошибка происходит иногда при неправильном понимании прин- ципов расположения переменных в памяти. Программисту ничего не известно о том, как используемые им данные располагаются в памяти, будут ли они расположены так же при следующем выполнении программы или как их расположат другие компиляторы. Поэтому сравнивать одни указатели с другими недопустимо. Например, программа char s [80], у[80]; char *pl, *р2; pl = s; р2 = у; if(pl < р2)... в общем случае неправильна. (В некоторых необычных ситуациях иногда определяют относительное положение переменных, но это делают очень редко.) Похожая ошибка возникает, когда делается необоснованное предположение о рас- положении массивов. Иногда, предполагая, что массивы расположены рядом, пыта- ются обращаться к ним с помощью одного и того же указателя, например: Iint first[10], second[10]; int *p, t; p = first; for(t=0; t<20; t++) *p++ = t; Так присваивать значения массивам first и second нельзя. Если компилятор разместит массивы рядом, это может и не привести к неправильному результату. Од- нако подобная ошибка особенно неприятна тем, что при проверке она может остаться незамеченной, а потом компилятор будет размещать массивы по-другому и программа выполнится неправильно. В следующей программе приведен пример очень опасной ошибки. Постарайтесь сами найти ее, не подсматривая в последующее объяснение. I/* Это программа с ошибкой */ #include <string.h> Глава 5. Указатели 145
^include <stdio.h> int main(void) { char *pl; char s[80] ; pl = s; do { gets(s); /* чтение строки */ /* печать десятичного эквивалента каждого символа */ while(*pl) printf(" %d", *pl++); }while(strcmp(sr "выполнено")); return 0; } Программа печатает значения символов ASCII, находящихся в строке s. Печать осуществляется с помощью pl, указывающего на s. Ошибка состоит в том, что указа- телю pl присвоено знач^ц^ только один раз, перед циклом. В первой итерации pl правильно проходит по символам строки s, однако в следующей итерации он начина- ет не с первого символа, а с того, которым закончил в предыдущей итерации. Так что во второй итерации pl мож^т указывать на середину второй строки, если она длиннее первой, или же вообще/йд конец остатка первой строки. Исправленная версия про- граммы записывается так: /* Это правильная программа */ ^include <string.h> ^include <stdio.h> int main(void) { char *pl; char s [80]; do { pl = s; /* установка pl в начало строки s */ gets(s); /* чтение строки */ /* печать десятичного эквивалента каждого символа */ while(*pl) printf(" %d", *pl++); }while(strcmp(s, "выполнено"));. return 0; } При такой записи указатель pl в начале каждой итерации устанавливается на пер- вый символ строки s. Об этом необходимо всегда помнить при повторном использо- вании указателей. То, что неправильные указатели могут быть очень “коварными”, не может служить причиной отказа от их использования. Следует лишь быть осторожным и вниматель- но проанализировать каждое применение указателя в. программе. 146 Часть I. Основы языка С
Полный справочник по Глава 6 Функции
ункции — это строительные элементы языка С и то место, в котором выполня- т ется вся работа программы. В этой главе изучаются свойства функций, в том числе их аргументы, возвращаемые значения, прототипы, а также рекурсия. Ш Общий вид функции В общем виде функция выглядит следующим образом: в о звр-тип имя-функции(список параметров) { тело функции } в о звр-тип определяет тип данного, возвращаемого функцией1. Функция может воз- вращать любой тип данных, за исключением массивов список параметров — это спи- сок, элементы которого отделяются друг от друга запятыми. Каждый такой элемент состоит из имени переменной и ее типа данных. При вызове функции параметры принимают значения аргументов. Функция может быть и без параметров, тогда их список будет пустым. Такой пустой список можно указать в явном виде, поместив для этого внутри скобок ключевое слово void. В объявлениях (декларациях) переменных можно объявить (декларировать) не- сколько переменных одного и того же типа, используя для этого список одних только имен, элементы которого отделены друг от друга запятыми. А все параметры функ- ций, наоборот, должны объявляться отдельно, причем для каждого из них надо указы- вать и тип, и имя. То есть в общем виде список объявлений параметров должен вы- глядеть следующим образом: f(mun имяпеременной 1, тип имяпеременной2,..., тип имяпеременнойТУ) Вот, например, два объявления параметров функций, первое из которых правиль- ное, а второе — нет: If(int i, int k, int j) /* правильное */ f(int i, k, float j) /* неправильное, у переменной k должен быть собственный спецификатор типа */ В Что такое область действия функции В языке правила работы с областями действия — это правила, которые определя- ют, известен ли фрагменту кода другой фрагмент кода или данных, или имеет ли он доступ к этому другому фрагменту. Об областях действия, определяемых в языке С, говорилось в главе 2. Здесь же мы более подробно рассмотрим одну специальную об- ласть действия — ту, которая определяется функцией. Каждая функция представляет собой конечный блок кода. Таким образом, она опре- деляет область действия этого блока. Это значит, что код функции является закрытым и недоступным ни для какого выражения из любой другой функции, если только не вы- полняется вызов содержащей его функции. (Например, нельзя перейти в середину дру- гой функции с помощью goto.) Код, который составляет тело функции, скрыт от ос- тальной части программы, и если он не использует глобальных переменных, то не мо- жет воздействовать на другие части программы или, наоборот, подвергаться воздействию 1 Данное, возвращаемое функцией, называется также результатом. Соответственно, возвра- щаемый тип часто называется также типом результата. — Прим. ред. 148 Часть I. Основы языка С
с их стороны. Иначе говоря, код и данные, определенные внутри одной функции, без глобальных переменных не могут воздействовать на код и данные внутри другой функ- ции, так как у любых двух разных функций разные области действия. Переменные, определенные внутри функции, являются локальными. Локальная переменная создается в начале выполнения функции, а при выходе из этой функции она уничтожается. Таким образом, локальная переменная не может сохранять свое значение в промежутках между вызовами функции. Единственное исключение из этого правила — переменные, объявленные со спецификатором класса памяти static. Таким переменным память выделяется так же, как и глобальным перемен- ным, которые используются для хранения значений, но область действия таких пере- менных ограничена содержащими их функциями. (Дополнительная информация о локальных и глобальных переменных приведена в главе 2.) Формальные параметры функции также находятся в ее области действия. Это зна- чит, что параметр доступен внутри всей функции. Параметр создается в начале вы- полнения функции, и уничтожается при выходе из нее. Все функции имеют файл в качестве области действия (file scope). Таким образом, функцию нельзя определять внутри другой функции. Поэтому С практически не яв- ляется языком с блочной структурой. Н Аргументы функции Если функция должна принимать аргументы, то в ее объявлении следует деклари- ровать параметры, которые примут значения этих аргументов. Как видно из объявле- ния следующей функции, объявления параметров стоят после имени функции. /* Возвращает 1, если символ с входит в строку з, и 0 в противном случае. */ int is_in(char *s, char с) { while(*s) if(*s==c) return 1; else s++; return 0; } Функция is_in () имеет два параметра: s и d. Если символ с входит в строку s, то эта функция возвращает 1, в противном случае она возвращает 0. Хотя параметры выполняют специальную задачу, — принимают значения аргумен- тов, передаваемых функции, — они все равно ведут себя так, как и другие локальные переменные. Формальным параметрам функции, например, можно присваивать ка- кие-либо значения или использовать эти параметры в каких-либо выражениях. Вызовы по значению и по ссылке В языках программирования имеется два способа передачи значений подпрограм- ме. Первый из них — вызов по значению. При его применении в формальный пара- метр подпрограммы копируется значение аргумента. В таком случае изменения пара- метра на аргумент не влияют. Вторым способом передачи аргументов подпрограмме является вызов по ссылке. При его применении в параметр копируется адрес аргумента. Это значит, что, в отли- чие от вызова по значению, изменения значения параметра приводят к точно таким же изменениям значения аргумента. Глава 6. Функции 149
За небольшим количеством исключений, в языке С для передачи аргументов использу- ется вызов по значению. Обычно это означает, что код, находящийся внутри функции, не может изменять значений аргументов, которые использовались при вызове функции. Проанализируйте следующую программу: #include <stdio.h> int sqr(int x); int main(void) { int t=10; printf("%d %d", sqr(t), t); return 0; } int sqr(int x) { x = x * x ; return(x); } В этом примере в параметр х копируется 10 — значение аргумента для sqr (). Когда выполняется присваивание х=х*х, модифицируется только локальная переменная х. А значение переменной t, использованной в качестве аргумента при вызове sqr(), по- прежнему остается равным 10. Поэтому выведено будет следующее: 100 10. Помните, что именно копия значения аргумента передается в функцию. А то, что происходит внутри функции, не влияет на значение переменной, которая была ис- пользована при вызове в качестве аргумента. Вызов по ссылке Хотя в С для передачи параметров применяется вызов по значению, можно создать вызов и по ссылке, передавая не сам аргумент, а указатель на него1. Так как функции передается адрес аргумента, то ее внутренний код в состоянии изменить значение этого аргумента, находящегося, между прочим, за пределами самой функции. Указатель передается функции так, как и любой другой аргумент. Конечно, в та- ком случае параметр следует декларировать как один из типов указателей. Это можно увидеть на примере функции swap (), которая меняет местами значения двух целых переменных, на которые указывают аргументы этой функции: | void swap(int *х, int *у) I { I int temp; | temp = *x; /* сохранить значение по адресу х */ 1 Конечно, при передаче указателя будет применен вызов по значению, и сам указатель внутри функции вы изменить не сможете. Однако для того объекта, на который указывает этот указатель, все произойдет так, будто этот объект был передан по ссылке. В некоторых языках программирования (например, в Алголе-60) имелись специальные средства, позволяющие уточ- нить, как следует передавать аргументы: по ссылке или по значению. Благодаря наличию указа- телей в С механизм передачи параметров удалось унифицировать. Параметры, не являющиеся массивами, в С всегда вызываются только по значению, но все, что в других языках вы можете сделать с объектом, получив ссылку на него (т.е. его адрес), вы можете сделать, получив значе- ние указателя на этот объект (т.е. опять же, его адрес). Так что в языке С благодаря свойствен ной ему унификации передачи параметров никаких проблем не возникает. А вот в других язы- ках трудности, связанные с отсутствием эффективных средств работы с указателями, встречают- ся довольно часто. — Прим. ред. 150 Часть I. Основы языка С
|*х = *у; /* поместить у в х */ * у = temp; /* поместить х в у */ } Функция swap () может выполнять обмен значениями двух переменных, на кото- рые указывают х и у, потому что передаются их адреса, а не значения. Внутри функ- ции, используя стандартные операции с указателями, можно получить доступ к со- держимому переменных и провести обмен их значений1. Помните, что swap () (или любую другую функцию, в которой используются па- раметры в виде указателей) необходимо вызывать вместе с адресами аргументов1 2. Сле- дующая программа показывает, как надо правильно вызывать swap (): #include <stdio.h> void swap(int *x, int *y); int main(void) { int i, j; i = 10; j = 20; printf ("i и j перед обменом значениями: %d %dn°, i, j); swap(&i, &j); /* передать адреса переменных i и j */ printf("i и j после обмена значениями: %d %dn", i, j); return 0; } void swap(int *x, int *y) { int temp; temp = *x; /* сохранить значение, хранящееся по адресу х */ * х = *у; /* поместить значение у в х */ * у = temp; /* поместить (старое) значение х в у */ } И вот что вывела эта программа: Ii и j перед обменом значениями: 10 20 i и j после обмена значениями: 20 10 1 Конечно, задача, решаемая этой программой, кажется тривиальной. Ну разве представляет трудность написать на каком-либо процедурном языке, например, на Алголе-60, процедуру, ко- торая обменивает значения своих параметров. Ведь так просто написать: procedure swap(x, у); integer х, у; begin integer t; t:= x; x:=y; y:=t end. Но эта процедура работает неправильно, хотя вы- зов значений здесь происходит по ссылке! Причем сразу найти тестовый пример, демонстри- рующий ошибочность этой процедуры, удается далеко не всем. Ведь в случае вызова swap(i, j) все работает правильно! А что будет в случае вызова swap(i, a[i])? Да и можно ли на Алголе-60 вообще написать требуемую процедуру? Если вы склоняетесь к отрицательному ответу, то это показывает, насколько все-таки необходимы указатели в развитых языках программирования. Если все же вы знаете правильный ответ, то обратите внимание на то, что требуемая процеду- ра, хотя и не длинная, но все же содержит своего рода программистский фокус! — Прим. ред. 2 Конечно, это просто программистский жаргон. На самом деле, конечно, аргументами яв- ляются именно адреса переменных, а не сами переменные. Просто в этом случае для краткости изложения программисты “делают вид”, что вроде бы и в самом деле происходит передача зна- чений по ссылке. — Прим. ред. Глава 6. Функции 151
В программе переменной i присваивается значение 10, а переменной j — значе- ние 20. Затем вызывается функция swap () с адресами этих переменных. (Для получе- ния адреса каждой из переменных используется унарный оператор &.) Поэтому в swap () передаются адреса переменных i и j, а не их значения. Язык C++ при помощи параметров-ссылок дает возможность полностью ав- томатизировать вызов по ссылке. А в языке С параметры-ссылки не под- держиваются. Вызов функций с помощью массивов Подробно о массивах рассказывалось в главе 4. В настоящем же разделе рассказы- вается о передаче массивов функциям в качестве аргументов. Этот вопрос рассматри- вается потому, что эта операция является исключением по отношению к обычной пе- редаче параметров, выполняемой путем вызова по значению1. Когда в качестве аргумента функции используется массив, то функции передается его адрес. В этом и состоит исключение по отношению к правилу, которое гласит, что при передаче параметров используется вызов по значению. В случае передачи массива функции ее внутренний код работает с реальным содержимым этого массива и вполне может изменить это содержимое. Проанализируйте, например, функцию print_upper (), которая печатает свой строковый аргумент на верхнем регистре: #include <stdio.h> #include <ctype.h> void print_upper(char *string); int main(void) { char s [80]; printf("Введите строку символов: "); gets (s); print_upper(s); printf("ns теперь на верхнем регистре: %s", s); /* Печатать строку на верхнем регистре */ void print_upper(char *string) { register int t; for(t=0; string[t]; ++t) { stringft] = toupper(string[t]); putchar(string[t]); } Вот что будет выведено в случае фразы “This is a test.” (это тест): I Введите строку символов: This is a test. I THIS IS A TEST. I s теперь в верхнем регистре: THIS IS A TEST. (Правда, эта программа не работает с символами кириллицы. — Прим, перев.) 1 Ведь при вызове по значению пришлось бы копировать весь массив! — Прим. ред. 152 Часть I. Основы языка С
После вызова print_upper() содержимое массива s в main() переводится в сим- волы верхнего регистра. Если вам это не нужно, программу можно написать следую- щим образом: ftinclude <stdio.h> #include <ctype.h> void print_upper(char *string); int main(void) { char s [80]; printf("Введите строку символов: "); gets (s); print_upper(s); printf(”ns не изменилась: %s", s); return 0; } void print_upper (char *string) ;1) { register int t; for(t=0; string[t]; ++t) putchar(toupper(string[t])); } Вот какой на этот раз получится фраза “This is a test.”: Введите строку символов: This is a test. THIS IS A TEST. s не изменилась: This is a test. На этот раз содержимое массива не изменилось, потому что внутри print_upper () не изменялись его значения. Классическим примером передачи массивов в функции является стандартная биб- лиотечная функция gets (). Хотя gets(), которая находится в вашей стандартной библиотеке, и более сложная, чем предлагаемая вам версия xgets (), но с помощью функции, xgets () вы сможете получить представление о том, как работает gets (). /* Упрощенная версия стандартной библиотечной функции gets() */ char *xgets(char *s) { char ch, *p; int t; p = s; /* xgets() возвращает указатель s */ for(t=0; t<80; ++t){ ch = getchar(); switch(ch) { case 'n': s[t] = ’ 1; /* завершить строку */ return p; case ' b' : if(t>0) t— ; break; Глава 6. Функции 153
default: s[t] = ch; } } s[79] = fOf; return p; } Функцию xgets () следует вызывать с указателем char *. Им, конечно же, может быть имя символьного массива, которое по определению является указателем char *. В самом начале программы xgets () выполняется цикл for от 0 до 80. Это не даст вводить с клавиатуры строки, содержащие более 80 символов. При попытке ввода большего количества символов происходит возврат из функции. (В настоящей функ- ции gets () такого ограничения нет.) Так как в языке С нет встроенной проверки границ, программист должен сам позаботиться, чтобы в любом массиве, используе- мом при вызове xgets (), помещалось не менее 80 символов. Когда символы вводятся с клавиатуры, они сразу записываются в строку. Если пользователь нажимает клавишу <Backspase>, то счетчик t уменьшается на 1, а из массива удаляется последний сим- вол, введенный перед нажатием этой клавиши. Когда пользователь нажмет <ENTER>, в конец строки запишется нуль, т.е. признак конца строки. Так как массив, использо- ванный для вызова xgpts ()., модифицируется, то при возврате из функции в нем бу- дут находиться введенные пользователем символы. Н Аргументы функции main(): argv и argc Иногда при запуске программы бывает полезно передать ей какую-либо информа- цию. Обычно такая информация передается функции main() с помощью аргументов командной строки. Аргумент командной строки — это информация, которая вводится в командной строке операционной системы вслед за именем программы. Например, чтобы запустить компиляцию программы, необходимо в командной строке после под- сказки набрать примерно следующее: сс имя_программы имя_программы представляет собой аргумент командной строки, он указывает имя той программы, которую вы собираетесь компилировать. Чтобы принять аргументы командной строки, используются два специальных встроенных аргумента: argc и argv. Параметр argc содержит количество аргументов в командной строке и является целым числом, причем он всегда не меньше 1, потому что первым аргументом считается имя программы. А параметр argv является указате- лем на массив указателей на строки. В этом массиве каждый элемент указывает на какой-либо аргумент командной строки. Все аргументы командной строки являются строковыми, поэтому преобразование каких бы то ни было чисел в нужный двоичный формат должно быть предусмотрено в программе при ее разработке. Вот простой пример использования аргумента командной строки. На экран выводятся слово Привет и ваше имя, которое надо указать в виде аргумента командной строки. tfinclude <stdio.h> tfinclude <stdlib.h> int main(int argc, char *argv[]) { if(argc!=2) { printf(”Вы забыли ввести свое имя.п”); exit(1); } 154 Часть I. Основы языка С
I printf (’’Привет, %s”, argv[l]); return 0; } Если вы назвали эту программу name (имя) и ваше имя Том, то для запуска про- граммы следует в командную строку ввести name Том. В результате выполнения про- граммы на экране появится сообщение Привет, Том. Во многих средах все аргументы командной строки необходимо отделять друг от друга пробелом или табуляцией. Запятые, точки с запятой и тому подобные символы разделителями не считаются. Например, | run Spot, run состоит из трех символьных строк, в то время как | Эрб,Рик,Фред представляет собой одну символьную строку — запятые, как правило, разделителями не считаются. Если в строке имеются пробелы, то, чтобы из нее не получилось несколько аргу- ментов, в некоторых средах эту строку можно заключать в двойные кавычки. В ре- зультате вся строка будет считаться одним аргументом. Чтобы подробнее узнать, как в вашей операционной системе задаются параметры командной строки, изучите доку- ментацию этой системы. Очень важно правильно объявлять argv. Вот как это делают чаще всего: | char *argv[]; Пустые квадратные скобки указывают на то, что у массива неопределенная длина. Теперь получить доступ к отдельным аргументам можно с помощью индексации мас- сива argv. Например, argv[0] указывает на первую символьную строку, которой всегда является имя программы; argv [1] указывает на первый аргумент и так далее. Другим небольшим примером использования аргументов командной строки явля- ется приведенная далее программа countdown (счет в обратном порядке). Эта про- грамма считает в обратном порядке, начиная с какого-либо значения (указанного в командной строке), и подает звуковой сигнал, когда доходит до 0. Обратите внима- ние, что первый аргумент, содержащий начальное значение, преобразуется в целое значение с помощью стандартной функции atoi(). Если вторым аргументом ко- мандной строки (а если считать аргументом имя программы, то третьим — Прим, пе- рев.) является строка “display” (вывод на экран), то результат отсчета (в обратном по- рядке) будет выводиться на экран. /* Программа счета в обратном порядке */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> int main(int argc, char *argv[]) { int disp, count; if(argc<2) { printf (’’В командной строке необходимо ввести число, с которогоп”); printf (’’начинается отсчет. Попробуйте Снова. п") ; exit (1); } Глава 6. Функции 155
if(argc==3 && !strcmp(argv[2], "display”)) disp = 1; else disp = 0; for(count=atoi(argv[1]); count; —count) if(disp) printf("%dn", count); putchar('a'); /* здесь подается звуковой сигнал */ printf("Счет закончен"); return 0; } Обратите внимание, если аргументы командной строки не будут указаны, то будет выведено сообщение об ошибке. В программах с аргументами командной строки часто делается следующее: в случае, когда пользователь запускает эти программы без ввода нужной информации, выводятся инструкции о том, как пра- вильно указывать аргументы. Чтобы получить доступ к отдельному символу одного из аргументов командной строки, введите в argv второй индекс. Например, следующая программа посимвольно выводит все аргументы, с которыми ее вызвали: ttinclude <stdio.h> int main(int argc, char *argv[]) { int t, i; for(t=0; t<argc; ++t) { i = 0; while(argv[t][i]) { putchar(argv[t][i]); } printf("n"); } return 0; } Помните, первый индекс argv обеспечивает доступ к строке, а второй индекс — доступ к ее отдельным символам. Обычно а где и argv используют для того, чтобы передать программе началь- ные команды, которые понадобятся ей при запуске. Например, аргументы ко- мандной строки часто указывают такие данные, как имя файла, параметр или альтернативное поведение. Использование аргументов командной строки придает вашей программе “профессиональный внешний вид” и облегчает ее использова- ние в пакетных файлах. Имена а где и argv являются традиционными, но не обязательными. Эти два параметра в функции main() вы можете назвать как угодно. Кроме того, в неко- торых компиляторах для main() могут поддерживаться дополнительные аргумен- ты, поэтому обязательно изучите документацию к вашему компилятору. Когда для программы не требуются параметры командной строки, то чаще всего явно декларируют функцию main() как не имеющую параметров. В таком случае в списке параметров этой функции используют ключевое слово void. 156 Часть I. Основы языка С
H Оператор return Механизм использования return описан в главе 3. Как вы помните, там говорит- ся, что этот оператор имеет два важных применения. Во-первых, он обеспечивает не- медленный выход из функции, т.е. заставляет выполняющуюся программу передать управление коду, вызвавшему функцию. Во-вторых, этот оператор можно использо- вать для того, чтобы возвратить значение. В следующих разделах рассказывается, ка- ким именно образом можно использовать оператор return. Возврат из функции Функция может завершать выполнение и осуществлять возврат в вызывающую программу двумя способами. Первый способ используется тогда, когда после выпол- нения последнего оператора в функции встречается закрывающая фигурная скобка (}). (Конечно, это просто жаргон, ведь в настоящем объектном коде фигурной скобки нет!) Например, функция pr_reverse() в приведенной ниже программе просто вы- водит на экран в обратном порядке строку Мне нравится С, а затем возвращает управление вызывающей программе. #include <string.h> #include <stdio.h> void pr_reverse(char *s); int main(void) { pr_reverse("Мне нравится С"); return 0; } void pr_reverse(char *s) { register int t; for(t=strlen(c)-1; t>=0; t—) putchar(s[t]); } Как только строка выведена на экран, функции pr_reverse() “делать больше нечего”, поэтому она возвращает управление туда, откуда она была вызвана. Но на практике не так уж много функций используют именно такой способ за- вершения выполнения. В большинстве функций для завершения выполнения исполь- зуется оператор return — или потому, что необходимо вернуть значение, или чтобы сделать код функции проще и эффективнее. В функции может быть несколько операторов return. Например, в следующей программе функция f ind_substr () возвращает начальную позицию подстроки в строке или же возвращает —1, если подстрока, наоборот, не найдена. В этой функции для упрощения кодирования используются два оператора return. tfinclude <stdio.h> int find_substr(char *sl, char *s2); int main(void) { Глава 6. Функции 157
if(find—Substr("С — это забавно", "это") 1= -1) printf("Подстрока найдена."); return 0; } /* Вернуть позицию первого вхождения s2 в si. */ int find_substr(char *sl, char *s2) { register int t;' char *p, *p2; for(t=0; sl[t]; t++) { p = & s 1 [ t ] ; p2 = s2; while(*p2 && *p2==*p) { P++; p2++; } if(!*p2) return t; /* 1-й оператор return */ } return -1; /* 2-й оператор return */ } Возврат значений Все функции, кроме тех, которые относятся к типу void, возвращают значение. Это значение указывается выражением в операторе return. Стандарт С89 допускает выполне- ние оператора return без указания выражения внутри функции, тип которой отличен от void. В этом случае все равно происходит возврат какого-нибудь произвольного значения. Но такое положение дел, мягко говоря, никуда не годится! Поэтому в Стандарте С99 (да и в C++) предусмотрено, что в функции, тип которой отличен от void, в операторе return необходимо обязательно указать возвращаемое значение. То есть, согласно С99, если для какой-либо функции указано, что она возвращает значение, то внутри этой функции у любого оператора return должно быть свое выражение. Однако если функция, тип кото- рой отличен от void, выполняется до самого конца (то есть до закрывающей ее фигурной скобки), то возвращается произвольное (непредсказуемое с точки зрения разработчика программы!) значение. Хотя здесь нет синтаксической ошибки, это является серьезным упущением и таких ситуаций необходимо избегать. Если функция не объявлена как имеющая тип void, она может использоваться как операнд в выражении. Поэтому каждое из следующих выражений является правильным: |х = power(у); if(max(х,у) > 100) printf("больше"); for(ch=getchar(); isdigit(ch); ) Общепринятое правило гласит, что вызов функции не может находиться в левой части оператора присваивания. Выражение | swap(х,у) = 100; /* неправильное выражение */ является неправильным. Если компилятор С в какой-либо программе найдет такое выражение, то пометит его как ошибочное и программу компилировать не будет. В программе можно использовать функции трех видов. Первый вид — простые вычисления. Эти функции предназначены для выполнения операций над своими ар- гументами и возвращают полученное в результате этих операций значение. Вычисли- 158 Часть I. Основы языка С
тельная функция является функцией “в чистом виде”. В качестве примеров можно назвать стандартные библиотечные функции sqrt() и sin(), которые вычисляют квадратный корень и синус своего аргумента соответственно. Второй вид включает в себя функции, которые обрабатывают информацию и воз- вращают значение, которое показывает, успешно ли была выполнена эта обработка. Примером является библиотечная функция f close О, которая закрывает файл. Если операция закрытия была завершена успешно, функция возвращает 0, а в случае ошибки она возвращает EOF. У функций последнего, третьего вида нет явно возвращаемых значений. В сущно- сти, такие функции являются чисто процедурными и никаких значений выдавать не должны. Примером является exit О, которая прекращает выполнение программы. Все функции, которые не возвращают значение, должны объявляться как возвра- щающие значение типа void. Объявляя функцию как возвращающую значение типа void, вы запрещаете ее применение в выражениях, предотвращая таким образом слу- чайное использование этой функции не по назначению. Иногда функции, которые, казалось бы, фактически не выдают содержательный результат, все же возвращают какое-то значение. Например, printf () возвращает ко- личество выведенных символов. Если бы нашлась такая программа, которая на самом деле проверяла бы это значение, то это было бы что-то необычное... Другими слова- ми, хотя все функции, за исключением относящихся к типу void, возвращают значе- ния, вовсе не нужно стремиться использовать эти значения во что бы то ни стало. Часто при обсуждении значений, возвращаемых функциями, возникает такой доволь- но распространенный вопрос: “Неужели не обязательно присваивать возвращенное значение какой-либо переменной? Не повиснет ли оно где-нибудь и не приведет ли это в дальнейшем к каким-либо неприятностям?” Отвечая на этот вопрос, повторим, что присваивание отнюдь не является обязательным, причем отсутствие его не станет причиной каких-либо неприятностей. Если возвращаемое значение не входит ни в один из операторов присваивания, то это значение будет просто отброшено. Отметим также, что такое отбрасывание значения встречается очень часто. Проанализируйте следующую программу, в которой используется функция mul (): #include <stdio.h> int mul(int a, int b) ; int main(void) { int x, y, z; x = 10; у = 20; z = mul(x,y); /* 1 */ printf("%d", mul(x,y)); /* 2 */ mul(x,y); /* 3 */ return 0; } int mul(int a, int b) { return a*b; } В строке 1 значение, возвращаемое функцией mul (), присваивается переменной z. В строке 2 возвращаемое значение не присваивается, но используется функцией printf (). И наконец, в строке 3 возвращаемое значение теряется, потому что не присваивается ни- какой из переменных и не используется как часть какого-либо выражения. Глава 6. Функции 159
Возвращаемые указатели Хотя с функциями, которые возвращают указатели, обращаются так же, как и с любы- ми другими функциями, все же будет полезно познакомиться с некоторыми основными понятиями и рассмотреть соответствующий пример. Указатели не являются ни целыми, ни целыми без знака. Они являются адресами в памяти и относятся к особому типу дан- ных. Такая особенность указателей определяется тем, что арифметика указателей (адресная арифметика) работает с учетом параметров базового типа. Например, если указателю на целое придать минимальное (ненулевое) приращение, то его текущее значение станет на четыре больше, чем предыдущее (при условии, что целые значения занимают 4 байта). Во- обще говоря, каждый раз, когда значение указателя увеличивается (уменьшается) на ми- нимальную величину, то он указывает на последующий (предыдущий) элемент, имеющий базовый тип указателя. Так как размеры разных типов данных могут быть разными, то компилятор должен знать тип данных, на которые может указывать указатель. Поэтому в объявлении функции, которая возвращает указатель, тип возвращаемого указателя должен декларироваться явно. Например, нельзя объявлять возвращаемый тип как int *, если возвращается указатель типа char *! Иногда (правда, крайне редко!) требуется, чтобы функция возвращала “универсальный” указатель, т.е. указатель, который может указывать на данные любого типа. Тогда тип результата функции следует определить как void *. Чтобы функция могла возвратить указатель, она должна быть объявлена как возвра- щающая указатель на нужный тип. Например, следующая функция возвращает указа- тель на первое вхождение символа, присвоенного переменной с, в строку s. Если этого символа в строке нет, то возвращается указатель на символ конца строки ( 'О'). /* Возвращает указатель на первое вхождение с в s. */ char *match(char с, char *s) { while(c!=*s && *s) s++; return (s) ; } Вот небольшая программа, в которой используется функция match (): #include <stdio.h> char *match(char c, char *s); /* прототип */ int main(void) { char s [80]t *p, ch; • gets(s); ch = getchar(); p = match(ch, s); if(*p) /* символ найден */ printf(”%s ”, p); else printf("Символа нет."); return 0; } Эта программа сначала считывает строку, а затем символ. Потом проводится поиск местонахождения символа в строке. При наличии символа в строке переменная р укажет на него, и программа выведет строку, начиная с найденного символа. Если символ в строке не найден, то р укажет на символ конца строки ( 'О' ), причем *р бу- дет представлять логическое значение ЛОЖЬ (false). В таком случае программа выве- дет сообщение Символа нет. 160 Часть I. Основы языка С
Функции типа void Одним из применений ключевого слова void является явное объявление функций, которые не возвращают значений. Мы уже знаем, что такие функции не могут при- меняться в выражениях, и указание ключевого слова void предотвращает их случай- ное использование не по назначению. Например, функция print_vertical () выво- дит в боковой части экрана свой строчный аргумент по вертикали сверху вниз. void print_vertical(char *str) { while(*str) printf("%cn"t *str++); } Вот пример использования функции print_vertical (): #include <stdio.h> void print—vertical(char *str); /* прототип */ int main(int argc, char *argv[]) { if(argc > 1) print_vertical(argv[1]); return 0; } void print—vertical(char *str) z { while(*str) printf("%cn", *str++); } И еще одно замечание: в ранних версиях С ключевое слово void не определялось. Таким образом, в программах, написанных на этих версиях С, функции, которые не возвращали значений, просто имели по умолчанию тип int — и это несмотря на то, что они не возвращали никаких значений! 1 Что возвращает функция main()? Функция main () возвращает целое число, которое принимает вызывающий про- цесс — обычно этим процессом является операционная система. Возврат значения из main () эквивалентен вызову функции exit () с тем же самым значением. Если main () не возвращает значение явно, то вызывающий процесс получает формально неопределенное значение. На практике же большинство компиляторов С автоматиче- ски возвращают 0, но если встает вопрос переносимости, то на такой результат пола- гаться с уверенностью нельзя. S Рекурсия В языке С функция может вызывать сама себя. В этом случае такая функция назы- вается рекурсивной. Рекурсия — это процесс определения чего-либо на основе самого себя, из-за чего рекурсию еще называют рекурсивным определением. Глава 6. Функции 161
Простым примером рекурсивной функции является factr О, которая вычисляет фак- ториал целого неотрицательного числа. Факториалом числа п (обозначается п! — Прим, пе- рев.) называется произведение всех целых чисел, от 1 до п включительно (для 0, по опреде- лению, факториал равен 1. — Прим, перев.). Например, 3! — это 1x2x3, или 6. Здесь пока- заны factr () и эквивалентная ей функция, в которой используется итерация: /* рекурсивная функция */ int factr(int n) { int answer; if(n==0) return(l); answer = factr(n-1)*n; /* рекурсивный вызов */ return(answer); /* нерекурсивная функция */ int fact(int n) { int t, answer; answer = 1; for(t=l; t<=n; t++) answer=answer*(t); return(answer); Нерекурсивное вычисление факториала, то есть вычисление с помощью fact (), выполняется достаточно просто. В этой функции в теле цикла, выполняющемся для t от 1 до п, вычисленное ранее произведение последовательно умножается на каждое из этих чисел. (Значение факториала для 0 получается, конечно, с помощью оператора присваивания. Значение факториала для 1 также получается умножением не на ранее полученное произведение, а на заранее подготовленное число, тоже равное 1. — Прим, перев.) Работа же рекурсивной функции factr () чуть более сложная. Когда factr () вы- зывается с аргументом 0, то она сразу возвращает 1. Если же аргумент больше 0, то возвращается произведение factr (п-1) *п. Чтобы вычислить значение этого выраже- ния, factr () вызывается с аргументом п-1. Это выполняется до тех пор, пока п не станет равным 0. Когда это произойдет, вызовы функции начнут возвращать вычис- ленные ими значения факториалов. При вычислении 2! первый вызов factr () влечет за собой второй, теперь уже ре- курсивный вызов с аргументом 1, который, в свою очередь, влечет третий, тоже ре- курсивный вызов с аргументом 0. Этот вызов возвращает число 1, которое затем ум- ножается на 1, а потом на 2 (первоначальное значение п). Ответ в данном случае ра- вен 2. Попробуйте самостоятельно вычислить 3!. (Вам, возможно, захочется вставить в функцию factr () выражения printf (), чтобы видеть уровень каждого вывода, и то, какие будут промежуточные ответы.) Когда функция вызывает сама себя, новый набор локальных переменных и пара- метров размещается в памяти в стеке, а код функции выполняется с самого своего на- чала, причем используются именно эти новые переменные. При рекурсивном вызове функции новая копия ее кода не создается. Новыми являются только значения, кото- рые использует данная функция. При каждом возвращении из рекурсивного вызова старые локальные переменные и параметры извлекаются из стека, и сразу за рекур- сивным вызовом возобновляется работа функции. При использовании рекурсивных функций стек работает подобно “телескопической” трубе, выдвигающейся вперед и складывающейся обратно. 162 Часть I. Основы языка С
Хотя и кажется, что рекурсия предлагает более высокую эффективность, но на са- мом деле такое бывает достаточно редко. Использование рекурсии в программах за- частую не очень сильно уменьшают их размер кода и обычно только незначительно увеличивает эффективность использования памяти. Кроме того, рекурсивные версии большинства программ могут выполняться несколько медленнее, чем их итеративные варианты, потому что при рекурсивных вызовах функций расходуются дополнитель- ные ресурсы. Кроме того, большое количество рекурсивных вызовов функции может вызвать переполнение стека. Из-за того, что память для параметров функции и ло- кальных переменных находится в стеке и при каждом новом вызове создается еще один набор этих переменных, то для переменных места в стеке может рано или позд- но не хватить. Переполнение стека — вот обычная причина аварийного завершения программы, когда функция утрачивает контроль над рекурсивными обращениями. Главным преимуществом рекурсивных функций является то, что с их помощью упрощается реализация некоторых алгоритмов, а программа становится понятнее. На- пример, алгоритм быстрой сортировки (описанный в части IV) трудно реализовать итеративным способом. Кроме того, для некоторых проблем, особенно связанных с искусственным интеллектом, больше подходят рекурсивные решения. И наконец, не- которым людям легче думать рекурсивными категориями, чем итеративными. В тексте рекурсивной функции обязательно должен быть выполнен условный опе- ратор, например if, который при определенных условиях вызовет завершение функ- ции, т.е. возврат, а не выполнит очередной рекурсивный вызов. Если такого операто- ра нет, то после вызова функция никогда не сможет завершить работы. Распростра- ненной ошибкой при написании рекурсивных функций как раз и является отсутствие в них условного оператора. При создании программ не отказывайтесь от функции printf (); тогда вы сможете увидеть, что происходит на самом деле и сможете пре- рвать выполнение, когда обнаружите ошибку. Г"""**! И Прототипы функций В современных, правильно написанных программах на языке С каждую функцию перед использованием необходимо объявлять. Обычно это делается с помощью про- тотипа функции, В первоначальном варианте языка С прототипов не было; но они были введены уже в Стандарт С89. Хотя прототипы формально не требуются, но их использование очень желательно. (Впрочем, в C++ прототипы обязательны'.) Во всех примерах этой книги имеются полные прототипы функций. Прототипы дают компи- лятору возможность тщательнее выполнять проверку типов, подобно тому, как это де- лается в таких языках как Pascal. Если используются прототипы, то компилятор мо- жет обнаружить любые сомнительные преобразования типов аргументов, необходи- мые при вызове функции, если тип ее параметров отличается от типов аргументов. При этом будут выданы предупреждения обо всех таких сомнительных преобразова- ниях. Компилятор также обнаружит различия в количестве аргументов, использован- ных при вызове функции, и в количестве параметров функции. В общем виде прототип функции должен выглядеть таким образом: тип имя_функции(тип имяпарам!, тип имя_парам2, имя_парам№); Использование имен параметров не обязательно. Однако они дают возможность компилятору при наличии ошибки указать имена, для которых обнаружено несоответ- ствие типов, так что не поленитесь указать этих имен — это позволит сэкономить время впоследствии. Следующая программа показывает, насколько ценными являются прототипы функций. В ней выводится сообщение об ошибке, происходящей из-за того, что программа содержит попытку вызова sqr_it() с целым аргументом, в то время как требуется указатель на целое. Глава 6. Функции 163
/★ В этой программе используется прототип функции, чтобы обеспечить тщательную проверку типов. */ void sqr_it(int *i); /* прототип */ int main(void) { int x; x = 10; sqr_it(x); /* несоответствие типов */ return 0; } void sqr_it(int *i) { *i = *i * *i; } В качестве прототипа функции может также служить ее определение, если оно на- ходится в программе до первого вызова этой функции. Вот, например, правильная программа: #include <stdio.h> /* Это определение будет также служить и прототипом внутри этой программы. */ void f(int a, int b) { printf("%d ”, a % b) ; } int main(void) { f (10,3); return 0; } В этом примере специальный прототип не требуется; так как функция f () определена еще до того, как она начинает использоваться в main (). Хотя определение функции и мо- жет служить ее прототипом в малых программах, но в больших такое встречается редко — особенно, когда используется несколько файлов. В программах, приведенных в качестве примеров в этой книге, для каждой функции автор старался приводить отдельный прото- тип потому, что именно так обычно и пишется код на языке С. Единственная функция, для которой не требуется прототип — это main (), так как это первая функция, вызываемая в начале работы программы. Имеется небольшая, но важная разница в том, как именно в С и C++ обрабатыва- ется прототип функции, не имеющей параметров. В C++ пустой список параметров указывается полным отсутствием в прототипе любых параметров. Например, | int f(); /* Прототип в C++ для функции, не имеющей параметров */ Однако в С это выражение означает нечто другое. Из-за необходимости придержи- ваться совместимости с первоначальной версией С пустой список параметров сообща- ет, что просто о параметрах не предоставлено никакой информации. Что касается ком- пилятора, то для него эта функция может иметь несколько параметров, а может не иметь ни одного. (Такой оператор называется старомодным объявлением функции, он описан в следующем разделе.) 164 Часть I. Основы языка С
Если функция в языке С не имеет параметров, то в ее прототипе внутри списка параметров стоит только ключевое слово void. Вот, например, прототип функции f () в том виде, в каком он должен быть в программе на языке С: | float f(void); Таким образом компилятор узнает, что у функции нет параметров, и любое обра- щение к ней, в котором имеются аргументы, будет считаться ошибкой. В C++ ис- пользование ключевого слова void внутри пустого списка параметров также разреше- но, но считается излишним. Прототипы функций позволяют “отлавливать” ошибки еще до запуска программы. Кроме того, они запрещают вызов функций при несовпадении типов (т.е. с неподхо- дящими аргументами) и тем самым помогают проверять правильность программы. И напоследок хотелось бы сказать следующее: так как в ранних версиях С синтаксис прототипов в полном объеме не поддерживался, то в С прототипы формально не обяза- тельны. Такой подход необходим для совместимости с С-кодом, созданным еще до по- явления прототипов. Но если старый С-код переносится в C++, то перед компиляцией этого кода в него необходимо добавить полные прототипы функций. Помните, что хотя прототипы в С не обязательны, но они обязательны в C++. Это значит, что каждая функция в программе на языке C++ должна иметь полный прототип. Поэтому при на- писании программ на С в них указываются полные прототипы функций — именно так поступает большинство программистов, работающих на этом языке. Старомодные объявления функций В “ранней молодости” языка С, еще до создания прототипов функций, все-таки была необходимость сообщить компилятору о типе результата функции, чтобы при вызове функции был создан правильный код. (Так как размеры разных типов данных разные, то размер типа результата надо было знать еще до вызова функции.) Это вы- полнялось с помощью объявления функции, не содержащего никакой информации о параметрах. С точки зрения теперешних стандартов этот старомодный подход являет- ся архаичным. Однако его до сих пор можно найти в старых кодах. По этой причине важно понимать, как он работает. Согласно старомодному подходу, тип результата и имя функции, как показано ни- же, объявляются почти что в начале программы: ttinclude <stdio.h> double div(); /* старомодное объявление функции */ int main(void) { printf("%f", div(10.2, 20.0)); return 0; } double div(double num, double denom) { return num / denom; } Старомодное объявление типа функции сообщает компилятору, что функция div () возвращает результат типа double. Это объявление позволяет компилятору правильно генерировать код для вызовов этой функции. Однако оно ничего не гово- рит о параметрах div (). Общий вид старомодного оператора объявления функции такой: Глава 6. Функции 165
спецификатор_типа имя_функции(); Обратите внимание, что список параметров пустой. Даже если функция принимает аргументы, то ни один из них не перечисляется в объявлении типа. Как уже говорилось, старомодное объявление функции устарело и не должно ис- пользоваться в новом коде. Кроме того, оно несовместимо с C++. Прототипы стандартных библиотечных функций Любая стандартная библиотечная функция в программе должна иметь прототип. Поэтому для каждой такой функции необходимо ввести соответствующий заголовок. Все необходимые заголовки предоставляются компилятором С. В системе программи- рования на языке С библиотечными заголовками (обычно) являются файлы, в именах которых используется расширение ,ь. В заголовке имеется два основных элемента: любые определения, используемые библиотечными функциями, и прототипы библио- течных функций. Например, почти во все программы из этой книги включается файл <stdio.h>, потому что в этом файле находится прототип для printf (). Заголовки для стандартных функций описаны в части II. в Объявление списков параметров переменной длины Можно вызвать функцию, которая имеет переменное количество параметров. Са- мым известным примером является printf (). Чтобы сообщить компилятору, что функции будет передано заранее неизвестное количество аргументов, объявление спи- ска ее параметров необходимо закончить многоточием. Например, следующий прото- тип указывает, что у функции f unc () будет как минимум два целых параметра и по- сле них еще некоторое количество (в том числе и 0) параметров: | int func(int a, int b, ...) ; В любой функции, использующей переменное количество параметров, должен быть как минимум один реально существующий параметр. Например, следующее объявление неправильное: | int func (...); /* ошибка */ Правило “неявного int” Первоначальная версия С отличалась особенностью, которую иногда называют прави- лом “неявного int” (а также правилом “int по умолчанию”). Это правило состоит в том, что если спецификатор базового типа явно не указан, то подразумевается спецификатор int. Это правило было включено в стандарт С89, но в С99 это правило не вошло. (И, кроме того, не поддерживается в языке C++.) Так как правило “неявного int” теперь ус- тарело, то в этой книге оно не используется. Однако из-за того, что во многих действую- щих программах это правило еще используется, то о нем следует немного рассказать. Больше всего правило “неявного int” использовалось при определении типа ре- зультата функции, т.е. при определении возвращаемого типа. Много лет назад боль- шинство программистов, писавших программы на С, пользовались этим правилом, когда писали код функций, возвращавших результат типа int. Поэтому многд лет на- зад такая функция, как 166 Часть I. Основы языка С
Iint f(void) { /* ... */ return 0; } часто могла быть написана таким образом: If(void) { /* int — возвращаемый тип по умолчанию */ /* ... */ return 0; } В первом случае возвращаемый тип int определяется явно. Во втором же — под- разумевается по умолчанию. Правило “неявного int” применяется не только к значениям, возвращаемым функциями (хотя это было самое распространенное применение). Например, для С89 и более ранних вариантов С правильной является следующая функция: /* по умолчанию возвращается тип int, такой же тип, как и у параметров а и Ъ */ f(register a, register b) { register с; /* переменная с по умолчанию также будет иметь тип int */ с = а + b; printf("%d", с); return с; } Здесь возвращаемым типом по умолчанию для f () является int; т.е. такой же тип по умолчанию, как и у параметров а и Ь, и у локальной переменной с. Помните, что правило “неявного int” не поддерживается в С99 или C++. Таким образом, использовать его в программах, совместимых с С89, не рекомендуется. Луч- ше всего явно определять каждый тип, используемый в вашей программе. В Старомодные и современные объявления параметров функций В ранних версиях С использовался синтаксис объявления параметров, отличаю- щийся от того, который используется в современных версиях этого языка, включая С89, С99 и C++. Такой ранний синтаксис иногда называется классическим. В отличие от него, синтаксис, который используется в этой книге, называется современным. В стандартном С поддерживаются оба синтаксиса, но настоятельно рекомендуется со- временный. (А в C++ поддерживается только современный синтаксис объявления па- раметров.) Однако старомодный синтаксис надо знать, потому что он до сих пор ис- пользуется во многих старых программах, написанных на языке С. Старомодное объявление параметров функции состоит из двух частей: списка па- раметров внутри круглых скобок, следующих за именем функции, а также объявлений параметров, находящихся между закрывающей круглой скобкой и открывающей фи- гурной скобкой функции. В общем виде старомодное определение параметров должно выглядеть таким образом: тип имя_функции(парм1, парм2,...пармИ) тип парм1; Глава 6. Функции 167
тип парм2; тип napMN; { код функции } Например, такое современное объявление, как I float f(int a, int b, char ch) { /* ... */ } в старомодном виде будет выглядеть следующим образом: float f(a, b, ch) int a, b; char ch; { /* ... */ } Обратите внимание, что старомодный синтаксис позволяет в списке, стоящем за именем типа, объявить более одного параметра. Старомодный синтаксис объявления параметров признан устаревшим для стандартного С и не поддерживается в языке C++. На заметку В Ключевое слово inline В Стандарте С99 было введено ключевое слово inline, применяемое к функциям. Оно подробно рассматривается в части II, а здесь мы дадим только его краткое опи- сание. Ставя перед объявлением функции ключевое слово inline, вы даете компиля- тору указание оптимизировать вызовы этой функции. Обычно это означает, что такие вызовы желательно заменить последовательной вставкой кода самой функции. Однако inline является всего лишь запросом к компилятору и может быть проигнорировано. На заметку Спецификатор inline также поддерживается в языке C++. 168 Часть I. Основы языка С
т-т V Полный справочник по Глава Структуры, объединения, перечисления и декларация typedef
К языке С имеется пять способов создания пользовательских типов данных. Поль- зовательские типы данных можно создать с помощью: структуры — группы переменных, имеющей одно имя и называемой агрегат- ным типом данных. (Кроме того, еще известны термины соединение (compound) и конгломерат (conglomerate).)} объединения, которое позволяет определять один и тот же участок памяти как два или более типов переменных; битового поля, которое является специальным типом элемента структуры или объединения, позволяющим легко получать доступ к отдельным битам; перечисления — списка поименованных целых констант; ключевого слова type def, которое определяет новое имя для существующего типа. Все эти способы описаны в этой главе. Структуры Структура — это совокупность переменных, объединенных под одним именем. С помощью структур удобно размещать в смежных полях связанные между собой эле- менты информации. Объявление структуры создает шаблон, который можно исполь- зовать для создания ее объектов (то есть экземпляров этой структуры). Переменные, из которых состоит структура, называются членами. (Члены структуры еще называют- ся элементами или полями.) Как правило, члены структуры связаны друг с другом по смыслу. Например, эле- мент списка рассылки, состоящий из имени и адреса логично представить в виде структуры. В следующем фрагменте кода показано, как объявить структуру, в которой определены поля имени и адреса. Ключевое слово struct сообщает компилятору, что объявляется (еще говорят, “декларируется”) структура. struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; }; Обратите внимание, что объявление завершается точкой с запятой, потому что объявление структуры является оператором. Кроме того, тег структуры addr иденти- фицирует эту конкретную структуру данных и является спецификатором ее типа. В данном случае на самом деле никакая переменная не создается. Всего лишь опре- деляется вид данных. Когда вы объявляете структуру, то определяете агрегатный тип, а не переменную. И пока вы не объявите переменную этого типа, то существовать она не будет. Чтобы объявить переменную (то есть физический объект) типа addr, напи- шите следующее: | struct addr addr_info; В этом операторе объявлена переменная типа addr, которая называется addr_info. Таким образом, addr описывает вид структуры (ее тип), a addr_info яв- ляется экземпляром (объектом) этой структуры. 170 Часть I. Основы языка С
Когда объявляется переменная-структура, компилятор автоматически выделяет ко- личество памяти, достаточное, чтобы разместить все ее члены. На рис. 7.1 показано, как addr_info размещена в памяти; в данном случае предполагается, что целые пе- ременные типа long занимают по 4 байта. Одновременно с объявлением структуры можно объявить одну или несколько пе- ременных. Например, struct addr { char name[30]; char street[40]; char city[20]; char state [ 3]; unsigned long int zip; } addr_info, binfo, cinfo; определяет тип структуры, называемый addr, и объявляет переменные этого типа addr_info, binfo и cinfo. Важно понимать, что каждая переменная-структура со- держит собственные копии членов структуры. Например, поле zip в binfo отличает- ся от поля zip в cinfo. Изменения в zip из binfo не повлияют на содержимое поля zip, находящегося в cinfo. Name (имя) 30 байт Street (улица) 40 байт City (город) 20 байт State (штат) 3 байта Zip (код) 4 байта Рис. 7.1. Расположение в памяти структуры addr_info Если нужна только одна переменная-структура, то тег структуры является лиш- ним. В этом случае наш пример объявления можно переписать следующим образом: struct { char name[30]; char street[40]; char city[20]; char state [ 3]; unsigned long int zip; } addr_info; В этом случае объявляется одна переменная с именем addr_info, причем ее поля указаны в структуре, которая предшествует этому имени. Общий вид объявления структуры такой: Глава 7. Структуры, объединения, перечисления и декларация typedef 171
struct тег { тип имя-члена; тип имя-члена; тип имя-члена; } переменные-структуры; причем тег или переменные-структуры могут быть пропущены, но только не оба од- новременно. Доступ к членам структуры Доступ к отдельным членам структуры осуществляется с помощью оператора . (который обычно называют оператором точка или оператором доступа к члену струк- туры). Например, в следующем выражении полю zip в уже объявленной перемен- ной-структуре addr_info присваивается значение ZIP-кода, равное 12345: | addr_info.zip = 12345; Этот отдельный член определяется именем объекта (в данном случае addr_info), за которым следует точка, а затем именем самого этого члена (в данном случае zip). В общем виде использование оператора точка для доступа к члену структуры выглядит таким образом: имя-объекта.имя-члена Поэтому, чтобы вывести ZIP-код на экран, напишите следующее: | printf("%1и", addr_info.zip); Будет выведен ZIP-код, который находится в члене zip переменной-структуры addr_info. Точно так же в вызове gets() можно использовать массив символов addr_infо.name: | gets(addr_info.name); Таким образом, в начало name передается указатель на символьную строку. Так как name является массивом символов, то чтобы получить доступ к отдельным символам в массиве addr_info.name, можно использовать индексы вместе с name. Например, с помощью следующего кода можно посимвольно вывести на экран со- держимое addr_info.name: |for(t=0; addr_info.name[t]; ++t) putchar(addr_infо.name[t]); Обратите внимание, что индексируется именно name (а не addr_info). Помните, что addr_info — это имя всего объекта-структуры, a name — имя элемента этой структуры. Таким образом, если требуется индексировать элемент структуры, то ин- декс необходимо указывать после имени этого элемента. Присваивание структур Информация, которая находится в одной структуре, может быть присвоена другой структуре того же типа при помощи единственного оператора присваивания. Нет не- обходимости присваивать значения каждого члена в отдельности. Как выполняется присваивание структур, показывает следующая программа: 172 Часть I. Основы языка С
#include <stdio.h> int main(void) { struct { int a; int b; } x, y; x.a = 10; у = x; /* присваивание одной структуры другой */ printf("%d", у.a); return 0; } После присвоения в у.а будет храниться значение 10. [I Массивы структур Структуры часто образуют массивы. Чтобы объявить массив структур, вначале необхо- димо определить структуру (то есть определить агрегатный тип данных), а затем объявить переменную массива этого же типа. Например, чтобы объявить 100-элементный массив структур типа addr, который был определен ранее, напишите следующее: | struct addr addr_list[100]; Это выражение создаст 100 наборов переменных, каждый из которых организован так, как определено в структуре addr. Чтобы получить доступ к определенной структуре, указывайте имя массива с индексом. Например, чтобы вывести ZIP-код из третьей структуры, напишите следующее: | printf(”%lu"г addr_list[2].zip); Как и в других массивах переменных, в массивах структур индексирование начи- нается с 0. Для справки: чтобы указать определенную структуру, находящуюся в массиве структур, необходимо указать имя этого массива с определенным индексом. А если нужно указать индекс определенного элемента в структуре, то необходимо указать ин- декс этого элемента. Таким образом, в результате выполнения следующего выражения первому символу члена name, находящегося в третьей структуре из addr_list, при- сваивается значение ’X’. | addr_list[2].name[0] = ’X’; Пример со списком рассылки Чтобы показать, как используются структуры и массивы структур, в этом разделе создается простая программа работы со списком рассылки, и в ее массиве структур будут храниться адреса и связанная с ними информация. Эта информация записыва- ется в следующие поля: name (имя), street (улица), city (город), state (штат) и zip (почтовый код, индекс). Вся эта информация, как показано ниже, находится в массиве структур типа addr: I struct addr { char name[30]; Глава 7. Структуры, объединения, перечисления и декларация typedef 173
Ichar street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; Обратите внимание, что поле zip имеет целый тип unsigned long. Правда, чаще можно встретить хранение почтовых кодов, в которых используются строки символов, потому что этот способ подходит для почтовых кодов, в которых вместе с цифрами используются и буквы (как, например, в Канаде и других странах). Однако в нашем примере почтовый индекс хранится в виде целого числа; это делается для того, чтобы показать использование числового элемента в структуре. Вот main () — первая функция, которая нужна программе: int main(void) { char choice; init_list(); /* инициализация массива структур */ for(;;) { choice = menU—Select(); switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list (); break; case 4: exit(0); } } return 0; } Функция начинает выполнение с инициализации массива структур, а затем реаги- рует на выбранный пользователем пункт меню. Функция init_list () готовит массив структур к использованию, обнуляя первый байт поля name каждой структуры массива. (В программе предполагается, что если поле name пустое, то элемент массива не используется.) А вот сама функция init_list(): /* Инициализация списка. */ void init_list(void) { register int t; for(t=0; t<MAX; ++t) addr_list[t].name[0] = ’’; } Функция menu_select () выводит меню на экран и возвращает то, что выбрал пользователь. /* Получение значения, выбранного в меню. */ int menu_select(void) { char s[80] ; int c; 174 Часть I. Основы языка С
printf ("1. Введите имяп"); printf("2. Удалите имяп"); printf("3. Выведите списокп"); printf("4. Выходп"); do { printf("пВведите номер нужного пункта: "); gets (s); с = atoi(s); } while(c<0 || c>4); return c; } Функция enter () подсказывает пользователю, что именно требуется ввести, и со- храняет введенную информацию в следующей свободной структуре. Если массив за- полнен, то выводится сообщение Список заполнен. Функция find_free() ищет в массиве структур свободный элемент. /* Ввод адреса в список. */ void enter(void) { int slot; char s[80] ; slot = f ind__f ree () ; if(slot==-l) { printf("пСписок заполнен"); return; } printf("Введите имя: "); gets(addr_list[slot].name); printf("Введите улицу: "); gets(addr_list[slot].street); printf("Введите город: "); gets(addr_list[slot].city); printf ("Введите штат:- "); gets(addr_list[slot].state); printf("Введите почтовый код: "); gets (s); addr_list [slot] . zip = strtoul(s, ’ ’, 10); /* Поиск свободной структуры. */ int find_free(void) { register int t; for(t=0; addr_list[t].name[0] && t<MAX; ++t) ; if(t==MAX) return -1; /* свободных структур нет */ return t; Глава 7. Структуры, объединения, перечисления и декларация typedef 175
Обратите внимание, что если все элементы массива структур заняты, то find_f гее () возвращает -1. Это удобное число, потому что в массиве нет -1-го элемента. Функция delete () предлагает пользователю указать индекс той записи с адресом, которую требуется удалить. Затем функция обнуляет первый байт поля name. /* Удаление адреса. */ void delete(void) { register int slot; char s [80]; printf("Введите № записи: "); gets (s) ; slot = atoi(s); if(slot>=0 && slot < MAX) addr_list[slot].name[0] = ’'; } И последняя функция, которая требуется программе, — это list (), которая выво- дит на экран весь список рассылки. Из-за большого разнообразия компьютерных сред язык С не определяет стандартную функцию, которая бы отправляла вывод на прин- тер. Однако все нужные для этого средства имеются во всех компиляторах С. Воз- можно, вам самим захочется сделать так, чтобы программа работы со списками могла еще и распечатывать список рассылки. /* Вывод списка на экран */ void list(void) { register int t; for(t=0; t<MAX; ++t) { if(addr_list[t].name[0]) { printf("%sn", addr_list[t].name); printf("%sn", addr_list[t].street); printf("%sn", addr_list[t].city); printf("%sn", addr_list[t].state); printf("%lunn", addr_list[t].zip); } } printf("nn"); } Ниже программа обработки списка рассылки приведена полностью. Если у вас ос- тались какие-либо сомнения относительно ее компонентов, введите программу в компьютер и проверьте ее работу, делая в программе изменения и получая соответст- вующие результаты. /* Простой пример программы обработки списка рассылки, в которой ис- пользуется массив структур. */ #include <stdio.h> #include <stdlib.h> #define MAX 100 struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; 176 Часть I. Основы языка С
} addr_list[MAX]; void (void), enter(void); void delete(void), list(void); int menu_select(void), find_free(void); int main(void) { char choice; init__list () ; /* инициализация массива структур */ for(;;) { choice = menu__select () ; switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list (); break; case 4: exit(0); } } return 0; } /* Инициализация списка */ void init_list(void) { register int t; for(t=0; t<MAX; ++t) addr_list[t].name[0] = ''; } /* Получение значения, выбранного в меню. */ int menu_select(void) { char s [80]; int c; printf("1. Введите имяп"); printf("2. Удалите имяп"); printf("3. Выведите списокп"); printf("4. Выходп"); do { printf("ХпВведите номер нужного пункта: "); gets (s); с = atoi (s); } while(c<0 || c>4); return c; } /* Ввод адреса в список. */ void enter(void) { int slot; Глава 7. Структуры, объединения, перечисления и декларация typedef 177
char s [80]; slot = find_free(); if(slot==-l) { printf("ХпСписок заполнен"); return; } printf("Введите имя: "); gets(addrJList[slot].name); printf("Введите улицу: "); gets(addr_list [slot].street) ; printf("Введите город: "); gets(addr_list[slot].city); printf("Введите штат: "); gets(addr_list[slot].state) ; printf("Введите почтовый индекс: "); gets(s); addr_list[slot].zip = strtoul(s, ’’, 10); /* Поиск свободной структуры */ int find_free(void) { register int t; for(t=0; addrJList[t].name[0] && t<MAX; ++t) ; if(t==MAX) return -1; /* свободных структур нет */ return t; /* Удаление адреса. */ void delete(void) { register int slot^'H''* char s [80]; printf ( "Введите N’ записи: "); gets (s); slot = atoi (s); if(slot>=0 && slot < MAX) addr_list[slot].name[0] = ''; /* Вывод списка на экран */ void list(void) { register int t; for(t=0; t<MAX; ++t) { if (addrJList [t] . name [0] ) { printf("%sn"> addrJList[t].name); printf("%sn", addr_list[t].street); printf("%sn", addr_list[t].city); 178 Часть I. Основы языка С
printf("%sn", addr_list[t] .state); printf("%lunn”, addr_list[ t ].zip); } } printf("nn"); H Передача структур функциям В этом разделе рассказывается о передаче структур и их членов функциям. Передача членов структур функциям При передаче функции члена структуры передается его значение, притом не играет роли то, что значение берется из члена структуры. Проанализируйте, например, сле- дующую структуру: struct fred { char х; int у; float z; char s[10] ; } mike; Например, обратите внимание, каким образом каждый член этой структуры пере- дается функции: func(mike.х); /* передается символьное значение х */ func2(mike.у); /* передается целое значение у */ func3(mike.z); /* передается значение с плавающей точкой z */ func4(mike.s); /* передается адрес строки s */ func(mike.s[2]); /* передается символьное значение s[2] */ В каждом из этих случаев функции передается значение определенного элемента, и здесь не имеет значения то, что этот элемент является частью какой-либо большей совокупности. Если же нужно передать адрес отдельного члена структуры, то перед именем струк- туры должен находиться оператор &. Например, чтобы передать адреса членов струк- туры mike, можно написать следующее: func(&mike.х); /* передается адрес символа х */ func2(&mike.у); /* передается адрес целого у */ func3(&mike.z); /* передается адрес члена z с плавающей точкой */ func4(mike.s); /* передается адрес строки s */ func(&mike.s[2]); /* передается адрес символа в s[2] */ Обратите внимание, что оператор & стоит непосредственно перед именем структу- ры, а не перед именем отдельного члена. И еще заметьте, что s уже обозначает адрес, поэтому & не требуется. Передача целых структур функциям Когда в качестве аргумента функции используется структура, то для передачи це- лой структуры используется обычный способ вызова по значению. Это, конечно, оз- начает, что любые изменения в содержимом параметра внутри функции не отразятся на той структуре, которая передана в качестве аргумента. Глава 7. Структуры, объединения, перечисления и декларация typedef 179
При использовании структуры в качестве аргумента надо помнить, что тип аргу- мента должен соответствовать типу параметра. Например, в следующей программе и аргумент arg, и параметр parm объявляются с одним и тем же типом структуры. #include <stdio.h> /* Определение типа структуры */ struct struct__type { int a, b; char ch; } ; void fl (struct struct__type parm); int main(void) { struct struct_type arg; arg.a = 1000; fl(arg); return 0; } void fl(struct struct_type parm) { printf("%d", parm.a); } Как видно из этой программы, при объявлении параметров, являющихся структу- рами, объявление типа структуры должно быть глобальным, чтобы структурный тип можно было использовать во всей программе. Например, если бы struct_type был бы объявлен внутри main (), то этот тип не был бы виден в f 1 (). Как уже говорилось, при передаче структуры тип аргумента должен совпадать с типом параметра. Для аргумента и параметра недостаточно просто быть физически похожими; должны совпадать даже имена их типов. Например, следующая версия предыдущей программы неправильная и компилироваться не будет. Дело в том, что имя типа для аргумещ^, цспользуемого при вызове функции fl О, отличается от имени типа ее параметра. /* Эта программа ^неправильная и при компиляции будут обнаружены ошибки. */ #include <stdio.h>; >S!t /★ Определение типа структуры */ struct struct_type { int a, b; char ch; } ; /* Определение структуры, похожей на struct_type, но с другим именем. */ struct struct_type2 { int a, b; char ch; } ; 180 Часть I. Основы языка С
void fl(struct struct_type2 parm); int main(void) { struct struct—type arg; arg.a = 1000; fl(arg); /* несовпадение типов ★/ return 0; } void fl(struct struct_type2 parm) { printf("%d", parm.a); } H Указатели на структуры В языке С указатели на структуры также официально признаны, как и указатели на любой другой вид объектов. Однако указатели на структуры имеют некоторые осо- бенности, о которых и пойдет речь. Объявление указателя на структуру Как и другие указатели, указатель на структуру объявляется с помощью звездочки *, которую помещают перед именем переменной структуры. Например, для ранее оп- ределенной структуры addr следующее выражение объявляет addr_pointer указате- лем на данные этого типа (то есть на данные типа addr): | struct addr *addr_pointer; Использование указателей на структуры Указатели на структуры используются главным образом в двух случаях: когда структура передается функции с помощью вызова по ссылке, и когда создаются связанные друг с другом списки и другие структуры с динамическими данными, работающие на основе ди- намического размещения. В этой главе рассматривается первый случай. У такого способа, как передача любых (кроме самых простых) структур функциям, имеется один большой недостаток: при выполнении вызова функции, чтобы помес- тить структуру в стек, необходимы существенные ресурсы. (Вспомните, что аргументы передаются функциям через стек.) Впрочем, для простых структур с несколькими членами эти ресурсы являются не такими уж большими. Но если в структуре имеется большое количество членов или некоторые члены сами являются массивами, то при передаче структур функциям производительность может упасть до недопустимо низ- кого уровня. Как же решить эту проблему? Надо передавать не саму структуру, а ука- затель на нее. Когда функции передается указатель на структуру, то в стек попадает только адрес структуры. В результате вызовы функции выполняются очень быстро. В некоторых случаях этот способ имеет еще и второе преимущество: передача указателя позволяет функции модифицировать содержимое структуры, используемой в качестве аргумента. Чтобы получить адрес переменной-структуры, необходимо перед ее именем помес- тить оператор &. Например, в следующем фрагменте кода Глава 7. Структуры, объединения, перечисления и декларация typedef 181
struct bal { float balance; char name[80]; }; struct bal *p; /* объявление указателя на структуру */ адрес структуры person можно присвоить указателю р: | р = &person; Чтобы с помощью указателя на структуру получить доступ к ее членам, необходимо использовать оператор стрелка ->. Вот, например, как можно сослаться на поле balance: | p->balance Оператор ->, который обычно называют оператором стрелки, состоит из знака “минус”, за которым следует знак “больше”. Стрелка применяется вместо оператора точки тогда, когда для доступа к члену структуры используется указатель на структуру. Чтобы увидеть, как можно использовать указатель на структуру, проанализируйте следующую простую программу, которая имитирует таймер, выводящий значения ча- сов, минут и секунд: /* Программа-имитатор таймера. */ tfinclude <stdio.h> tfdefine DELAY 128000 struct my_time { int hours; int minutes; int seconds; } ; void display(struct my_time *t); void update(struct my_time *t); void delay(void); int main(void) struct my_time $^fetime; systime.hours = 0; systime.minutes = 0; systime.seconds = 0; for(;;) { update(&systime); display(&systime); } return 0; } void update(struct my_time *t) { t->seconds++; if'(t->seconds==60) { t->seconds = 0; t->minutes++; Часть I. Основы языка С
} if(t->minutes==60) { t->minutes = 0; t->hours++; } if(t->hours==24) t->hours = 0; delay(); } void display(struct my_time *t) { printf(”%02d:", t->hours); printf(”%02d:”, t->minutes); printf(”%02dn", t->seconds); } void delay(void) { long int t; /* если надо, можете изменить константу DELAY (задержка) */ for(t=l; t<DELAY; ++t) ; } Эту программу можно настраивать, меняя определение DELAY. В этой программе объявлена глобальная структура my_time, но при этом не объ- явлены никакие другие переменные программы. Внутри же main() объявлена струк- тура systime и она инициализируется значением 00:00:00. Это значит, что systime непосредственно видна только в функции main (). Функциям update () (которая изменяет значения времени) и display () (которая выводит эти значения) передается адрес структуры systime. Аргументы в обеих функциях объявляются как указатель на структуру my_time. Внутри update () и display () доступ к каждому члену systime осуществляется с помощью указателя. Так как функция update () принимает указатель на структуру systime, то она в состоянии обновлять значение этой структуры. Например, необхо- димо “в полночь”, когда значение переменной, в которой хранится количество часов, станет равным 24, сбросить отсчет и снова сделать значение этой переменной равным 0. Для этого в update () имеется следующая строка: | if(t->hours==24) t->hours = 0; Таким образом, компилятору дается указание взять адрес t (этот адрес указывает на переменную systime из main ()) и сбросить значение hours в нуль. Помните, что оператор точка используется для доступа к элементам структуры при работе с самой структурой. А когда используется указатель на структуру, то надо при- менять оператор стрелка. Н Массивы и структуры внутри структур Членом структуры может быть или простая переменная, например, типа int или double, или составной (не скалярный) тип. В языке С составными типами являются массивы и структуры. Один составной тип вы уже видели — это символьные массивы, которые использовались в addr. Глава 7. Структуры, объединения, перечисления и декларация typedef 183
Члены структуры, которые являются массивами, можно считать такими же члена- ми структуры, как и те, что нам известны из предыдущих примеров. Например, про- анализируйте следующую структуру: I struct X { int а[10] [10]; /* массив 10 х 10 из целых значений */ float Ь; } у; Целый элемент с индексами 3, 7 из массива а, находящегося в структуре у, обо- значается таким образом: | у.а[3] [7] Когда структура является членом другой структуры, то она называется вложенной. Например, в следующем примере структура address вложена в етр: I struct етр { struct addr address; /* вложенная структура */ float wage; } worker; Здесь структура была определена как имеющая два члена. Первым является струк- тура типа addr, в которой находится адрес работника. Второй член — это wage, где находятся данные по его зарплате. В следующем фрагменте кода элементу zip из address присваивается значение 93456. Iworker.address.zip = 93456; Как вы видите, в каждой структуре любой член обозначают с помощью тех струк- тур, в которые он вложен — начиная от самых общих и заканчивая той, непосредст- венно в которой он находится. В соответствии со стандартом С89 структуры могут быть вложенными вплоть до 15-го уровня. А стандарт С99 допускает уровень вложен- ности до 63-го включительно. й Объединения Объединение— это место в памяти, которое используется для хранения перемен- ных разных типов. Объединение дает возможность интерпретировать один и тот же набор битов не менее, чем двумя разными способами. Объявление объединения (начинается с ключевого слова union) похоже на объявление структуры и в общем виде выглядит так: union тег { тип имя-члена; тип имя-члена; тип имя-члена; } переменные-этого-объединения; Например: I union u_type { I int i; I char ch; 184 Часть I. Основы языка С
Это объявление не создает никаких переменных. Чтобы объявить переменную, ее имя можно поместить в конце объявления или написать отдельный оператор объявления. Что- бы с помощью только что написанного кода объявить переменную-объединение, которая называется cnvt и имеет тип u type, можно написать следующий оператор: | union u_type cnvt; В cnvt одну и ту же область памяти занимают целая переменная i и символьная переменная ch. Конечно, i занимает 2 байта (при условии, что целые значения зани- мают по 2 байта), a ch — только 1. На рис. 7.2 показано, каким образом i и ch поль- зуются одним и тем же адресом. В любом месте программы хранящиеся в cnvt дан- ные можно обрабатывать как целые или символьные. <-------------- i ------------► Байт О Байт 1 Ч-------ch------► Рис. 7.2. Как i, так и ch, хранятся в объединении cnvt (подразумевается, что целые значения занимают по 2 байта) Когда переменная объявляется с ключевым словом union, компилятор автомати- чески выделяет столько памяти, чтобы в ней поместился самый большой член нового объединения. Например, при условии, что целые значения занимают по 2 байта, для размещения i в cnvt необходимо, чтобы длина этого объединения составляла 2 бай- та, даже если для ch требуется только 1 байт. Для получения доступа к члену объединения используйте тот же синтаксис, что и для структур: операторы точки и стрелки. При работе непосредственно с объединени- ем следует пользоваться точкой. А при получении доступа к объединению с помощью указателя нужен оператор стрелка. Например, чтобы присвоить целое значение 10 элементу i из cnvt, напишите | cnvt.i = 10; В следующем примере функции fund передается указатель на cnvt: Ivoid fund (union u_type *un) { un->i = 10; /* присвоение cnvt значения 10 с помощью указателя */ } Объединения часто используются тогда, когда нужно выполнить специфическое преобразование типов, потому что хранящиеся в объединениях данные можно обо- значать совершенно разными способами. Например, используя объединения, можно манипулировать байтами, составляющими значение типа double, и делать так, чтобы менять его точность или выполнять какое-либо необычное округление. Чтобы получить представление о полезности объединений в случаях, когда нужны нестандартные преобразования типа, подумайте над проблемой записи целых значе- ний типа short в файл, который находится на диске. Глава 7. Структуры, объединения, перечисления и декларация typedef 185
В стандартной библиотеке языка С не определено никакой функции, специально предназначенной для выполнения этой записи. Хотя данные любого типа можно записывать в файл, пользуясь функцией fwriteO, но было бы нерационально применять этот способ для такой простой операции, как запись на диск целых значений типа short, так как получится чрез- мерный перерасход ресурсов. А вот, используя объединение, можно легко создать функцию putw (), которая по одному байту будет записывать в файл двоичное пред- ставление целого значения типа short. (В этом примере предполагается, что такие значения имеют длину 2 байта каждое.) Чтобы увидеть, как это делается, вначале соз- дадим объединение, состоящее из целой переменной типа short и из массива 2-байтовых символов: I union pw { I short int i; I char ch[2]; I b Теперь с помощью pw можно написать вариант putw (), приведенный в следующей программе. #include <stdio.h> #include <stdlib.h> union pw { short int i; char ch[2]; }; int putw(short int num, FILE *fp); int main(void) { FILE *fp; fp = fopen("test.tmp", "wb+"); if(fp == NULL) { printf("Файл не открыт.n"); exit (1); } putw(1025, fp); /* записать значение 1025 */ fclose(fp); return 0; } int putw(short int num, FILE *fp) { union pw word; word.i = num; putc(word.ch[0], fp); /* записать первую половину */ return putc(word.ch[1], fp); /* записать вторую половину */ } Хотя функция putw () и вызывается с целым аргументом типа short, ей для вы- полнения побайтовой записи в файл на диске все равно приходится использовать стандартную функцию putc (). 186 Часть I. Основы языка С
Ц Битовые поля В отличие от некоторых других компьютерных языков, в языке С имеется встро- енная поддержка битовых полей1, которая дает возможность получать доступ к еди- ничному биту. Битовые поля могут быть полезны по разным причинам, а именно: Если память ограничена, то в одном байте можно хранить несколько булевых переменных (принимающих значения ИСТИНА и ЛОЖЬ); Некоторые устройства передают информацию о состоянии, закодированную в байте в одном или нескольких битах; Для некоторых процедур шифрования требуется доступ к отдельным битам внутри байта. Хотя для решения этих задач можно успешно применять побитовые операции, би- товые поля могут придать вашему коду больше упорядоченности (и, возможно, с их помощью удастся достичь большей эффективности). Битовое поле может быть членом структуры или объединения. Оно определяет длину поля в битах. Общий вид определения битового поля такой: тип имя : длина; Здесь тип означает тип битового поля, а длина — количество бит, которые занима- ет это поле. Тип битового поля может быть int, signed или unsigned. (Кроме того, в соответствии со стандартом С99, у битового поля еще может быть тип _Воо1.) Битовые поля часто используются при анализе данных, поступающих в программу с аппаратуры. Например, в результате опроса состояния адаптера последовательной связи может возвращаться байт состояния, организованный следующим образом: Бит Что означает, если установлен О Изменение в линии сигнала разрешения на передачу (change in clear-to-send line) 1 Изменение состояния готовности устройства сопряжения (change in data-set-ready) 2 Обнаружена концевая запись (trailing edge detected) 3 Изменение в приемной линии (change in receive line) 4 Разрешение на передачу. Сигналом CTS (clear-to-send) модем разрешает под- ключенному терминалу передавать данные 5 Модем готов (data-set-ready) 6 Телефонный вызов (telephone ringing) 7 Сигнал принят (received signal) Информацию в байте состояния можно представить с помощью следующего битового поля: struct status_type { unsigned delta__cts: 1; unsigned delta__dsr: 1; unsigned tr_edge: 1; unsigned delta__rec: 1; unsigned cts: 1; unsigned dsr: 1; unsigned ring: 1; unsigned rec__line: 1; } status; 1 Называются также полями битов. — Прим. ред. Глава 7. Структуры, объединения, перечисления и декларация typedef 187
Для того чтобы программа могла определить, когда можно отправлять или прини- мать данные, можно использовать такие операторы: I status = get_port_status() ; if(status.cts) printf("Разрешение на передачу"); if(status.dsr) printf("Данные готовы"); Для присвоения битовому полю значения используйте тот же способ, что и для элемента, находящегося в структуре любого другого типа. Вот, например, фрагмент кода, выполняющий сброс поля ring: | status.ring = 0; Как видно из этого примера, каждое битовое поле доступно с помощью оператора точка. Однако если структура передана с помощью указателя, то следует использовать оператор стрелка ->. Нет необходимости давать имя каждому битовому полю. Таким образом можно легко получать доступ к нужному биту, обходя неиспользуемые. Например, если вас интересуют только биты cts и dsr, то структуру status_type можно объявить таким образом: I struct status_type { unsigned : 4; unsigned cts: 1; unsigned dsr: 1; } status; Кроме того, обратите внимание, что если биты, расположенные после dsr, не ис- пользуются, то определять их не надо. В структурах можно сочетать обычные члены с битовыми полями. Например, в структуре struct emp { struct addr address; float pay; unsigned lay_off: 1; /* временно уволенный или работающий */ unsigned hourly: 1; /* почасовая оплата или оклад */ unsigned deductions: 3; /* налоговые (IRS) удержания */ }; определены данные о работнике, для которых выделяется только один байт, содержа- щий информацию трех видов: статус работника, на окладе ли он, а также количество удержаний из его зарплаты. Без битового поля эта информация занимала бы 3 байта. Использование битовых полей имеет определенные ограничения. Нельзя получить адрес битового поля. Нет массивов битовых данных. При переносе кода на другую машину неизвестно, будут ли поля обрабатываться справа налево или слева направо; это значит, что выполнение любого кода, в котором используются битовые поля, в определенной степени может зависеть от машины, на которой он выполняется. Дру- гие ограничения будут зависеть от конкретных реализаций. Щ Перечисления Перечисление — это набор именованных целых констант. Перечисления довольно часто встречаются в повседневной жизни. Вот, например, перечисление, в котором приведены названия монет, используемых в Соединенных Штатах: penny (пенни, монета в один цент), nickel (никель, монета в пять центов), dime (монета в 10 центов), quarter (25 центов, четверть доллара), half-dollar (полдоллара), dollar (доллар) 188 Часть I. Основы языка С
Перечисления определяются во многом так же, как и структуры; началом объявле- ния перечислимого типа1 служит ключевое слово enum. Перечисление в общем виде выглядит так: enum тег { список перечисления } список переменных; Здесь тег и список переменных не являются обязательными. (Но хотя бы что-то одно из них должно присутствовать.) Следующий фрагмент кода определяет перечис- ление с именем coin (монета): | enum coin { penny, nickel, dime, quarter, half_dollar, dollar }; Тег перечисления можно использовать для объявления переменных данного пере- числимого типа. Вот код, в котором money (деньги) объявляется в качестве перемен- ной типа coin: | enum coin money; С учетом этих объявлений совершенно верными являются следующие операторы: I money = dime; if(money == quarter) printf("Денег всего четверть доллара.n"); Главное, что нужно знать для понимания перечислений — каждый их элемент1 2 представляет целое число. В таком виде элементы перечислений можно применять везде, где используются целые числа. Каждому элементу дается значение, на единицу большее, чем у его предшественника. Первый элемент перечисления имеет значение 0. Поэтому, при выполнении кода | printf("%d %d", penny, dime); на экран будет выведено 0 2. Однако для одного или более элементов можно указать значение, используемое как инициализатор. Для этого после перечислителя надо поставить знак равенства, а затем — целое значение. Перечислителям, которые идут после инициализатора, при- сваиваются значения, большие предшествующего. Например, следующий код при- сваивает quarter значение 100: | enum coin { penny, nickel, dime, quarter=100, half-dollar, dollar}; И вот какие значения появились у этих элементов: penny 0 nickel 1 : dime 2 i / quarter 100 ! half-dollar 101 dollar 102 Относительно перечислений есть одно распространенное, но ошибочное мнение. Оно состоит в том, что их элементы можно непосредственно вводить и выводить. Это не так. Например, следующий фрагмент кода не будет выполняться так, как того ожидают многие неопытные программисты: I /* этот код работать не будет */ I money = dollar; I printf("йз", money); 1 Иногда используется термин перечисляемый тип. — Прим. ред. 2 Элементы списка перечисления называются также перечислителями и идентификатора- ми. — Прим. ред. Глава 7. Структуры, объединения, перечисления и декларация typedef 189
Здесь dollar — это имя для значения целого типа; это не строка. Таким образом, попытка вывести money в виде строки по существу обречена. По той же причине для достижения нужных результатов не годится и такой код: I/* этот код неправильный */ strcpy(money, "dime"); То есть строка, содержащая имя элемента, автоматически в этот перечислитель не превратится. На самом же деле создавать код для ввода и вывода элементов перечислений — это довольно-таки скучное занятие (но его можно избежать лишь тогда, когда будет дос- таточно именно целых значений этих перечислителей). Например, чтобы выводить название монеты, вид которой находится в money, потребуется следующий код: switch(money) { case penny: printf("пенни"); break; case nickel: printf("никель"); break; case dime: printf("монета в 10 центов"); break; case quarter: printf("четверть доллара"); break; case half-dollar: printf("полдоллара"); break; case dollar: printf("доллар"); } Иногда можно объявить строчный массив и использовать значение перечисления как индекс при переводе этого значения в соответствующую строку. Например, сле- дующий код также выводит нужную строку: char name[][12]={ "пенни", " никель", " монета в 10 центов", "четверть доллара", "полдоллара", "доллар" }; printf("%s", name [money] ) ; Конечно, он будет работать только тогда, когда не инициализирован ни один из элементов перечисления, так как строчный массив должен иметь индекс, который на- чинается с 0 и возрастает каждый раз на 1. Так как при операциях ввода/вывода необходимо специально заботиться о преоб- разовании перечислений в их строчный эквивалент, который можно легко прочитать, то перечисления полезнее всего именно в тех процедурах, где такие преобразования не нужны. Например, перечисления часто применяются, чтобы определить таблицы соответствия символов в компиляторах. Н Важное различие между С и C++ Что касается имен типов структур, объединений и перечислений, то между языка- ми С и C++ имеется важное различие. Чтобы понять эту разницу, проанализируйте следующее объявление структуры: 190 Часть I. Основы языка С
I struct MyStruct { int a; int b; } ; В языке С имя MyStruct называется тегом. Чтобы объявить объект типа MyStruct, необходимо использовать выражение, аналогичное следующему: | struct MyStruct obj; Как можно заметить, перед именем MyStruct находится ключевое слово struct. Однако в C++ для этой операции достаточно использовать объявление покороче: (MyStruct ob j; /* Нормально для C++, неправильно для С */ В C++ не требуется ключевое слово struct. В C++, как только структура объяв- лена, можно объявлять переменные ее типа, используя только ее тег и не ставя перед ним ключевого слова struct. Дело здесь в том, что в С имя структуры не определяет полное имя типа. Вот почему в С это имя называется не полным именем, а тегом. Однако в C++ имя структуры является полным именем типа, и оно может использо- ваться для определения переменных. Не надо, впрочем, забывать, что до сих пор можно на вполне законных основаниях в программе на языке C++ использовать и объявление в стиле С. Все сказанное выше можно обобщить для объединений и перечислений. Таким образом, в С при объявлении объектов непосредственно перед тегом имени должно находиться одно из ключевых слов: struct, union или enum (в зависимости от кон- кретного случая). А в C++ ключевое слово не требуется. Так как для C++ подходят объявления в стиле С, то во время переноса программ из С в C++ по этому поводу беспокоиться нечего. Но при переносе из C++ в С соот- ветствующие изменения сделать придется. В Использование sizeof для обеспечения переносимости Вы имели возможность убедиться, что структуры и объединения можно использо- вать для создания переменных разных размеров, а также в том, что настоящий размер этих переменных в разных машинах может быть разным. Оператор sizeof подсчиты- вает размер любой переменной или любого типа и может б^ь полезен, если в про- граммах требуется свести к минимуму машинно-зависимый код. Этот оператор осо- бенно полезен там, где приходится иметь дело со структурами или объединениями. Перед тем как переходить к дальнейшему изложению, цр^дположим, что опреде- ленные типы данных имеют следующие размеры: Тип Размер в байтах char 1 int 4 double 8 Поэтому при выполнении следующего кода на экран будут выведены числа 1, 4 и 8: char ch; int i ; double f; printf(”%d”, sizeof(ch)); Глава 7. Структуры, объединения, перечисления и декларация typedef 191
I printf("%d", sizeof(i)); printf("%d", sizeof(f)); Размер структуры равен сумме размеров ее членов или, возможно, даже больше этой суммы. Рассмотрим пример: I struct S { char ch; int i ; double f; } s_var; Здесь sizeof (s_var) равняется как минимум 13 (=8+4+1). Однако размер s_var может быть и больше, потому что компилятору иногда необходимо специально увели- чить размер структуры, выровнять некоторые ее члены на границу слова или парагра- фа. (Параграф занимает 16 байтов.) Так как размер структуры может быть больше, чем сумма размеров ее членов, то всегда, когда нужно знать размер структуры, следует использовать sizeof. Например, если требуется динамически выделять память для объекта типа s, необходимо использовать последовательность операторов, аналогич- ную той, что показана здесь (а не вставлять вручную значения длины его членов): struct s *р; р = malloc(sizeof(struct s)); Так как sizeof — это оператор времени компиляции, то вся информация, необ- ходимая для вычисления размера любой переменной, становится известной как раз во время компиляции. Это особенно важно для объединений, потому что размер каждого из них всегда равен размеру наибольшего члена. Например, проанализируйте сле- дующее объединение: I union и { char ch; int i; double f; } u_var; Для него sizeof (u_var) равняется 8. Впрочем, во время выполнения не имеет значения, какой размер на самом деле имеет u_var. Важен размер его наибольшего члена, так как любое объединение должно быть такого же размера, как и его самый большой элемент. И Средство typedef Новые имена типов данных можно определять, используя ключевое слово typedef. На самом деле таким способом новый тип данных не создается, а всего лишь определяется новое имя для уже существующего типа. Этот процесс может по- мочь сделать машинно-зависимые программы более переносимыми. Если вы для каж- дого машинно-зависимого типа данных, используемого в вашей программе, опреде- ляете данное вами имя, то при компиляции для новой среды придется менять только операторы typedef. Такие выражения могут помочь в самодокументировании кода, позволяя давать понятные имена стандартным типам данных. Общий вид декларации typedef (оператора typedef) такой: typedef тип новое_имя; где тип — это любой тип данных языка С, а новое_имя — новое имя этого типа. Но- вое имя является дополнением к уже существующему, а не его заменой. Например, для float можно создать новое имя с помощью 192 Часть I. Основы языка С
| typedef float balance; Это выражение дает компилятору указание считать balance еще одним именем float. Затем, используя balance, можно создать переменную типа float: | balance over_due; Теперь имеется переменная с плавающей точкой over_due типа balance, а balance является еще одним именем типа float. Теперь, когда имя balance определено, его можно использовать и в другом опера- торе typedef. Например, выражение | typedef balance overdraft; дает компилятору указание признавать overdraft в качестве еще одного имени balance, которое в свою очередь является еще одним именем float. Использование операторов typedef может облегчить чтение кода и его перенос на но- вую машину. Однако новый физический тип данных таким способом вы не создадите. Глава 7. Структуры, объединения, перечисления и декларация typedef 193
Полный справочник по Глава 8 Ввод/вывод на консоль
В языке С не определено никаких ключевых слов, с помощью которых можно вы- полнять ввод/вывод. Вместо них используются библиотечные функции. Система ввода/вывода языка С — это элегантная конструкция, которая обеспечивает гибкий и в то же время слаженный механизм передачи данных от одного устройства к другому. Впрочем, эта система достаточно большая и состоит из нескольких различных функ- ций. Заголовочным файлом для функций ввода/вывода является <stdio.h>. Имеются как консольные, так и файловые1 функции ввода/вывода. С практиче- ской точки зрения консольный и файловый ввод/вывод отличаются друг от друга очень мало. Однако теоретически они находятся в двух очень “разных мирах”. В этой главе подробно рассказывается о функциях ввода/вывода на консоль. В следующей же главе представлена система файлового ввода/вывода, а также говорится, что имеется общего между этими двумя системами. За одним исключением, в этой главе рассказывается только о функциях вво- да/вывода на консоль, которые определяются стандартом языка С. В стандарте язы- ка С не определены никакие функции, предназначенные для выполнения различ- ных операций управления экраном (например, позиционирования курсора) или вы- вода на него графики. И не определены потому, что эти операции на разных машинах очень сильно отличаются. Кроме того, в стандартном С не определены никакие функции, которые выполняют операции вывода в обычном или диалоговом окне, создаваемом в среде Windows. Функции ввода/вывода на консоль выполняют всего лишь телетайпный вывод. Однако в библиотеках большинства компиляторов имеются функции графики и управления экраном, предназначенные для той среды, в которой как раз и должны выполняться программы. И, конечно же, на языке С можно писать Windows-программы. Просто в С не определены функции, которые выполняли бы эти задачи напрямую. В этой главе консольными функциями ввода/вывода называются те, которые вы- полняют ввод с клавиатуры и вывод на экран. В действительности же эти функции работают со стандартным потоком ввода и стандартным потоком вывода1 2 3. Более того, стандартный ввод? и стандартный вывод4 могут быть перенаправлены на другие уст- ройства. Таким образом, “консольные функции” не обязательно должны работать только с консолью. Перенаправление ввода/вывода описано в главе 9. В данной же главе предполагается, что ни стандартный ввод, ни стандартный вывод на другие уст- ройства не перенаправляются. На заметку В языке C++ ввод/вывод выполняют не только функции, но имеются еще и операторы (знаки операций. — Прим, ред.) ввода/вывода. Впрочем, в языке С эти операторы ввода/вывода не поддерживаются. 1 Ввод/вывод в файл и файловый ввод/вывод — синонимы. — Прим. ред. 2 Иногда говорят, что ввод происходит со стандартного устройства ввода, а вывод — на стандартное устройство вывода. — Прим. ред. 3 Стандартный ввод — логический файл для ввода данных, связываемый с физическим фай- лом или стандартным выводом другой программы при запуске. По умолчанию стандартный ввод в пакетном режиме связывается со входным потоком, а в диалоговом режиме — с терми- налом. — Прим. ред. 4 Стандартный вывод — логический файл вывода данных, связываемый с физическим фай- лом или стандартным вводом другой программы при запуске. По умолчанию стандартный вы- вод в пакетном режиме связывается с выходным потоком, а в диалоговом режиме — с термина- лом. — Прим. ред. 196 Часть I. Основы языка С
В Чтение и запись символов Самыми простыми из консольных функций ввода/вывода являются getcharO, которая читает символ с клавиатуры, и putchar (), которая отображает символ на эк- ране. Первая из этих функций ожидает, пока не будет нажата клавиша, а затем воз- вращает значение этой клавиши. Кроме того, при нажатии клавиши на клавиатуре на экране дисплея автоматически отображается соответствующий символ. Вторая же функция, putchar (), отображает символ на экране в текущей позиции курсора. Вот прототипы функций getchar () и putchar (): int getchar(void); int putchar(int c); Как видно из прототипа, считается, что функция getchar () возвращает целый ре- зультат. Однако возвращаемое значение можно присвоить переменной типа char, что обычно и делается, так как символ содержится в младшем байте. (Старший байт при этом обычно обнулен.) В случае ошибки getcharO возвращает EOF. (Макрос EOF определяется в <stdio.h> и часто равен -1.) Что же касается putchar(), то несмотря на то, что эта функция объявлена как принимающая целый параметр, она обычно вызывается с символьным аргументом. На самом деле из ее аргумента на экран выводится только младший байт. Функция putchar () возвращает записанный символ или, в случае ошибки, EOF. В следующей программе продемонстрировано применение getcharO и putchar (). В этой программе с клавиатуры вводятся символы, а затем они отобра- жаются на другом регистре. То есть символы, вводимые на верхнем регистре, выво- дятся на нижнем, а вводимые на нижнем — выводятся на верхнем. Чтобы остановить программу, введите точку. tfinclude <stdio.h> ttinclude <ctype.h> int main(void) { char ch; printf("Введите какой-нибудь текст (для завершения работы введите точку).п"); do { ch = getchar(); if(islower(ch)) ch = toupper(ch); else ch = tolower(ch); putchar(ch); } while (ch != ’.’); return 0; } (Эта программа не работает, правда, с кириллическими символами. — Прим, перев.) Трудности использования getcharO Использование getcharO может быть связано с определенными трудностями. Во многих библиотеках компиляторов эта функция реализуется таким образом, что она заполняет буфер ввода до тех пор, пока не будет нажата клавиша <ENTER>. Это назы- Глава 8. Ввод/вывод на консоль 197
вается построчно буферизованным вводом. Чтобы функция getcharO возвратила ка- кой-либо символ, необходимо нажать клавишу <ENTER>. Кроме того, эта функция при каждом ее вызове вводит только по одному символу. Поэтому сохранение в буфе- ре целой строки может привести к тому, что в очереди на ввод останутся ждать один или несколько символов, а в интерактивной среде это раздражает достаточно сильно. Хотя getcharO и можно использовать в качестве интерактивной функции, но это делается редко. Так что если предшествующая программа ведет себя не так, как ожи- далось, то вы теперь знаете, в чем тут дело. Альтернативы getcharO Так как getcharO, имеющаяся в библиотеке компилятора, может оказаться не- подходящей в интерактивной среде, то для чтения символов с клавиатуры может по- требоваться другая функция. В стандарте языка С не определяется никаких функций, которые гарантировали бы интерактивный ввод, но их определения имеются букваль- но в библиотеках всех компиляторов С. И пусть в стандарте С эти функции не опре- делены, но известны они всем! А известны они благодаря функции get char (), кото- рая для большинства программистов явно не подходит. У двух из самых распространенных альтернативных функций getch () и getche () имеются следующие прототипы: int getch(void); int getche(void); В библиотеках большинства компиляторов прототипы таких функций находятся в заголовочном файле <conio.h>. В библиотеках некоторых компиляторов имена этих функций начинаются со знака подчеркивания (_). Например, в Visual C++ компании Microsoft они называются _getch () и _getche (). Функция getch () ожидает нажатия клавиши, после которого она немедленно возвра- щает значение. Причем, символ, введенный с клавиатуры, на экране не отображается. Имеется еще и функция getche (), которая хоть и такая же, как getch (), но символ на экране отображает. И если в интерактивной программе необходимо прочитать символ с клавиатуры, то часто вместо getchar () применяется getche () или getch (). Вот, напри- мер, предыдущая программа, в которой getchar () заменена функцией getch (): #include <stdio.h> #include <conio.h> #include <ctype.h> int main(void) { char ch; printf("Введите какой-нибудь текст (для завершения работы введите точку).п"); do { ch = getch(); if(islower(ch)) ch = toupper(ch); else ch = tolower(ch); putchar(ch); } while (ch != ’.’); return 0; } 198 Часть I. Основы языка С
Когда выполняется эта версия программы, при каждом нажатии клавиши соответ- ствующий символ сразу передается программе и выводится на другом регистре. А ввод в строках не буферизируется. И хотя в кодах в этой книге функции getch() и getcheO больше не встречаются, но они вполне могут пригодиться в тех програм- мах, которые напишете вы. На заметку Тогда, когда писались эти слова, при использовании компилятора Visual C++ компании Microsoft функции _getch () и _getch () были несовместимы с функ- циями ввода стандартного С, например, с функциями scanf () или gets (). По- этому вам придется вместо стандартных функций использовать такие их специальные версии, как cscanff) или cgetsO. Чтобы получить более под- робную информацию, следует изучить документацию по Visual C++. Ш Чтение и запись строк Среди функций ввода/вывода на консоль есть и более сложные, но и более мощ- ные: это функции gets () и puts(), которые позволяют считывать и отображать строки символов. Функция gets () читает строку символов, введенную с клавиатуры, и записывает ее в память по адресу, на который указывает ее аргумент. Символы можно вводить с клавиатуры до тех пор, пока не будет введен символ возврата каретки. Он не станет частью строки, а вместо него в ее конец будет помещен символ конца строки ( ’О' ), после чего произойдет возврат из функции gets(). На самом деле вернуть символ возврата каретки с помощью этой функции нельзя (а с помощью getcharO — как раз можно). Перед тем как нажимать <ENTER>, можно исправлять неправильно вве- денные символы, пользуясь для этого клавишей возврата каретки на одну позицию (клавишей backspace). Вот прототип для gets (): char *gets(char *стр); Здесь стр — это указатель на массив символов, в который записываются символы, вводимые пользователем, gets () также возвращает стр. Следующая программа читает строку в массив str и выводит ее длину: #include <stdio.h> #include <string.h> int main(void) { char str[80]; gets (str); printf("Длина в символах равна %d", strlen(str)); return 0; } Необходимо очень осторожно использовать gets(), потому что эта функция не проверяет границы массива, в который записываются введенные символы. Таким об- разом, может случиться, что пользователь введет больше символов, чем помещается в этом массиве. Хотя функция gets() прекрасно подходит для программ-примеров и простых утилит, предназначенных только для вас, но в профессиональных программах ею лучше не пользоваться. Ее альтернативой, позволяющей предотвратить переполне- ние массива, будет функция fgets (), которая описана в следующей главе. Функция puts () отображает на экране свой строковый аргумент, после чего кур- сор переходит на новую строку. Вот прототип этой функции: Глава 8. Ввод/вывод на консоль 199
int puts(const char *cmp) puts () признает те же самые управляющие последовательности1, что и printf (), на- пример, t в качестве символа табуляции. Вызов функции puts() требует намного меньше ресурсов, чем вызов printf (). Это объясняется тем, что puts () может толь- ко выводить строку символов, но не может выводить числа или делать преобразования формата. В результате эта функция занимает меньше места и выполняется быстрее, чем printf (). Поэтому тогда, когда не нужны преобразования формата, часто ис- пользуется функция puts (). Функция puts () в случае успешного завершения возвращает неотрицательное значение, а в случае ошибки — EOF. Однако при записи на консоль обычно предпо- лагают, что ошибки не будет, поэтому значение, возвращаемое puts(), проверяется редко. Следующий оператор выводит фразу Привет: | puts("Привет”); В таблице 8.1 перечислены основные функции консольного ввода/вывода. Таблица 8.1. Основные функции ввода/вывода Функция________Ее действия____________________________________________________________ getcharO Читает символ с клавиатуры; обычно ожидает возврат каретки getche() Читает символ, при этом он отображается на экране; не ожидает возврата карет- ки; в стандарте С не определена, но распространена достаточно широко getch() Читает символ, но не отображает его на экране; не ожидает возврата каретки; в стандарте С не определена, но распространена достаточно широко putchar() Отображает символ на экране gets() Читает строку с клавиатуры puts() Отображает строку на экране В следующей программе — простом компьютеризованном словаре — показано применение нескольких основных функций консольного ввода/вывода. Эта програм- ма предлагает пользователю ввести слово, а затем проверяет, совпадает ли оно с ка- ким-либо из тех слов, что находятся в ее базе данных. Если оно там есть, то програм- ма выводит значение слова. Обратите особое внимание на использование косвенной адресации в этой программе. Чтобы легче было понять программу, прежде всего вспомните, что массив die — это массив указателей на строки. Обратите внимание, что список должен завершаться двумя нулями. /* Простой словарь. */ #include <stdio.h> #include <string.h> #include <ctype.h> /* список слов и их значений */ char *dic[][40] = { "атлас”, "Том географических и/или топографических карт.", "автомобиль", "Моторизованное средство передвижения.", "телефон", "Средство связи.", "самолет", "Летающая машина.", "", "" /* нули, завершающие список */ }; 1 Называются также ESC-последовательностямщ в C/C++ —это комбинация символов, обычно используемая для задания неотображаемых символов и символов, имеющих специаль- ное значение. Представление управляющих последовательностей начинается с обратной косой черты. — Прим. ред. 200 Часть I. Основы языка С
int main(void) { char word[80], ch; char **p; do { puts("ХпВведите слово: "); scanf("%s", word); p = (char **)dic; /* поиск слова в словаре и вывод его значения */ do { if(!strcmp(*р, word)) { puts("Значение:”); puts(*(p+1)); break; } if(!strcmp(*p, word)) break; p = p + 2; /* продвижение по списку */ } while(*p); if(!*p) puts("Слово в словаре отсутствует."); printf("Будете еще вводить? (у/п): "); scanf(" %с%*с", &ch); } while(toupper(ch) != 'N'); return 0; H Форматный ввод/вывод на консоль Функции printf О и scanf () выполняют форматный ввод и вывод, то есть они могут читать и писать данные в разных форматах. Данные на консоль выводит printf (). А ее “дополнение”, функция scanf (), наоборот— считывает данные с клавиатуры. Обе функции могут работать с любым встроенным типом данных, а так- же с символьными строками, которые завершаются символом конца строки ( 'О' ). Н printf О Вот прототип функции printf (): int printf(const char * управляющая_строка, Функция printf () возвращает число выведенных символов или отрицательное значение в случае ошибки. Управляющая-Строка1 состоит из элементов двух видов. Первый из них — это сим- волы, которые предстоит вывести на экран; второй — это спецификаторы преобразова- ния1, которые определяют способ вывода стоящих за ними аргументов. Каждый такой спецификатор начинается со знака процента, за которым следует код формата. Аргу- ментов должно быть ровно столько, сколько и спецификаторов, причем спецификато- 1 2 1 Часто называется просто форматной строкой, форматным стрингом или форматом. — Прим. ред. 2 Называются также спецификациями формата. — Прим. ред. Глава 8. Ввод/вывод на консоль 201
ры преобразования и аргументы должны попарно соответствовать друг другу в на- правлении слева направо. Например, в результате такого вызова printf () | printf("Мне нравится язык %с %s", ’С’, "и к тому же очень сильно!"); будет выведено | Мне нравится язык С и к тому же очень сильно! В этом примере первому спецификатору преобразования (%с), соответствует сим- вол 'С, а второму (%s), — строка "и к тому же очень сильно!". В функции printf (), как видно из табл. 8.2, имеется широкий набор специфика- торов преобразования. Таблица 8.2. Спецификаторы преобразования для функции printf () Код_____Формат_______________________________________________________________________ %а Шестнадцатеричное в виде Qxh.hhhhp+d (только С99) %А Шестнадцатеричное в виде QXh.hhhhP+d (только С99) %с Символ %d Десятичное целое со знаком %i Десятичное целое со знаком %е Экспоненциальное представление ('е' на нижнем регистре) %Е Экспоненциальное представление ('Е* на верхнем регистре) %f Десятичное с плавающей точкой %д В зависимости от того, какой вывод будет короче, используется %е или %f %G В зависимости от того, какой вывод будет короче, используется %Е или %F %о Восьмеричное без знака %s Строка символов %и Десятичное целое без знака %х Шестнадцатеричное без знака (буквы на нижнем регистре) %Х Шестнадцатеричное без знака (буквы на верхнем регистре) %р Выводит указатель %п Аргумент, соответствующий этому спецификатору, должен быть указателем на цело- численную переменную. Спецификатор позволяет сохранить в этой переменной количе- ство записанных символов (записанных до того места, в котором находится код %п) % % Выводит знак % Вывод символов Для вывода отдельного символа используйте % с. В результате соответствующий аргумент будет выведен на экран без изменения. Для вывода строки используйте %s. Вывод чисел Числа в десятичном формате со знаком отображаются с помощью спецификатора пре- образования %d или %i. Эти спецификаторы преобразования эквивалентны; оба поддер- живаются в силу сложившихся привычек программистов, например, из-за желания под- держивать те же спецификаторы, которые применяются в функции scanf (). Для вывода целого значения без знака используйте % и. Спецификатор преобразования %f дает возможность выводить числа в формате с плавающей точкой. Соответствующий аргумент должен иметь тип double. 202 Часть I. Основы языка С
Спецификаторы преобразования %е и %Е в функции printf () позволяют отобра- жать аргумент типа double в экспоненциальном формате. В общем виде числа в та- ком формате выглядят следующим образом: x.dddddE+/-yy Чтобы отобразить букву Е в верхнем регистре, используйте спецификатор преобра- зования %е; в противном случае используйте спецификатор преобразования %е. Спецификатор преобразования %д или %G указывает, что функции printf () не- обходимо выбрать один из спецификаторов: %f или %е. В результате printf () выбе- рет тот спецификатор преобразования, который позволяет сделать самый короткий вывод. Если нужно, чтобы при выборе экспоненциального формата буква Е отобра- жалась на верхнем регистре, используйте спецификатор преобразования %G; в про- тивном случае используйте спецификатор преобразования %д. Применение спецификатора преобразования %д показано в следующей программе:. tfinclude <stdio.h> int main(void) { double f; for(f=1.0; f<1.0e+10; f=f*10) printf("%g ", f); return 0; В результате выполнения получится следующее: I 1 10 100 1000 10000 100000 1е+06 1е+07 1е+08 1е+09 Целые числа без знака можно выводить в восьмеричном или шестнадцатеричном формате, используя спецификатор преобразования % о или %х. Так как в шестнадца- теричной системе для представления чисел от 10 до 15 используются буквы от А до F, то эти буквы можно выводить на верхнем или на нижнем регистре. Как показано ни- же, в первом случае используется спецификатор преобразования %х, а во втором — спецификатор преобразования %х: #include <stdio.h> int main(void) { unsigned num; for (num=0; num < 16; num++) { printf("%o ", num); printf(”%x ”, num); printf("%Xn”, num); return 0; Вот что вывела эта программа: 0 0 0 111 2 2 2 3 3 3 4 4 4 Глава 8. Ввод/вывод на консоль 203
5 5 5 6 6 6 7 7 7 10 8 8 11 9 9 12 a A 13 b В 14 с C 15 d D 16 e E 17 f F Отображение адреса Для отображения адреса используйте спецификатор преобразования %р. Этот спе- цификатор преобразования дает printf () указание отобразить машинный адрес в формате, совместимом с адресацией, которая используется компьютером. Следующая программа отображает адрес переменной sample: #include <stdio.h> int sample; int main(void) { printf("%p", &sample); return 0; } Спецификатор преобразования %n Спецификатор %n довольно значительно отличается от остальных спецификаторов пре- образования. Когда функция printf () встречает его, ничто не выводится. Вместо этого выполняется совсем другое действие: в целую переменную, указанную соответствующим аргументом функции, записывается количество выведенных символов. Другими словами, значение, которое соответствует спецификатору преобразования %п, должно быть указате- лем на переменную. После завершения вызова printf () в этой переменной будет хра- ниться количество символов, выведенных до того момента, когда встретился спецификатор преобразования %п. Чтобы уяснить смысл этого несколько необычного спецификатора преобразования, разберитесь, как работает следующая программа: #include <stdio.h> int main(void) { int count; printf("Это%п проверкап", &count); printf("%d", count); return 0; } Программа отображает строку Это проверка, после которой появляется число 3. Спецификатор преобразования %п в основном используется в программе для выпол- нения динамического форматирования. 204 Часть I. Основы языка С
Модификаторы формата Во многих спецификаторах преобразования можно указать модификаторы1, кото- рые слегка меняют их значение. Например, можно указывать минимальную ширину поля, количество десятичных разрядов и выравнивание по левому краю. Модифика- тор формата помещают между знаком процента и кодом формата. Об этих модифика- торах сейчас и пойдет речь. Модификатор минимальной ширины поля Целое число, расположенное между знаком % и кодом формата, играет роль модифи- катора минимальной ширины поля. Если указан модификатор минимальной ширины поля, то чтобы ширина поля вывода была не меньше указанной минимальной длины, при необ- ходимости вывод будет дополнен пробелами. Если же выводятся строки или числа, кото- рые длиннее указанного минимума, то они все равно будут отображаться полностью. По умолчанию для дополнения используются пробелы. А если для этого надо использовать нули, то перед модификатором ширины поля следует поместить 0. Например, %05d озна- чает, что любое число, количество цифр которого меньше пяти, будет дополнено таким количеством нулей, чтобы число состояло из пяти цифр. В следующей программе показа- но, как применяется модификатор минимальной ширины поля: tfinclude <stdio.h> int main(void) { double item; item = 10.12304; printf("%fn", item); printf("%10fn", item); printf("%012fn", item); return 0; } Вот что выводится при выполнении этой программы: 110.123040 10.123040 00010.123040 Модификатор минимальной ширины поля чаще всего используется при создании таблиц, в которых столбцы должны быть выровнены по вертикали. Например, сле- дующая программа выводит таблицу квадратов и кубов чисел от 1 до 19: #include <stdio.h> int main(void) { int i; /* вывод таблицы квадратов и кубов */ for(i=l; i<20; i++) printf("%8d %8d %8dn", i, i*i, i*i*i); return 0; } 1 Называются также спецификаторами. — Прим. ред. Глава 8. Ввод/вывод на консоль 205
А вот пример полученного с ее помощью вывода: 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 б 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 11 121 1331 12 144 1728 13 169 2197 14 196 2744 15 225 3375 16 256 4096 17 289 ’ 4913 18 324 5832 19 361 6859 Модификатор точности Модификатор точности следует за модификатором минимальной ширины поля (если таковой имеется). Он состоит из точки и расположенного за ней целого числа. Значение этого модификатора зависит от типа данных, к которым его применяют. Когда модификатор точности применяется к данным с плавающей точкой, для преобразования которых используются спецификаторы преобразования %f, %е или %Е, то он определяет количество выводимых десятичных разрядов. Например, %10.4f оз- начает, что ширина поля вывода будет не менее 10 символов, причем для десятичных разрядов будет отведено четыре позиции. Если модификатор точности применяется к %д или %G, то он определяет количест- во значащих цифр. Примененный к строкам, модификатор точности определяет максимальную длину поля. Например, %5.7s означает, что длина выводимой строки будет составлять ми- нимум пять и максимум семь символов. Если строка окажется длиннее, чем макси- мальная длина поля, то конечные символы выводиться не будут. Если модификатор точности применяется к целым типам, то он определяет минималь- ное количество цифр, которые будут выведены для каждого из чисел. Чтобы получилось требуемое количество цифр, добавляется некоторое количество ведущих нулей. В следующей программе показано, как можно использовать модификатор точности: tfinclude <stdio.h> int main(void) { printf("%.4fn", 123.1234567); printf("%3.8dn", 1000); printf("%10.15sn"t "Это простая проверка."); return 0; } Вот что выводится при выполнении этой программы: I 123.1235 I 00001000 I Это простая про 206 Часть I. Основы языка С
Выравнивание вывода По умолчанию весь вывод выравнивается по правому краю. То есть если ширина поля больше ширины выводимых данных, то эти данные располагаются по правому краю поля. Вывод по левому краю можно назначить принудительно, поместив знак минус прямо за %. Например, %-10.2f означает, что число с плавающей точкой и с двумя десятичными разрядами будет выровнено по левому краю 10-символьного поля. В следующей программе показано, как применяется выравнивание по левому краю: #include <stdio.h> int main(void) { printf ("........................n") ; printf("по правому краю: %8dn", 100); printf(" по левому краю: %-8dn", 100); return 0; И вот что получилось: I по правому краю: 100 I по левому краю: 100 Обработка данных других типов Некоторые модификаторы в вызове функции printf () позволяют отображать це- лые числа типа short и long. Такие модификаторы можно использовать для следую- щих спецификаторов типа: d, i, о, и и х. Модификатор 1 (эль) в вызове функции printf () указывает, что за ним следуют данные типа long. Например, % Id означает, что надо выводить данные типа long int. После модификатора h функция printf () выведет целое значение в виде short. Например, %hu означает, что выводимые дан- ные имеют тип short unsigned int. Модификаторы 1 и h можно также применить к спецификатору п. Это делается с той целью, чтобы показать — соответствующий аргумент является указателем соответ- ственно на длинное (long) или короткое (short) целое. Если компилятор поддерживает обработку символов в расширенном 16-битном ал- фавите, добавленную Поправкой 1 от 1995 года (1995 Amendment 1), то для указания символа в расширенном 16-битном алфавите вы можете применять модификатор 1 для спецификатора преобразования с. Кроме того, для указания строки из символов в рас- ширенном 16-битном алфавите можно применять модификатор 1 для спецификатора преобразования s. Модификатор L может находиться перед спецификаторами преобразования с пла- вающей точкой е, f и д, и указывать этим, что преобразуется значение long double. В Стандарте С99 вводится два новых модификатора формата: hh и 11. Модифика- тор hh можно применять для спецификаторов преобразования d, i, о, и, х или п. Он показывает, что соответствующий аргумент является значением signed или unsigned char или, в случае п, указателем на переменную signed char. Модификатор 11 так- же можно применять для спецификаторов преобразования d, i, о, и, х или п. Он по- казывает, что соответствующий аргумент является значением signed или unsigned long long int или, в случае п, указателем на long long int. В С99 также разреша- ется применять 1 для спецификаторов преобразования с плавающей точкой а, е, f и д; впрочем, это не дает никакого результата. Глава 8. Ввод/вывод на консоль 207
На заметку В составе С99 имеются некоторые дополнительные модификаторы типа для функцииprintf о; о них рассказывается в части II. Модификаторы * и # Для некоторых из своих спецификаторов преобразования функция printf () под- держивает два дополнительных модификатора: * и #. Непосредственное расположение # перед спецификаторами преобразования g, G, f, Е или е означает, что при выводе обязательно появится десятичная точка — даже если десятичных цифр нет. Если вы поставите # непосредственно перед х или X, то шестнадцатеричное число будет выведено с префиксом Ох. Если # будет непосредст- венно предшествовать спецификатору преобразования о, число будет выведено с ве- дущим нулем. К любым другим спецификаторам преобразования модификатор # применять нельзя. (В С99 модификатор # можно применять по отношению к преоб- разованию % а; это значит, что обязательно будет выведена десятичная точка.) Модификаторы минимальной ширины поля и точности можно передавать функ- ции printf () не как константы, а как аргументы. Для этого в качестве заполнителя используйте звездочку (*). При сканировании строки формата функция printf () бу- дет каждой звездочке * из этой строки ставить в соответствие очередной аргумент, причем в том порядке, в каком расположены аргументы. Например, при выполнении оператора, показанного на рис. 8.1, минимальная ширина поля будет равна 10 симво- лам, точность — 4, а отображаться будет число 123.3. В следующей программе показано применение обоих модификаторов # и *: #include <stdio.h> int main(void) { printf("%x %#xn", 10, 10); printf("%*.*f", 10, 4, 1234.34); return 0; } printf(”%*.*f”, 10, 4,123.3) Puc. 8.1. Обратите внимание на то, каким образом звездочке (*) ставится в соответ- ствие определенное значение S scanf () Функция scanf () — это программа ввода общего назначения, выполняющая ввод с консоли. Она может читать данные всех встроенных типов и автоматически преоб- разовывать числа в соответствующий внутренний формат, scanf () во многом выгля- дит как обратная к printf (). Вот прототип функции scanf () : int scanf(const char *управляющая_строка, ...); 208 Часть I. Основы языка С
Эта функция возвращает количество тех элементов данных, которым было успеш- но присвоено значение. В случае ошибки scanf() возвращает EOF. управляю- щая_строка определяет преобразование считываемых значений при записи их пере- менные, на которые указывают элементы списка аргументов. Управляющая строка состоит из символов трех видов: спецификаторов преобразования, разделителей, символов, не являющихся разделителями. Теперь поговорим о каждом из этих видов. Спецификаторы преобразования Каждый спецификатор формата ввода начинается со знака %, причем специфика- торы формата ввода сообщают функции scanf () тип считываемых данных. Перечень этих кодов (т.е. литер-спецификаторов) приведен в табл. 8.3. Спецификаторам преоб- разования в порядке слева направо ставятся в соответствие элементы списка аргумен- тов. Рассмотрим некоторые примеры. Ввод чисел Для чтения целого числа используйте спецификатор преобразования %d или %i. А для чтения числа с плавающей точкой, представленного в стандартном или экспонен- циальном виде, используйте спецификатор преобразования %е, %f или %д. (Кроме того, для чтения числа с плавающей точкой стандарт С99 разрешает использовать также спецификатор преобразования % а.) Таблица 8.3. Спецификаторы преобразования для функции scanf () Код Значение %а Читает значение с плавающей точкой (только С99) %с Читает одиночный символ %d Читает десятичное целое число %i Читает целое число как в десятичном, так и восьмеричном или шестнадцатеричном формате %е Читает число с плавающей точкой %f Читает число с плавающей точкой %д Читает число с плавающей точкой %о Читает восьмеричное число %s Читает строку %х Читает шестнадцатеричное число %р Читает указатель %п Принимает целое значение, равное количеству уже считанных символов %и Читает десятичное целое число без знака %[] Читает набор сканируемых символов % % Читает знак процента Функцию scanf () можно использовать для чтения целых значений в восьмерич- ной или шестнадцатеричной форме, применяя для этого соответственно команды форматирования % о и %х, последняя из которых может быть как на верхнем, так и на нижнем регистре. Когда вводятся шестнадцатеричные числа, то буквы от А до F, представляющие шестнадцатеричные цифры, должны быть на том же самом регистре, что и литера-спецификатор. Следующая программа читает восьмеричное и шестна- дцатеричное число: Глава 8. Ввод/вывод на консоль 209
ttinclude <stdio.h> int main(void) { int i, j; scanf(”%o%x", &i, &j); printf("%o %x", i, j); return 0; } Функция scanf () прекращает чтение числа тогда, когда встречается первый нечисло- вой символ. Ввод целых значений без знака Для ввода целого значения без знака используйте спецификатор формата % и. На- пример, операторы (unsigned num; scanf("%u", &num); выполняют считывание целого числа без знака и присваивают его переменной num. Чтение одиночных символов с помощью scanf() Как уже говорилось в этой главе, одиночные символы можно прочитать с помощью функции getchar () или какой-либо функции, родственной с ней. Для той же цели мож- но использовать также вызов функции scanf () со спецификатором формата % с. Но, как и большинство реализаций getchar (), функция scanf () при использовании специфика- тора преобразования % с обычно будет выполнять построчно буферизованный ввод. В ин- терактивной среде такая ситуация вызывает определенные трудности. При чтении одиночного символа символы разделителей читаются так же, как и любой другой символ, хотя при чтении данных других типов разделители интерпретируются как разделители полей. Например, при вводе с входного потока “х у” фрагмент кода | scanf("%с%с%с", &а, &Ь, &с); помещает символ х в а, пробел — в Ь, а символ у — вс. Чтение строк Для чтения из входного потока строки можно использовать функцию scanf () со спе- цификатором преобразования %s. Использование спецификатора преобразования %s за- ставляет scanf () читать символы до тех пор, пока не встретится какой-либо разделитель. Читаемые символы помещаются в символьный массив, на который указывает соответст- вующий аргумент, а после введенных символов еще добавляется символ конца строки ( 'О' ). Что касается scanf (), то таким разделителем может быть пробел, разделитель строк, та- буляция, вертикальная табуляция или подача страницы. В отличие от gets (), которая чи- тает строку, пока не будет нажата клавиша <ENTER>, scanf () читает строку до тех пор, пока не встретится первый разделитель. Это означает, что scanf () нельзя использовать для чтения строки “это испытание”, потому что после пробела процесс чтения прекратит- ся. Чтобы увидеть, как действует спецификатор %s, попробуйте при выполнении этой программы ввести строку “привет всем”: 210 Часть I. Основы языка С
ttinclude <stdio.h> int main(void) { char str[80]; printf("Введите строку: "); scanf("%s", str); printf("Вот ваша строка: %s", str); return 0; } Программа выведет только часть строки, то есть слово “привет”. Ввод адреса Для ввода какого-либо адреса памяти используйте спецификатор преобразования %р. Этот спецификатор преобразования заставляет функцию scanf () читать адрес в том формате, который определен архитектурой центрального процессора. Например, следующая программа вначале вводит адрес, а затем отображает то, что находится в памяти по этому адресу: ttinclude <stdio.h> int main(void) { char *p; printf("Введите адрес: "); scanf("%p", &p); printf("По адресу %p находится %cn", p, *p); return 0; } Спецификатор %n Спецификатор %n указывает, что scanf () должна поместить количество символов, считанных (до того момента, когда встретился %п) из входного потока в целую пере- менную, указанную соответствующим аргументом. Использование набора сканируемых символов Функция scanf () поддерживает спецификатор формата общего назначения, назы- ваемый набором сканируемых символов (scanset). Набор сканируем^ символов пред- ставляет собой множество символов. Когда scanf () обрабатывает такое множество, то вводит только те символы, которые входят в набор сканируемых символов. Читае- мые символы будут помещаться в массив символов, который указан аргументом, со- ответствующим набору сканируемых символов. Этот набор определяется следующим образом: все те символы, которые предстоит сканировать, помещают в квадратные скобки. Непосредственно перед открывающей квадратной скобкой должен находиться знак %. Например, следующий набор сканируемых символов дает указание scanf () сканировать только символы X, Y и Z: | %[XYZ] Глава 8. Ввод/вывод на консоль 211
При использовании набора сканируемых символов функция scanf () продолжает читать символы, помещая их в соответствующий массив символов, пока не встретится символ, не входящий в этот набор. При возвращении из scanf () в массиве символов будет находиться строка, состоящая из считанных символов, причем эта строка будет заканчиваться символом конца строки. Чтобы увидеть, как это все работает, запустите следующую программу: tfinclude <stdio.h> int main(void) { int i; char str[80], str2[80]; scanf("%d%[abcdefg]%s", &i, str, str2); printf("%d %s %s", i, str, str2); return 0; } Введите 123abcdtye, а затем нажмите клавишу <ENTER>. После этого программа вы- ведет 123 abed tye. Так как в данном случае Ч’ не входит в набор сканируемых симво- лов, то scanf () прекратила чтение символов в переменную str сразу после того, как встретился символ Ч’. Оставшиеся символы были помещены в переменную str2. Кроме того, можно указать набор сканируемых символов, работающий с точно- стью до наоборот; тогда первым символом в таком наборе должен быть Л. Этот сим- вол дает указание scanf () принимать любой символ, который «Обходит в набор ска- нируемых символов. В большинстве реализаций для указания диапазона можно использовать дефис. Например, указанный ниже набор сканируемых символов дает функции scanf () ука- зание принимать символы от А до Z: | % [A-Z] Следует обратить внимание на такой важный момент: набор сканируемых симво- лов чувствителен к регистру букв. Если нужно сканировать буквы и на верхнем, и на нижнем регистре, то их надо указывать отдельно для каждого регистра. Пропуск лишних разделителей Разделитель в управляющей строке дает scanf () указание пропустить в потоке ввода один или несколько начальных разделителей. Разделителями являются пробелы, табуляции, вертикальные табуляции, подачи страниц и разделители строк. В сущно- сти, один разделитель в управляющей строке заставляет scanf () читать, но не сохра- нять любое количество (в том числе и нулевое) разделителей, которые находятся пе- ред первым символом, не являющимся разделителем. Символы в управляющей строке, не являющиеся разделителями Если в управляющей строке находится символ, не являющийся разделителем, то функция scanf () прочитает символ из входного потока, проверит, совпадает ли про- читанный символ с указанным в управляющей строке, и в случае совпадения пропус- тит прочитанный символ. Например, "%d, %d" заставляет scanf () прочитать целое значение, прочитать запятую и пропустить ее (если это была запятая!), а затем прочи- 212 Часть I. Основы языка С
тать следующее целое значение. Если же указанный символ во входном потоке не бу- дет найден, то scanf () завершится. Когда нужно прочитать и отбросить знак процен- та, то в управляющей строке следует указать %%. Функции scanf() необходимо передавать адреса Для всех переменных, которые должны получить значения с помощью scanf (), должны быть переданы адреса. Это означает, что все аргументы должны быть указате- лями. Вспомните, что именно так в С создается вызов по ссылке и именно тогда функция может изменить содержимое аргумента. Например, для считывания целого значения в переменную count можно использовать такой вызов функции scanf (): | scanf("%d"r &count); Строки будут читаться в символьные массивы, а имя массива без индекса является адресом первого его элемента. Таким образом, чтобы прочитать строку в символьный массив, можно использовать оператор | scanf(”%s”, str); В этом случае str является указателем, и потому перед ним не нужно ставить оператор &. Модификаторы формата Как и printf (), функция scanf () дает возможность модифицировать некоторое число своих спецификаторов формата. В спецификаторах формата моно указать мо- дификатор максимальной длины поля. Это целое число, расположенное между % и спецификатором формата; оно ограничивает число символов, считываемых из этого поля. Например, чтобы считывать в переменную str не более 20 символов, пишите | scanf(”%20s”, str); Если поток ввода содержит больше 20 символов, то при следующем вызове функ- ций ввода считывание начнется после того места, где оно закончилось при предыду- щем вызове. Например, если вы в ответ на вызов scanf () из этого примера введете ABCDEFGHIJKLMNOPRSTUVWXYZ то в str из-за спецификатора максимальной ширины поля будет помещено только 20 символов, то есть символы вплоть до Т. Это значит, что оставшиеся символы UVWXYZ пока еще не прочитаны. При следующем вызове scanf (), например при выполнении оператора | scanf(”%s”, str); в str будут помещены буквы UVWXYZ. Ввод из поля может завершиться и до того, как будет достигнута максимальная длина поля — если встретится разделитель. В та- ком случае scanf () переходит к следующему полю. Чтобы прочитать длинное целое, перед спецификатором формата поместите 1 (эль). А для чтения короткого целого значения перед спецификатором формата следу- ет поместить h. Эти модификаторы можно использовать со следующими кодами фор- матов: d, i, о, и, х и п. По умолчанию спецификаторы f, е и g дают scanf () указание присваивать дан- ные переменной типа float. Если перед одним из этих спецификаторов будет поме- щен 1 (эль), то scanf () будет присваивать данные переменной типа double. Исполь- зование L дает scanf () указание, чтобы переменная, принимающая данные, имела тип long double. Глава 8. Ввод/вывод на консоль 213
Если в компиляторе предусмотрена обработка двухбайтовых символов1, добавлен- ных в язык С Поправкой 1 от 1995 года, то модификатор 1 можно также использовать с такими кодами формата, как с и s. 1 непосредственно перед с является признаком указателя на объект типа wchar_t. А 1 непосредственно перед s — признак указателя на массив элементов типа wchar_t. Кроме того, 1 также применяется для модифика- ции набора сканируемых символов, чтобы этот набор можно было использовать для двухбайтовых символов. В Стандарте С99, кроме перечисленных, предусмотрены также модификаторы 11 и hh, последний из которых можно применять к спецификаторам d, i, о, и, х или п. Он являет- ся признаком того, что соответствующий аргумент является указателем на значение типа signed или unsigned char. Кроме того, к спецификаторам d, i, о, и, х и п можно при- менять и 11. этот спецификатор является признаком того, что соответствующий аргумент является указателем на значение типа signed (или unsigned) long long int. В C99 для функции scanf () имеются еще и другие модификаторы типа; о них рассказывается в части II. На заметку Подавление ввода scanf () может прочитать поле, но не присваивать прочитанное значение никакой переменной; для этого надо перед литерой-спецификатором формата поля поставить звездочку, *. Например, когда выполняется оператор | scanf("%d%*c%d", &х, &у); можно ввести пару координат 10,10. Запятая будет прочитана правильно, но ничему не будет присвоена. Подавление присвоения особенно полезно тогда, когда нужно об- работать только часть того, что вводится. 1 Называются также символами в расширенном 16-битном алфавите или символами уникода. (Unicode (уникод) — 16-битовый стандарт кодирования символов, позволяющий представлять алфавиты всех существующих в мире языков.) — Прим. ред. 214 Часть I. Основы языка С
Полный справочник по Глава 9 Файловый ввод/вывод
В этой главе описана работа с файловой системой в языке С. Как уже говорилось в главе 8, в языке С система ввода/вывода реализуется с помощью библиотечных функций, а не ключевых слов. Благодаря этому система ввода/вывода является очень мощной и гибкой. Например, во время работы с файлами данные могут передаваться или в своем внутреннем двоичном представлении или в текстовом формате, то есть в более удобочитаемом виде. Это облегчает задачу создания файлов в нужном формате. S Файловый ввод/вывод в С и C++ Так как С является фундаментом C++, то иногда возникает путаница в отношени- ях его файловой системы с аналогичной системой C++. Во-первых, C++ поддержива- ет всю файловую систему С. Таким образом, при перемещении более старого С-кода в C++ нет необходимости менять все процедуры ввода/вывода. Во-вторых, следует иметь в виду, что в C++ определена своя собственная, объектно-ориентированная система ввода/вывода, в которую входят как функции, так и операторы ввода/вывода. В системе ввода/вывода C++ полностью поддерживаются все возможности аналогич- ной системы С и это делает излишней файловую систему языка С. Вообще говоря, при написании программ на языке C++ обычно более удобно использовать именно его систему ввода/вывода, но, если необходимо воспользоваться файловой системой языка С, то это также вполне возможно. И Файловый ввод/вывод в стандартном С и UNIX Первоначально язык С был реализован в операционной системе UNIX. Как таковые, ранние версии С (да и многие нынешние) поддерживают набор функций ввода/вывода, совместимый с UNIX. Этот набор иногда называют UNIX-подобной системой ввода/вывода или небуферизованной системой ввода/вывода. Однако когда С был стандартизован, то UNIX-подобные функции в него не вошли — в основном из-за того, что оказались лиш- ними. Кроме того, UNIX-подобная система может оказаться неподходящей для некоторых сред, которые могут поддерживать язык С, но не эту систему ввода/вывода. В этой главе говорится только о тех функциях ввода/вывода, которые определены в стандарте С. В предыдущих изданиях этой книги немного говорилось и о UNIX- подобной файловой системе. Но в течение того времени, которое прошло с выхода последнего издания, число случаев использования стандартных функций ввода/вывода устойчиво росло, а UNIX-подобных функций— устойчиво падало. И теперь боль- шинство программистов пользуются стандартными функциями — эти функции мож- но переносить во все среды (и даже в C++). А тем программистам, которым нужно пользоваться UNIX-подобными функциями, приходится обращаться к документации по имеющимся у них компиляторам. В Потоки и файлы Перед тем как начать изучение файловой системы языка С, необходимо уяснить, в чем разница между потоками и файлами. В системе ввода/вывода С для программ поддерживается единый интерфейс, не зависящий от того, к какому конкретному уст- ройству осуществляется доступ. То есть в этой системе между программой и устройст- вом находится нечто более общее, чем само устройство. Такое обобщенное устройство ввода или вывода (устройство более высокого уровня абстракции. — Прим, ред.) назы- 216 Часть I. Основы языка С
вается потоком, в то время как конкретное устройство называется файлом. (Впрочем, файл — тоже понятие абстрактное. — Прим, ред.) Очень важно понимать, каким обра- зом происходит взаимодействие потоков и файлов. Потоки Файловая система языка С предназначена для работы с самыми разными устройст- вами, в том числе терминалами, дисководами и накопителями на магнитной ленте. Даже если какое-то устройство сильно отличается от других, буферизованная файло- вая система все равно представит его в виде логического устройства, которое называ- ется потоком. Все потоки ведут себя похожим образом. И так как они в основном не зависят от физических устройств, то та же функция, которая выполняет запись в дис- ковый файл, может ту же операцию выполнять и на другом устройстве, например, на консоли. Потоки бывают двух видов: текстовые и двоичные. Текстовые потоки Текстовый поток — это последовательность символов. В стандарте С считается, что текстовый поток организован в виде строк, каждая из которых заканчивается сим- волом новой строки. Однако в конце последней строки этот символ не является обя- зательным. В текстовом потоке по требованию базовой среды могут происходить оп- ределенные преобразования символов. Например, символ новой строки может быть заменен парой символов — возврата каретки и перевода строки. Поэтому может и не быть однозначного соответствия между символами, которые пишутся (читаются), и теми, которые хранятся во внешнем устройстве. Кроме того, количество тех символов, которые пишутся (читаются), и тех, которые хранятся во внешнем устройстве, может также не совпадать из-за возможных преобразований. Двоичные потоки Двоичный поток — это последовательность байтов, которая взаимно однозначно соот- ветствует байтам на внешнем устройстве, причем никакого преобразования символов не происходит. Кроме того, количество тех байтов, которые пишутся (читаются), и тех, кото- рые хранятся на внешнем устройстве, одинаково. Однако в конце двоичного потока может добавляться определяемое приложением количество нулевых байтов. Такие нулевые байты, например, могут использоваться для заполнения свободного места в блоке памяти незна- чащей информацией, чтобы она в точности заполнила сектор на диске. Файлы В языке С файлом может быть все что угодно, начиная с дискового файла и закан- чивая терминалом или принтером. Поток связывают с определенным файлом, выпол- няя операцию открытия. Как только файл открыт, можно проводить обмен инфор- мацией между ним и программой. Но не у всех файлов одинаковые возможности. Например, к дисковому файлу прямой доступ возможен, в то время как к некоторым принтерам — нет. Таким обра- зом, мы пришли к одному важному принципу, относящемуся к системе ввода/вывода языка С: все потоки одинаковы, а файлы — нет. Если файл может поддерживать запросы на местоположение (указатель текущей позиции), то при открытии такого файла указатель текущей позиции в файле устанав- ливается в начало. При чтении из файла (или записи в него) каждого символа указа- тель текущей позиции увеличивается, обеспечивая тем самым продвижение по файлу. Файл отсоединяется от определенного потока (т.е. разрывается связь между фай- лом и потоком) с помощью операции закрытия. При закрытии файла, открытого с Глава 9. Файловый ввод/вывод 217
целью вывода, содержимое (если оно есть) связанного с ним потока записывается на внешнее устройство. Этот процесс, который обычно называют дозаписью1 потока, га- рантирует, что никакая информация случайно не останется в буфере диска. Если программа завершает работу нормально, т.е. либо main () возвращает управление опе- рационной системе, либо вызывается exit (), то все файлы закрываются автоматиче- ски. В случае аварийного завершения работы программы, например, в случае краха или завершения путем вызова abort (), файлы не закрываются. У каждого потока, связанного с файлом, имеется управляющая структура, содер- жащая информацию о файле; она имеет тип FILE. В этом блоке управления файлом1 никогда ничего не меняйте3. Если вы новичок в программировании, то разграничение потоков и файлов может показаться излишним или даже “заумным”. Однако надо помнить, что основная цель такого разграничения — это обеспечить единый интерфейс. Для выполнения всех операций ввода/вывода следует использовать только понятия потоков и применять всего лишь одну файловую систему. Ввод или вывод от каждого устройства автомати- чески преобразуется системой ввода/вывода в легко управляемый поток. Н Основы файловой системы Файловая система языка С состоит из нескольких взаимосвязанных функций. Са- мые распространенные из них показаны в табл. 9.1. Для их работы требуется заголо- вок <stdio. h>. Таблица 9.1. Часто используемые функции файловой системы С Имя Что делает fopen() fclose() putc() fputc() getc() fgetc() fgets() fputs() fseek() ftell() fprintf() fscanf() feof() ferror() rewind() remove() fflushQ Открывает файл Закрывает файл Записывает символ в файл То же, что и putc () Читает символ из файла То же, что и getc () Читает строку из файла Записывает строку в файл Устанавливает указатель текущей позиции на определенный байт файла Возвращает текущее значение указателя текущей позиции в файле Для файла то же, что printf () для консоли Для файла то же, что scanf () для консоли Возвращает значение true (истина), если достигнут конец файла Возвращает значение true, если произошла ошибка Устанавливает указатель текущей позиции в начало файла Стирает файл Дозапись потока в файл 1 Или принудительным освобождением (содержимого) буфера. — Прим. ред. 2 Блок управления файлом — небольшой блок памяти, временно выделенный операционной системой для хранения информации о файле, который был открыт для использования. Блок управления файлом обычно содержит информацию об идентификаторе файла, его расположе- нии на диске и указателе текущей позиции в файле. — Прим. ред. 3 Если, конечно, вы не разрабатываете систему ввода-вывода. — Прим. ред. 218 Часть I. Основы языка С
Заголовок <stdio.h> предоставляет прототипы функций ввода/вывода и опреде- ляет следующие три типа: size_t, fpos_t и FILE. size_t и fpos_t представляют собой определенные разновидности такого типа, как целое без знака. А о третьем ти- пе, FILE, рассказывается в следующем разделе. Кроме того, в <stdio.h> определяется несколько макросов. Из них к материалу ЭТОЙ главы ОТНОСЯТСЯ NULL, EOF, FOPEN_MAX, SEEK_SET, SEEK_CUR И SEEK_END. Мак- рос NULL определяет пустой (null) указатель. Макрос EOF, часто определяемый как -1, является значением, возвращаемым тогда, когда функция ввода пытается выполнить чтение после конца файла. fopen_max определяет целое значение, равное максималь- ному числу одновременно открытых файлов. Другие макросы используются вместе с fseek () — функцией, выполняющей операции прямого доступа к файлу. Указатель файла Указатель файла — это то, что соединяет в единое целое всю систему ввода/вывода языка С. Указатель файла— это указатель на структуру типа FILE. Он указывает на структуру, содержащую различные сведения о файле, например, его имя, статус и ука- затель текущей позиции в начало файла. В сущности, указатель файла определяет конкретный файл и используется соответствующим потоком при выполнении функ- ций ввода/вывода. Чтобы выполнять в файлах операции чтения и записи, программы должны использовать указатели соответствующих файлов. Чтобы объявить перемен- ную-указатель файла, используйте такого рода оператор: | FILE *fp; Открытие файла Функция fopen () открывает поток и связывает с этим потоком определенный файл. Затем она возвращает указатель этого файла. Чаще всего (а также в оставшейся части этой главы) под файлом подразумевается дисковый файл. Прототип функции f open () такой: FILE *fopen(const char *имя_файла, const char *режим)', где имя_файла — это указатель на строку символов, представляющую собой допусти- мое имя файла, в которое также может входить спецификация пути к этому файлу. Строка, на которую указывает режим, определяет, каким образом файл будет открыт. В табл. 9.2 показано, какие значения строки режим являются допустимыми. Строки, подобные “г+Ь” могут быть представлены и в виде “rb+”. Как уже упоминалось, функция fopen() возвращает указатель файла. Никогда не следует изменять значение этого указателя в программе. Если при открытии файла происходит ошибка, то f open () возвращает пустой (null) указатель. В следующем коде функция fopen() используется для открытия файла по имени TEST для записи. IFILE *fp; fp = fopen("test”t "w") ; Таблица 9.2. Допустимые значения режим Режим_______Что означает______________________________________________________ г Открыть текстовый файл для чтения w Создать текстовый файл для записи а Добавить в конец текстового файла rb Открыть двоичный файл для чтения wb Создать двоичный файл для записи Глава 9. Файловый ввод/вывод 219
Окончание табл. 9.2 Режим Что означает ab г+ W+ а+ Добавить в конец двоичного файла Открыть текстовый файл для чтения/записи Создать текстовый файл для чтения/записи Добавить в конец текстового файла или создать текстовый файл для чте- ния/записи г+Ь w+b a+b Открыть двоичный файл для чтения/записи Создать двоичный файл для чтения/записи Добавить в конец двоичного файла или создать двоичный файл для чтения/записи Хотя предыдущий код технически правильный, но его обычно пишут немного по-другому: FILE *fp; if ((fp = fopen("test", "w”) ) ==NULL) { printf ("Ошибка при открытии файла. n’’); exit(1); } Этот метод помогает при открытии файла обнаружить любую ошибку, например, защиту от записи или полный диск, причем обнаружить еще до того, как программа попытается в этот файл что-либо записать. Вообще говоря, всегда нужно вначале по- лучить подтверждение, что функция f open () выполнилась успешно, и лишь затем выполнять с файлом другие операции. Хотя название большинства файловых режимов объясняет их смысл, однако не поме- шает сделать некоторые дополнения. Если попытаться открыть файл только для чтения, а он не существует, то работа fopen() завершится отказом. А если попытаться открыть файл в режиме дозаписи, а сам этот файл не существует, то он просто будет создан. Более того, если файл открыт в режиме дозаписи, то все новые данные, которые записываются в него, будут добавляться в конец файла. Содержимое, которое хранилось в нем до открытия (если только оно было), изменено не будет. Далее, если файл открывают для записи, но выясняется, что он не существует, то он будет создан. А если он существует, то содержи- мое, которое хранилось в нем до открытия, будет утеряно, причем будет создан новый файл. Разница между режимами г+ и w+ состоит в том, что если файл не существует, то в режиме открытия г+ он создан не будет, а в режиме w+ все произойдет наоборот: файл будет создан! Более того, если файл уже существует, то открытие его в режиме w+ приведет к утрате его содержимого, а в режиме г+ оно останется нетронутым. Из табл. 9.2 видно, что файл можно открыть либо в одном из текстовых, либо в одном из двоичных режимов. В большинстве реализаций в текстовых режимах каждая комбина- ция кодов возврата каретки (ASCII 13) и конца строки (ASCII 10) преобразуется при вводе в символ новой строки. При выводе же происходит обратный процесс: символы новой строки преобразуются в комбинацию кодов возврата каретки (ASCII 13) и конца строки (ASCII 10). В двоичных режимах такие преобразования не выполняются. Максимальное число одновременно открытых файлов определяется FOPEN_MAX. Это значение не меньше 8, но чему оно точно равняется — это должно быть написано в документации по компилятору. Закрытие файла Функция fclose () закрывает поток, который был открыт с помощью вызова fopen () .Функция f close () записывает в файл все данные, которые еще оставались в дисковом буфере, и проводит, так сказать, официальное закрытие файла на уровне 220 Часть I. Основы языка С
операционной системы. Отказ при закрытии потока влечет всевозможные неприятно- сти, включая потерю данных, испорченные файлы и возможные периодические ошибки в программе. Функция fcloseO также освобождает блок управления фай- лом, связанный с этим потоком, давая возможность использовать этот блок снова. Так как количество одновременно открытых файлов ограничено, то, возможно, придется закрывать один файл, прежде чем открывать другой. Прототип функции f close () такой: int fclose(FILE *уф)', где уф — указатель файла, возвращенный в результате вызова f open (). Возвращение нуля означает успешную операцию закрытия. В случае же ошибки возвращается EOF. Чтобы точно узнать, в чем причина этой ошибки, можно использовать стандартную функцию ferrorO (о которой вскоре пойдет речь). Обычно отказ при выполнении fclose () происходит только тогда, когда диск был преждевременно удален (стерт) с дисковода или на диске не осталось свободного места. Запись символа В системе ввода/вывода языка С определяются две эквивалентные функции, пред- назначенные для вывода символов: putc () и fputc (). (На самом деле putc () обыч- но реализуется в виде макроса.) Две идентичные функции имеются просто потому, чтобы сохранять совместимость со старыми версиями С. В этой книге используется putc (), но применение fputc () также вполне возможно. Функция putc () записывает символы в файл, который с помощью fopen() уже открыт в режиме записи. Прототип этой функции следующий: int putc(int ch, FILE *уф, где уф — это указатель файла, возвращенный функцией f open (), a ch — выводимый символ. Указатель файла сообщает putc (), в какой именно файл следует записывать символ. Хотя ch и определяется как int, однако записывается только младший байт. Если функция putc () выполнилась успешно, то возвращается записанный сим- вол. В противном же случае возвращается EOF. Чтение символа Для ввода символа также имеются две эквивалентные функции: getc() и fgetc (). Обе определяются для сохранения совместимости со старыми версиями С. В этой книге используется get с () (которая обычно реализуется в виде макроса), но ес- ли хотите, применяйте fgetc (). Функция getc() записывает символы в файл, который с помощью fopen() уже открыт в режиме для чтения. Прототип этой функции следующий: int getc(FILE *уф)', где уф— это указатель файла, имеющий тип file и возвращенный функцией fopen (). Функция getc() возвращает целое значение, но символ находится в млад- шем байте. Если не произошла ошибка, то старший байт (байты) будет обнулен. Если достигнут конец файла, то функция getc () возвращает EOF. Поэтому, чтобы прочитать символы до конца текстового файла, можно использовать следующий код: Ido { ch = getc(fp); } while(ch!=EOF); Однако getc () возвращает EOF и в случае ошибки. Для определения того, что же на самом деле произошло, можно использовать ferror (). Глава 9. Файловый ввод/вывод 221
Использование fopen(), getc(), putc() и fclose() Функции f open (), getc (), putc () и f close () — это минимальный набор функ- ций для операций с файлами. Следующая программа, KTOD, представляет собой про- стой пример, в котором используются только функции putc (), fopen () и f close (). В этой программе символы считываются с клавиатуры и записываются в дисковый файл до тех пор, пока пользователь не введет знак доллара. Имя файла определяется в командной строке. Например, если вызвать программу KTOD, введя в командной строке KTOD TEST, то строки текста будут вводиться в файл TEST. /* KTOD: программа ввода с клавиатуры на диск. */ #include <stdio.h> ttinclude <stdlib.h> int main(int argc, char *argv[]) { FILE *fp; char ch; if(argc!=2) { printf("Вы забыли ввести имя файла.п") ; exit(1); } if((fp=fopen(argv[1], "w"))==NULL) { printf("Ошибка при открытии файла.n"); exit(1); } do { ch = getchar(); putc(ch, fp); } while (ch != ’$’); fclose(fp); return 0; } Программа DTOS, являющаяся дополнением к программе KTOD, читает любой текстовый файл и выводит его содержимое на экран. /* DTOS: программа, которая читает файлы и выводит их на экран. */ #include <stdio.h> #include <stdli-b.h> int main(int argc, char *argv[]) { FILE *fp; char ch; if(argc!=2) { printf("Вы забыли ввести имя файла.n"); exit(1); } if((fp=fopen(argv[1], "r"))==NULL) { printf("Ошибка при открытии файла.n"); 222 Часть I. Основы языка С
exit (1); } ch = getc(fp); /* чтение одного символа */ while (ch!=EOF) { putchar(ch); /* вывод на экран */ ch = getc(fp); } fclose(fp); return 0; } Испытывая эти две программы, вначале с помощью KTOD создайте текстовый файл, а затем с помощью DTOS прочитайте его содержимое. Использование feof () Как уже говорилось, если достигнут конец файла, то getc О возвращает EOF. Однако проверка значения, возвращенного getc (), возможно, не является наилучшим способом узнать, достигнут ли конец файла. Во-первых, файловая система языка С может работать как с текстовыми, так и с двоичными файлами. Когда файл открывается для двоичного ввода, то может быть прочитано целое значение, которое, как выяснится при проверке, равняется EOF. В таком случае программа ввода сообщит о том, что достигнут конец фай- ла, чего на самом деле может и не быть. Во-вторых, функция getc () возвращает EOF и в случае отказа, а не только тогда, когда достигнут конец файла. Если использовать только возвращаемое значение getc (), то невозможно определить, что же на самом деле про- изошло. Для решения этой проблемы в С имеется функция feof (), которая определяет, достигнут ли конец файла. Прототип функции feof () такой: int feof(FILE *уф); Если достигнут конец файла, то feof () возвращает true (истина); в противном же случае эта функция возвращает нуль. Поэтому следующий код будет читать двоичный файл до тех пор, пока не будет достигнут конец файла: | while(!feof(fp)) ch = getc(fp); Ясно, что этот метод можно применять как к двоичным, так и к текстовым файлам. В следующей программе, которая копирует текстовые или двоичные файлы, име- ется пример применения feof (). Файлы открываются в двоичном режиме, а затем feof () проверяет, не достигнут ли конец файла. /* Копирование файла. */ #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { FILE *in, *out; char ch; if(argc!=3) { printf(”Вы забыли ввести имя файла.п”); exit (1); } if((in=fopen(argv[1], ”rb”))==NULL) { Глава 9. Файловый ввод/вывод 223
printf("Нельзя открыть исходный файл.п"); exit(1); } if((out=fopen(argv[2], "wb")) == NULL) { printf("Нельзя открыть файл результатов.n"); exit(1); } /* Именно этот код копирует файл. */ while(!feof(in)) { ch = getc(in); if(!feof(in)) putc(ch, out); } fclose(in); fclose(out); return 0; } Ввод/вывод строк: fputs() и fgets() Кроме getc () и putc (), в языке С также поддерживаются родственные им функ- ции fgets () и fputs (). Первая из них читает строки символов из файла на диске, а вторая записывает строки такого же типа в файл, тоже находящийся на диске. Эти функции работают почти как putc() и getc О, но читают и записывают не один символ, а целую строку. Прототипы функций fgets () и fputs () следующие: int fputs(const char *стр, FILE *уф)', char *fgets(char *cmp, int длина, FILE *уф); Функция fputs () пишет в определенный поток строку, на которую указывает стр. В случае ошибки эта функция возвращает EOF. Функция fgets () читает из определенного потока строку, и делает это до тех пор, пока не будет прочитан символ новой строки или количество прочитанных символов не станет равным длина-. Если был прочитан разделитель строк, он записывается в строку, чем функция fgets () отличается от функции gets (). Полученная в резуль- тате строка будет оканчиваться символом конца строки ( ’0’ ). При успешном завер- шении работы функция возвращает стр, а в случае ошибки — пустой указатель (null). В следующей программе показано использование функции fputs (). Она читает строки с клавиатуры и записывает их в файл, который называется TEST. Чтобы за- вершить выполнение программы, введите пустую строку. Так как функция gets () не записывает разделитель строк, то его приходится специально вставлять перед каждой строкой, записываемой в файл; это делается для того, чтобы файл было легче читать: #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char str[80]; FILE *fp; if((fp = fopen("TEST", "w"))==NULL) { printf("Ошибка при открытии файла.n"); exit (1); } 224 Часть I. Основы языка С
do { printf("Введите строку (пустую — для выхода):п”); gets(str); strcat(str, "n"); /* добавление разделителя строк */ fputs(str, fp) ; } while(*str!=*n’); return 0; } Функция rewind() Функция rewind () устанавливает указатель текущей позиции в файле на начало файла, указанного в качестве аргумента этой функции. Иными словами, функция rewind () выполняет “перемотку” (rewind) файла. Вот ее прототип: void rewind(FILE *уф)*9 где уф — это допустимый указатель файла. Чтобы познакомиться с rewind (), изменим программу из предыдущего раздела таким образом, чтобы она отображала содержимое файла сразу после его создания. Чтобы выполнить отображение, программа после завершения ввода “перематывает” файл, а затем с помощью fback () читает его с самого начала. Обратите внимание, что сейчас файл необходимо открыть в режиме чтения/записи, используя в качестве аргумента, задающего режим, строку “w+”. #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char str[80]; FILE *fp; if((fp = fopen("TEST”, "w+”))==NULL) { printf("Ошибка при открытии файла.n”); exit(1); } do { printf("Введите строку (пустую — для выхода):n"); gets(str); strcat(str, "n"); /* ввод разделителя строк */ fputs(str, fp); } while(*str!=•n'); /* теперь выполняется чтение и отображение файла */ rewind(fp); /* установить указатель текущей позиции на начало файла ★/ while(!feof(fp)) { fgets(str, 79, fp) ; printf(str); } return 0; } Глава 9. Файловый ввод/вывод 225
Функция ferror() Функция ferror () определяет, произошла ли ошибка во время выполнения опе- рации с файлом. Прототип этой функции следующий: int ferror(FILE *уф)', где уф — допустимый указатель файла. Она возвращает значение true (истина), если при последней операции с файлом произошла ошибка; в противном же случае она возвращает false (ложь). Так как при любой операции с файлом устанавливается свое условие ошибки, то после каждой такой операции следует сразу вызывать ferror (), а иначе данные об ошибке могут быть потеряны. В следующей программе показано применение ferror (). Программа удаляет та- буляции из файла, заменяя их соответствующим количеством пробелов. Размер табу- ляции определяется макросом TAB_SIZE. Обратите внимание, что ferrorO вызыва- ется после каждой операции с файлом. При запуске этой программы указывайте в командной строке имена входного и выходного файлов. /* Программа заменяет в текстовом файле символы табуляции пробелами и отслеживает ошибки */ #include <stdio.h> #include <stdlib.h> #define TAB_SIZE 8 #define IN 0 #define OUT 1 void err(int e); int main(int argc, char *argv[]) { FILE *in, *out; int tab, i; char ch; if(argc!=3) { printf("синтаксис: detab <входной_файл> <выходной_файл>п"); exit(1); } if((in = fopen(argv[1], "rb"))==NULL) { printf("Нельзя открыть %s.n", argv[l]); exit(1); } if((out = fopen(argv[2], "wb")) == NULL) { printf("Нельзя открыть %s.n", argv[2]); exit(1); } tab = 0; do { ch = getc(in); if(ferror(in)) err(IN); /* если найдена табуляция, выводится соответствующее число пробелов */ if(ch==’t’) { for(i=tab; i<8; i++) { 226 Часть I. Основы языка С
putc(’ ’f out) ; if(ferror(out)) err(OUT); } tab = 0; } else { putc(ch, out); if(ferror(out)) err(OUT); tab++; if(tab==TAB_SIZE) tab = 0; if(ch=='n' || ch==’r’) tab = 0; } } while(!feof(in)); fclose(in); fclose(out); return 0; } void err(int e) { if(e==IN) printf("Ошибка при вводе.n"); else printf("Ошибка при выводе.n"); exit(1); } Стирание файлов Функция remove () стирает указанный файл. Вот ее прототип: int remove(const char ^имя_файла) В случае успешного выполнения эта функция возвращает нуль, а в противном слу- чае — ненулевое значение. Следующая программа стирает файл, указанный в командной строке. Однако вна- чале она дает возможность передумать. Утилита, подобная этой, может пригодиться компьютерным пользователям-новичкам. /* Двойная проверка перед стиранием. */ #include <stdio.h> #include <stdlib.h> tfinclude <ctype.h> int main(int argc, char *argv[]) { char str[80]; if(argc!=2) { printf("синтаксис: xerase <имя_файла>п") ; exit(1); } printf("Стереть %s? (Y/N): ", argv[l]); gets(str); if(toupper(*str)==’Y’) if(remove(argv[1]) ) { printf("Нельзя стереть файлАп"); exit (1); Глава 9. Файловый ввод/вывод 227
1} return 0; } Дозапись потока Для дозаписи содержимого выводного потока в файл применяется функция fflush (). Вот ее прототип: int fflush(FILE *уф); Эта функция записывает все данные, находящиеся в буфере в файл, который ука- зан с помощью уф. При вызове функции fflushO с пустым (null) указателем файла уф будет выполнена дозапись во все файлы, открытые для вывода. После своего успешного выполнения fflushO возвращает нуль, в противном случае — EOF. В Функции fread() и fwrite() Для чтения и записи данных, тип которых может занимать более 1 байта, в файло- вой системе языка С имеется две функции: fread О и fwrite О. Эти функции по- зволяют читать и записывать блоки данных любого типа. Их прототипы следующие: size_t fread(void *буфер, size_t колич_байт, size_t счетчик, FILE *уф)', size_t fwrite(const void * буфер, size_t колич_байт, size_t счетчик, FILE *уф)‘, Для f read () буфер — это указатель на область памяти, в которую будут прочитаны данные из файла. А для fwrite О буфер— это указатель на данные, которые будут записаны в файл. Значение счетчик определяет, сколько считывается или записывает- ся элементов данных, причем длина каждого элемента в байтах равна количбайт. (Вспомните, что тип size_t определяется как одна из разновидностей целого типа без знака.) И, наконец, уф — это указатель файла, то есть на уже открытый поток. Функция fread () возвращает количество прочитанных элементов. Если достигнут конец файла или произошла ошибка, то возвращаемое значение может быть меньше, чем счетчик. А функция fwrite () возвращает количество записанных элементов. Ес- ли ошибка не произошла, то возвращаемый результат будет равен значению счетчик. Использование fread() и fwriteQ Как только файл открыт для работы с двоичными данными, f read () и fwrite () соответственно могут читать и записывать информацию любого типа. Например, сле- дующая программа записывает в дисковый файл данные типов double, int и long, а затем читает эти данные из того же файла. Обратите внимание, как в этой программе при определении длины каждого типа данных используется функция sizeof (). /* Запись несимвольных данных в дисковый файл и последующее их чтение. */ #include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; double d = 12.23; int i = 101; 228 Часть I. Основы языка С
long 1 = 123023L; if((fp=fopen("test", ”wb+”))==NULL) { printf("Ошибка при открытии файла.n"); exit(1) ; } fwrite(&d, sizeof(double), 1, fp); fwrite(&i, sizeof(int), 1, fp); fwrite(&l, sizeof(long), 1, fp); rewind(fp); fread(&d, sizeof(double), 1, fp); fread(&i, sizeof(int), 1, fp); fread(&l, sizeof(long), 1, fp); printf("%f %d %ld", d, i, 1); fclose(fp); return 0; } Как видно из этой программы, в качестве буфера можно использовать (и часто именно так и делают) просто память, в которой размещена переменная. В этой простой программе значения, возвращаемые функциями fread() и fwriteO, игнорируются. Однако на практике эти значения необходимо проверять, чтобы обнаружить ошибки. Одним из самых полезных применений функций fread() и fwriteO является чтение и запись данных пользовательских типов, особенно структур. Например, если определена структура I struct struct_type { float balance; char name[80]; } oust; то следующий оператор записывает содержимое oust в файл, на который указывает fp: | fwrite(&cust, sizeof(struct struct_type), 1, fp); Пример co списком рассылки Чтобы показать, как можно легко записывать большие объемы данных, пользуясь функциями fread() и fwriteO, мы переделаем программу работы со списком рас- сылки, с которой впервые встретились в главе 7. Усовершенствованная версия сможет сохранять адреса в файле. Как и раньше, адреса будут храниться в массиве структур следующего типа: struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr__list [MAX] ; Значение max определяет максимальное количество адресов, которое может быть в списке. Глава 9. Файловый ввод/вывод 229
При выполнении программы поле паше каждой структуры инициализируется пустым указателем (NULL). В программе свободной считается та структура, поле паше которой содержит строку нулевой длины, т.е. имя адресата представляет собой пустую строку. Далее приведены функции save () и load (), которые используются соответствен- но для сохранения и загрузки базы данных (списка рассылки). Обратите внимание, насколько кратко удалось закодировать каждую из функций, а ведь эта краткость дос- тигнута благодаря мощи freadO и fwriteO! И еще обратите внимание на то, как эти функции проверяют значения, возвращаемые функциями freadO и fwriteO, чтобы обнаружить таким образом возможные ошибки. /* Сохранение списка. */ void save(void) { FILE *fp; register int i; if((fp=fopen("maillist", "wb"))==NULL) { printf("Ошибка при открытии файла.n"); return; } for(i=0; i<MAX; i++) if(*addr_list[i].name) if(fwrite(&addr_list[i], sizeof(struct addr), 1, fp)!=1) printf("Ошибка при записи файла.n"); fclose(fp); } /* Загрузка файла. */ void load(void) { FILE *fp; register int i; if((fp=fopen("maillist", "rb"))==NULL) { printf("Ошибка при открытии файла.n"); return; } init__list () ; for(i=0; i<MAX; i++) if (fread (&addr__list [i], sizeof(struct addr), 1, fp)!=1) { if(feof(fp)) break; printf("Ошибка при чтении файла.n"); } fclose(fp); } Обе функции, save () и load (), подтверждают (или не подтверждают) успешность выполнения функциями freadO и fwriteO операций с файлом, проверяя значе- ния, возвращаемые функциями freadO и fwriteO. Кроме того, функция load() явно проверяет, не достигнут ли конец файла. Делает она это с помощью вызова функции feof (). Это приходится делать потому, что freadO и в случае ошибки, и при достижении конца файла возвращает одно и то же значение. 230 Часть I. Основы языка С
Далее показана вся программа, обрабатывающая списки рассылки. Ее можно ис- пользовать как ядро для дальнейших расширений, в нее, например, можно добавить средства поиска адресов. /* Простая программа обработки списка рассылки, в которой используется массив структур */ #include <stdio.h> tfinclude <stdlib.h> tfdefine MAX 100 struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; void init_list(void), enter(void); void delete(void), list(void); void load(void), save(void); int menu_select(void), find_free(void); int main(void) { char choice; init_list(); /* инициализация массива структур */ for(;;) { choice = menU—Select(); switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list (); break; case 4: save(); break; case 5: load(); break; case 6: exit(0); } } return 0; } /* Инициализация списка. */ void init_list(void) { register int t; for(t=0; t<MAX; ++t) addr_list[t].name[0] = ’’ ; } /* Получение значения, выбранного в меню */ int menu_select(void) Глава 9. Файловый ввод/вывод 231
{ char s[80]; int c; printf("1. Ввести имяп"); printf("2. Удалить имяп"); printf(”3. Вывести списокп"); printf("4. Сохранить файлп"); printf("5. Загрузить файлп"); printf("6. ВыходХп"); do { printf("ХпВведите номер нужного пункта: ”); gets (s) ; с = atoi (s) ; } while (с<0 | | Об) ; return с; } /* Добавление адреса в список. */ void enter(void) { int slot; char s [80]; slot = find_free(); if(slot==-l) { printf("ХпСписок заполнен"); return; } printf("Введите имя: ”) ; gets(addr_list[slot].name); printf("Введите улицу: "); gets(addr_list[slot].street); printf("Введите город: ”); gets(addr_list[slot].city); printf("Введите штат: "); gets(addr_list[slot].state); printf("Введите почтовый индекс: ") ; gets(s); addr_list[slot].zip = strtoul(sz ••z 10); } /* Поиск свободной структуры. */ int find—free(void) { register int t; for(t=0; addr_list[t].name[0] && t<MAX; ++t) ; if(t==MAX) return -1; /* свободных структур нет */ return t; } 232 Часть I. Основы языка С
/* Удаление адреса. */ void delete(void) { register int slot; char s [80] ; printf (’’Введите N’ записи: ”) ; gets (s); slot = atoi (s); if(slot>=0 && slot < MAX) addr_list[slot].name[0] = ’’; } /* Вывод списка на экран. */ void list(void) { register int t; for(t=0; t<MAX; ++t) { if(addr_list[t].name[0]) { printf(”%sn”, addr_list[t].name); printf(”%sn”, addr_list[t].street); printf("%sn”, addr_list[t].city); printf(”%sn", addr_list[t].state); printf(”%lunn", addr_list[t].zip); } } printf(”nn”); } /* Сохранение списка. */ void save(void) { FILE *fp; register int i; if ((fp=fopen (’’maillist”, "wb”) ) ==NULL) { printf (’’Ошибка при открытии файла. n”); return; } for(i=0; i<MAX; i++) if(*addr_list[i].name) if(fwrite(&addr_list[i], sizeof(struct addr), 1, fp)!=1) printf (’’Ошибка при записи файла. n’’); fclose(fp); } /* Загрузить файл. */ void load(void) { FILE *fp; register int i; if ((fp=fopen (’’maillist”, "rb”) ) ==NULL) { Глава 9. Файловый ввод/вывод 233
printf (’’Ошибка при открытии файла. п”) ; return; init_list(); for(i=0; i<MAX; i++) if(fread(&addr_list[i], sizeof(struct addr), 1, fp)!=l) { if(feof(fp)) break; printf (’’Ошибка при чтении файла. n"); } fclose(fp); } HI Ввод/вывод при прямом доступе: функция fseek() При прямом доступе можно выполнять операции ввода/вывода, используя систему ввода/вывода языка С и функцию fseek(), которая устанавливает указатель текущей позиции в файле. Вот прототип этой функции: int fseek(FILE *уф, long int колич_байт, int начало _отсчета) Здесь уф — это указатель файла, возвращаемый в результате вызова функции fopen (), колич_байт — количество байтов, считая от начало_отсчета, оно определяет новое значе- ние указателя текущей позиции, а начало отсчета — это один из следующих макросов: Начало отсчета Макрос Начало файла SEEK_SET Текущая позиция SEEK_CUR Конец файла SEEK_END Поэтому, чтобы получить в файле доступ на расстоянии колич_байт байтов от на- чала файла, начало_отсчета должно равняться SEEK_SET. Чтобы при доступе расстоя- ние отсчитывалось от текущей позиции, используйте макрос seek_cur, а чтобы при доступе расстояние отсчитывалось от конца файла, нужно указывать макрос SEEK_END. При успешном завершении своей работы функция fseek() возвращает нуль, а в случае ошибки — ненулевое значение. В следующей программе показано, как используется f seek (). Данная программа в определенном файле отыскивает некоторый байт, а затем отображает его. В команд- ной строке нужно указать имя файла, а затем нужный байт, то есть его расстояние в байтах от начала файла. #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { FILE *fp; if(argc!=3) { printf (’’Синтаксис: SEEK <имя_файла> <байт>п”); exit (1); } 234 Часть I. Основы языка С
if((fp = fopen(argv[1], "rb"))==NULL) { printf("Ошибка при открытии файла.n"); exit(1); } if(fseek(fpr atol(argv[2]), SEEK_SET)) { printf("Сбой при поиске.n"); exit(1); } printf("В %ld-M байте содержится %c.", atol(argv[2])r getc(fp)); fclose(fp); return 0; } Функцию fseek () можно использовать для доступа внутри многих значений од- ного типа, просто умножая размер данных на номер элемента, который вам нужен. Например, предположим, имеется список рассылки, который состоит из структур ти- па addr (определенных ранее). Чтобы получить доступ к десятому адресу в файле, в котором хранятся адреса, используйте следующий оператор: | fseek (fp, 9*sizeof(struct addr)r SEEK_SET); Текущее значение указателя текущей позиции в файле можно определить с помо- щью функции ftell (). Вот ее прототип: long int ftell(FILE *уф) Функция возвращает текущее значение указателя текущей позиции в файле, свя- занном с указателем файла уф. При неудачном исходе она возвращает -1. Обычно прямой доступ может потребоваться лишь для двоичных файлов. Причина тут простая — так как в текстовых файлах могут выполняться преобразования симво- лов, то может и не быть прямого соответствия между тем, что находится в файле и тем байтом, к которому нужен доступ. Единственный случай, когда надо использовать fseek () для текстового файла — это доступ к той позиции, которая была уже найде- на с помощью ftell (); такой доступ выполняется с помощью макроса SEEK_SET, используемого в качестве начала отсчета. Хорошо помните следующее: даже если в файле находится один только текст, все равно этот файл при необходимости можно открыть и в двоичном режиме. Никакие ограниче- ния, связанные с тем, что файлы содержат текст, к операциям прямого доступа не отно- сятся. Эти ограничения относятся только к файлам, открытым в текстовом режиме. 1 Функции fprintf() и fscanf () Кроме основных функций ввода/вывода, о которых шла речь, в системе ввода/вывода языка С также имеются функции fprintf () и fscanf (). Эти две функции, за исключе- нием того, что предназначены для работы с файлами, ведут себя точно так же, как и printf () и scanf (). Прототипы функций fprintf () и fscanf () следующие: int fprintf(FILE *уф, const char *управляющая_строка,...) int fscanf(FILE *уф, const char *управляющая_строка,...) где уф — указатель файла, возвращаемый в результате вызова f open (). Операции ввода/вывода функции fprintf () и fscanf () выполняют с тем файлом, на который указывает уф. Глава 9. Файловый ввод/вывод 235
В качестве примера предлагается рассмотреть следующую программу, которая чи- тает с клавиатуры строку и целое значение, а затем записывает их в файл на диске; имя этого файла — TEST. После этого программа читает этот файл и выводит инфор- мацию на экран. После запуска программы проверьте, каким получится файл TEST. Как вы и увидите, в нем будет вполне удобочитаемый текст. /* пример использования fscanf() и fprintfO */ #include <stdio.h> #include <io.h> #include <stdlib.h> int main(void) { FILE *fp; char s[80]; int t; if((fp=fopen("test", ”w"))==NULL) { printf("Ошибка при открытии файла.n"); exit (1); } printf("Введите строку и число: "); fscanf(stdin, "%s%d", s, &t); /* читать с клавиатуры */ fprintf(fp, "%s %d", s, t); /* писать в файл */ fclose(fp); if((fp=fopen("test", "r"))==NULL) { printf("Ошибка при открытии файла.n"); exit (1); } fscanf(fp, "%s%d", s, &t); /* чтение из файла */ fprintf(stdout, "%s %d", s, t); /* вывод на экран */ return 0; } Маленькое предупреждение. Хотя читать разносортные данные из файлов на дис- ках и писать их в файлы, расположенные также на дисках, часто легче всего именно с помощью функций fprintfO и fscanf (), но это не всегда самый эффективный способ выполнения операций чтения и записи. Так как данные в формате ASCII за- писываются так, как они должны появиться на экране (а не в двоичном виде), то ка- ждый вызов этих функций сопряжен с определенными накладными расходами. По- этому, если надо заботиться о размере файла или скорости, то, скорее всего, придется использовать f read () и fwrite (). В Стандартные потоки Что касается файловой системы языка С, то в начале выполнения программы ав- томатически открываются три потока. Это stdin (стандартный поток ввода), stdout (стандартный поток вывода) и stderr (стандартный поток ошибок). Обычно эти по- токи направляются к консоли, но в средах, которые поддерживают перенаправление ввода/вывода, они могут быть перенаправлены операционной системой на другое уст- 236 Часть I. Основы языка С
ройство. (Перенаправление ввода/вывода поддерживается, например, такими опера- ционными системами, как Windows, DOS, UNIX и OS/2.) Так как стандартные потоки являются указателями файлов, то они могут исполь- зоваться системой ввода/вывода языка С также для выполнения операций вво- да/вывода на консоль. Например, putchar () может быть определена таким образом: Iint putchar(char с) { return putc(с, stdout); } Вообще говоря, stdin используется для считывания с консоли, a stdout и stderr — для записи на консоль. В роли указателей файлов потоки stdin, stdout и stderr можно применять в любой функции, где используется переменная типа file *. Например, для ввода строки с консоли можно написать примерно такой вызов fgets (): I char str[255]; fgets(str, 80, stdin); И действительно, такое применение fgets() может оказаться достаточно полез- ным. Как уже говорилось в этой книге, при использовании gets () не исключена возможность, что массив, который используется для приема вводимых пользователем символов, будет переполнен. Это возможно потому, что gets () не проводит проверку на отсутствие нарушения границ. Полезной альтернативой gets () является функция fgets () с аргументом stdin, так как эта функция может ограничивать число читае- мых символов и таким образом не допустить переполнения массива. Единственная проблема, связанная с fgets (), состоит в том, что она не удаляет символ новой стро- ки (в то время как gets () удаляет!), поэтому его приходится удалять “вручную”, как показано в следующей программе: #include <stdio.h> #include <string.h> int main(void) { char str[80]; int i; printf("Введите строку: ”); fgets(str, 10, stdin); /★ удалить символ новой строки, если он есть */ i = strlen(str)-1; if(str[i]==’n') str[i] = ’’; printf (’’Это ваша строка: %s”, str); return 0; He забывайте, что stdin, stdout и stderr — это не переменные в обычном смысле, и им нельзя присваивать значение с помощью fopen (). Кроме того, именно потому, что в начале работы программы эти указатели файлов создаются автоматиче- ски, в конце работы они и закрываются автоматически. Так что и не пытайтесь само- стоятельно их закрыть. Глава 9. Файловый ввод/вывод 237
Связь с консольным вводом/выводом В языке С консольный и файловый ввод/вывод не слишком отличаются друг от друга. Функции консольного ввода/вывода, описанные в главе 8, на самом деле на- правляют результаты своих операций на один из потоков — stdin или stdout, и по сути, каждая из них является специальной версией соответствующей файловой функ- ции. Функции консольного ввода/вывода для того и существуют, чтобы было удобно именно программисту. Как говорилось в предыдущем разделе, ввод/вывод на консоль можно выполнять с помощью любой файловой функции языка С. Однако для вас может быть сюрпризом, что, оказывается, операции ввода/вывода на дисковых файлах можно выполнять с помощью функции консольного ввода/вывода, например, printf ()! Дело в том, что все функции консольного ввода/вывода, о которых говорилось в главе 8, выполняют свои операции с потоками stdin и stdout. В средах, поддерживающих перенаправ- ление ввода/вывода, это равносильно тому, что stdin или stdout могут быть пере- направлены на устройство, отличное от клавиатуры или экрана. Проанализируйте, например, следующую программу: #include <stdio.h> int main(void) { char str[80]; printf("Введите строку: "); gets(str); printf(str) ; return 0; } Предположим, что эта программа называется TEST. При ее нормальном выполне- нии на экран выводится подсказка, затем читается строка, введенная с клавиатуры, и, наконец, эта строка выводится на экран. Однако в средах, в которых поддерживается перенаправление ввода/вывода, один из потоков stdin или stdout (или оба одно- временно) можно перенаправить в файл. Например, в среде DOS или Windows сле- дующий запуск TEST | TEST > OUTPUT приводит к тому, что вывод этой программы будет записан в файл по имени OUTPUT. А следующий запуск TEST | TEST < INPUT > OUTPUT направляет поток stdin в файл по имени INPUT, а поток стандартного вывода — в файл по имени OUTPUT. Когда С-программа завершается, то все перенаправленные потоки возвращаются в состояния, которые были установлены по умолчанию. Перенаправление стандартных потоков: функция freopen() Дня перенаправления стандартных потоков можно воспользоваться функцией freopen(). Эта функция связывает имеющийся поток с новым файлом. Так что она вполне может связать с новым файлом и стандартный поток. Вот прототип этой функции: FILE *freopen(const char *имя_файла, const char * режим, FILE * поток)', 238 Часть I. Основы языка С
где имя_файла — это указатель на имя файла, который требуется связать с потоком, на который указывает указатель поток. Файл открывается в режиме режим', этот па- раметр может принимать те же значения, что и соответствующий параметр функции fopen (). Если функция f reopen () выполнилась успешно, то она возвращает поток, а если встретились ошибки, — то NULL. В следующей программе показано использование функции freopenO для перена- правления стандартного потока вывода stdout в файл с именем OUTPUT. #include <stdio.h> int main(void) { char str[80]; freopen("OUTPUT", "w" , stdout); printf ("Введите строку: ’’); gets (str); printf(str); return 0; } Вообще говоря, перенаправление стандартных потоков с помощью freopenO в некоторых случаях может быть полезно, например, при отладке. Однако выполнение дисковых операций ввода/вывода на перенаправленных потоках stdin и stdout не настолько эффективно, как использование таких функций, как fread() или fwrite (). Глава 9. Файловый ввод/вывод 239
Полный справочник по Глава 10 Препроцессор и комментарии
В исходный код программы на языке С можно вставлять различные инструкции компилятору. Они называются директивами препроцессора и расширяют воз- можности среды программирования. Кроме них, в этой главе еще рассказывается о комментариях. В Препроцессор Имеются следующие директивы препроцессора: #define #endif #ifdef #line #elif #error #ifndef #pragma #else #if #include #undef Как легко заметить, все они начинаются со знака #. Кроме того, каждая директива препроцессора должна занимать отдельную строку. Например, строка | ttinclude <stdio.h> #include <stdlib.h> рассматривается как недопустимая. И Директива #define Директива #define определяет идентификатор и последовательность символов, которая будет подставляться вместо идентификатора каждый раз, когда он встретится в исходном файле. Идентификатор называется именем макроса, а сам процесс заме- ны — макрозаменой1. В общем виде директива выглядит таким образом: #define имя_макроса последователъностъ_символов Обратите внимание, что в этом выражении нет точки с запятой. Между идентифи- катором и последовательностью символов последователъность_символов может быть любое количество пробелов, но признаком конца последовательности символов может быть только разделитель строк. Предположим, например, что вместо значения 1 нужно использовать слово LEFT (левый), а вместо значения 0 — слово RIGHT (правый). Тогда можно сделать следую- щие объявления с помощью директивы #define: | tfdefine LEFT 1 | ttdefine RIGHT 0 В результате компилятор будет подставлять 1 или 0 каждый раз, когда в вашем файле исходного кода встречается идентификатор соответственно LEFT или RIGHT. Например, следующий код выводит на экран 0 1 2: | printf(”%d %d %d", RIGHT, LEFT, LEFT+1); После определения имя макроса можно использовать в определениях других имен мак- росов. Вот, например, код, определяющий значения ONE (один), TWO (два) и THREE (три): i#define ONE 1 tfdefine TWO ONE+ONE tfdefine THREE ONE+TWO 1 А также макрорасширением, макрогенерацией и макроподстановкой. Определение макроса часто называют макроопределением, а обращение к макросу — макровызовом или макрокомандой. Впрочем, иногда макроопределение также называется макрокомандой. — Прим. ред. 242 Часть I. Основы языка С
Макроподстановка — это просто замена какого-либо идентификатора связанной с ним последовательностью символов. Поэтому если требуется определить стандартное сообщение об ошибке, то можно написать примерно следующее: |#define Е_MS "стандартная ошибка при вводеп" /* ... */ printf (E_MS) ; Теперь каждый раз, когда встретится идентификатор e_ms, компилятор будет его заменять строкой “стандартная ошибка при вводеп”. Для компилятора выражение printf () на самом деле будет выглядеть таким образом: | printf("стандартная ошибка при вводеп"); Если идентификатор находится внутри строки, заключенной в кавычки, то замены не будет. Например, при выполнении кода |#define XYZ это проверка printf("XYZ"); вместо сообщения это проверка будет выводиться последовательность символов XYZ. Если последовательность_символов не помещается в одной строке, то эту последо- вательность можно продолжить на следующей строке, поместив в конце предыдущей, как показано ниже, обратную косую черту: Itfdefine LONG_STRING "это очень длинная строка, используемая в качестве примера" Программисты, пишущие программы на языке С, в именах определяемых иденти- фикаторов часто используют буквы верхнего регистра. Если разработчики программ следуют этому правилу, то тот, кто будет читать их программу, с первого взгляда пой- мет, что будет происходить макрозамена. Кроме того, все директивы # de fine обычно лучше всего помещать в самом начале файла или в отдельном заголовочном файле, а не разбрасывать по всей программе. Имена макросов часто используются для определения имен так называемых “магических чисел” (встречающихся в программе). Например, имеется программа, в которой определяется массив и несколько процедур, получающих доступ к этому мас- сиву. Вместо того чтобы размер массива “зашивать в код” в виде константы, этот раз- мер можно определить с помощью оператора # de fine, а затем использовать это имя макроса везде, где требуется размер массива. Таким образом, если требуется изменить этот размер, то потребуется изменить только соответствующий оператор tfdefine, а затем перекомпилировать программу. Рассмотрим, например, фрагмент программы #define MAX_SIZE 100 /* ... */ float balance[MAX_SIZE]; /* ... */ for(i=0; i< MAX_SIZE; i++) printf("%f", balance[i]); /* ... */ for(i=0; i< MAX_SIZE; i++) x =+ balance[i]; Размер массива balance определяется именем макроса MAX_SIZE, и поэтому если этот размер потребуется в будущем изменить, то надо будет изменить только определение MAX_SIZE. В результате при перекомпиляции программы все обращения к этому имени макроса, находящиеся после измененного определения, будут автоматически изменены. Определение макросов с формальными параметрами У директивы # de fine имеется еще одно большое достоинство: имя макроса может определяться с формальными параметрами. Тогда каждый раз, когда в программе встречается имя макроса, то используемые в его определении формальные параметры Глава 10. Препроцессор и комментарии 243
заменяются теми аргументами, которые встретились в программе. Такого рода макро- сы называются макросами с формальными параметрами1. Например, ttinclude <stdio.h> tfdefine ABS(a) (a) < 0 ? -(a) : (a) int main(void) { printf("модули чисел -1 и 1 равны соответственно %d и %d", ABS(-l), ABS (1)) ; return 0; } Во время компиляции этой программы вместо формального параметра а из опре- деления макроса будут подставляться значения -1 и 1. Скобки, в которых находится а, позволяют в любом случае сделать правильную замену. Например, если скобки, стоя- щие вокруг а, удалить, то выражение | ABS(10-20) после макрозамены будет преобразовано в | 10-20 < 0 ? -10-20 : 10-20 и может привести к неправильному результату. Использование вместо настоящих функций макросов с формальными параметрами дает одно существенное преимущество: увеличивается скорость выполнения кода, пото- му что в таких случаях не надо тратить ресурсы на вызов функций. Однако если у мак- роса с формальными параметрами очень большие размеры, то тогда из-за дублирования кода увеличение скорости достигается за счет увеличения размеров программы. И вот еще что: хотя макросы с формальными параметрами являются полезным средством, но в С99 (и в C++) есть еще более эффективный способ создания машин- ной программы — с использованием ключевого слово inline. В С99 можно определить макрос с переменным количеством формальных па- раметров; об этом рассказывается в части II этой книги. На заметку Йя Директива #еггог Директива tferror заставляет компилятор прекратить компиляцию. Эта директива используется в основном для отладки. В общем виде директива tferror выглядит та- ким образом: #еггог сообщение-об-ошибке сообщение-об-ошибке в двойные кавычки не заключается. Когда встречается директива tferror, то выводится сообщение об ошибке — возможно, вместе с другой информа- цией, определяемой компилятором. 1 А также макроопределениями с параметрами и макросами, напоминающими функции. — Прим. ред. 244 Часть I. Основы языка С
В Директива #include Директива #include дает указание компилятору читать еще один исходный файл — в дополнение к тому файлу, в котором находится сама эта директива. Имя исходного файла должно быть заключено в двойные кавычки или в угловые скобки. Например, обе директивы Itfinclude "stdio.h" #include <stdio.h> дают компилятору указание читать и компилировать заголовок для библиотечных функций системы ввода/вывода. Файлы, имена которых находятся в директивах #include, могут в свою очередь содержать другие директивы #include. Они называются вложенными директивами ^include. Количество допустимых уровней вложенности у разных компиляторов может быть разным. Однако в стандарте С89 предусмотрено, что компиляторы должны до- пускать не менее 8 таких уровней. А в стандарте С99 предусмотрена поддержка не ме- нее 15 уровней вложенности. Способ поиска файла зависит от того, заключено ли его имя в двойные кавычки или же в угловые скобки. Если имя заключено в угловые скобки, то поиск файла проводится тем способом, который определен в компиляторе. Часто это означает поиск определенного каталога, специально предназначенного для хранения таких файлов. Если имя заключено в кавычки, то поиск файла проводится другим способом. Во многих компиляторах это озна- чает поиск файла в текущем рабочем каталоге. Если же файл не найден, то поиск повторя- ется уже так, как будто имя файла заключено в угловые скобки. Обычно большинство программистов имена стандартных заголовочных файлов заклю- чают в угловые скобки. А использование кавычек обычно приберегается для имен специ- альных файлов, относящихся к конкретной программе. Впрочем, твердого и простого пра- вила, по которому кавычки требуется использовать именно таким образом, не существует. В С-программе директиву #include можно использовать не только для указания имени файла, содержащего обычный исходный текст программы, но и для указания заголовка. В языке С определен набор стандартных заголовков, содержащих необхо- димую информацию о различных библиотеках этого языка. Заголовок — это стан- дартный идентификатор, который может соответствовать имени файла, а может и не соответствовать ему. Таким образом, заголовок является просто абстракцией, которая гарантирует наличие некоторой информации. Однако на практике в языке С заголов- ки почти всегда являются именами файлов. Н Директивы условной компиляции Имеется несколько директив, которые дают возможность выборочно компилиро- вать части исходного кода вашей программы. Этот процесс называется условной ком- пиляцией и широко используется фирмами, живущими за счет коммерческого про- граммного обеспечения — теми, которые поставляют и поддерживают многие специ- альные версии одной программы. Директивы #if, #else, #elif и #endif Возможно, самыми распространенными директивами условной компиляции явля- ются #if, #else, #elif и ttendif. Они дают возможность в зависимости от значения константного выражения включать или исключать те или иные части кода. В общем виде директива #if выглядит таким образом: Глава 10. Препроцессор и комментарии 245
#if константное_выражение последовательность операторов #endif Если находящееся за #if константное выражение истинно, то компилируется код, ко- торый находится между этим выражением и #endif. В противном случае этот промежу- точный код пропускается. Директива #endif обозначает конец блока #if. Например, /* Простой пример #if. */ #include <stdio.h> #define MAX 100 int main(void) { #if MAX>99 printf("Компилируется для массива, размер которого больше 99.п"); #endif return 0; } Это программа выводит сообщение на экран, потому что МАХ больше 99. В этом при- мере показано нечто очень важное. Значение выражения, находящегося за директивой #if, должно быть вычислено во время компиляции. Поэтому в этом выражении могут на- ходиться только ранее определенные идентификаторы и константы, — но не переменные. Директива #else работает в основном так, как else — ключевое слово языка С: задает альтернативу на тот случай, если не выполнено условие #if. Предыдущий пример можно дополнить следующим образом: /* Простой пример #if/#else. */ #include <stdio.h> #define MAX 10 int main(void) { #if MAX>99 printf("Компилируется для массива, размер которого больше 99.п"); #else printf("Компилируется для небольшого массива.п"); #endif return 0; } В этом случае выясняется, что мах меньше 99, поэтому часть кода, относящаяся к #if, не компилируется. Однако компилируется альтернативный код, относящийся к #else, и откомпилированная программа будет отображать сообщение Компилируется для небольшого массива. Обратите внимание, что директива #else используется для того, чтобы обозначить и конец блока #if, и начало блока #else. Это естественно, поскольку любой дирек- тиве #if может соответствовать только одна директива #endif. Директива #elif означает “else if’ и устанавливает для множества вариантов компиля- ции цепочку if-else-if. После #elif находится константное выражение. Если это выраже- ние истинно, то компилируется находящийся за ним блок кода, и больше не проверяются никакие другие выражения #elif. В противном же случае проверяется следующий блок этой последовательности. В общем виде #elif выглядит таким образом: 246 Часть I. Основы языка С
#if выражение последовательность операторов # elif выражение 1 последовательность операторов # elif выражение 2 последовательность операторов # elif выражение 3 последовательность операторов # elif выражение 4 # elif выражение N последовательность операторов #endif Например, в следующем фрагменте для определения знака денежной единицы ис- пользуется значение active_country (для какой страны): tfdefine US О # define ENGLAND 1 tfdefine FRANCE 2 tfdefine ACTIVE_COUNTRY US # if ACTIVE_COUNTRY =*= US char currency[] = "dollar"; # elif ACTIVE_COUNTRY == ENGLAND char currency[] = "pound"; #else char currency[] = "franc"; #endif В соответствии co стандартом C89 у директив #if и #elif может быть не менее 8 уровней вложенности. А в соответствии со стандартом С99 программистам разрешает- ся использовать не менее 63 уровней вложенности. При вложенности каждая дирек- тива #endif, #else или #elif относится к ближайшей директиве #if или #elif. Например, совершенно правильным является следующий фрагмент кода: # if МАХ>100 #if SERIAL—VERSION /* последовательная связь */ int port=198; #elif int port=200; #endif #else char out—buffer[100]; #endif Директивы # if def и #ifndef Другой способ условной компиляции— это использование директив #ifdef и lifndef, которые соответственно означают “if defined” (если определено) и “if not defined” (если не определено). В общем виде #ifdef выглядит таким образом: #ifdef имя_макроса последовательность операторов tfendif Глава 10. Препроцессор и комментарии 247
Блок кода будет компилироваться, если имя макроса было определено ранее в опе- раторе #define. В общем виде оператор tfifndef выглядит таким образом: #ifndef имя_макроса последовательность операторов #endif Блок кода будет компилироваться, если имя макроса еще не определено в операто- ре tfdefine. И в #ifdef, и в tfifndef можно использовать оператор #else или #elif. Например, tfinclude <stdio.h> #define TED 10 int main(void) { #ifdef TED printf("Привет, Тед.п"); #else printf("Привет, кто-нибудь.n"); tfendif #ifndef RALPH printf("A RALPH не определен, так что Ральфу не повезлоп") ; tfendif return 0; } выведет Привет, Тед, а также A RALPH не определен, так что Ральфу не повезло. В соответствии со стандартом С89 допускается не менее 8 уровней # if def и #ifndef. А стандарт С99 устанавливает, что должно поддерживаться не менее 63 уровней вложенности. Щ Директива #undef Директива tfundef удаляет ранее заданное определение имени макроса, то есть “аннулирует” его определение; само имя макроса должно находиться после директи- вы. В общем виде директива tfundef выглядит таким образом: #undef имя_макроса Вот как, например, можно использовать эту директиву: #define LEN 100 tfdefine WIDTH 100 char array[LEN][WIDTH]; tfundef LEN tfundef WIDTH /* а здесь и LEN и WIDTH уже не определены */ И LEN, и WIDTH определены, пока не встретился оператор tfundef. Директива tfundef используется в основном для того, чтобы локализовать имена макросов в тех участках кода, где они нужны. 248 Часть I. Основы языка С
В Использование defined Кроме применения #ifdef, есть еще второй способ узнать, определено ли имя макроса. Можно использовать директиву #if в сочетании с оператором времени ком- пиляции defined. В общем виде оператор defined выглядит таким образом: defined имя_макроса Если имя_макроса определено, то выражение считается истинным; в противном случае — ложным. Например, чтобы узнать, определено ли имя макроса MYFILE, можно использовать одну из двух команд препроцессора: | #if defined MYFILE ИЛИ I #ifdef MYFILE Можно также задать противоположное условие, поставив ! прямо перед defined. Например, следующий фрагмент компилируется только тогда, когда имя макроса debug не определено: |#if ’defined DEBUG printf("Окончательная версия!n"); ttendif Единственная причина, по которой используется оператор defined. состоит в том, что с его помощью в #elif можно узнать, определено ли имя макроса. В Директива #line Директива #line изменяет содержимое__LINE___и_____FILE___, которые явля- ются зарезервированными идентификаторами в компиляторе. В первом из них содер- жится номер компилируемой в данный момент строки кода. А второй идентифика- тор — это строка, содержащая имя компилируемого исходного файла. В общем виде директива #line выглядит таким образом: #line номер "имя_файла" где номер — это положительное целое число, которое становится новым значением __LINE___, а необязательное имя_файла— это любой допустимый идентификатор файла, становящийся новым значением___FILE___. Директива #line в основном используется для отладки и специальных применений. Например, следующий код определяет, что счетчик строк будет начинаться с 100, а оператор printf () выводит номер 102, потому что он расположен в третьей строке программы после оператора #line 100: tfinclude <stdio.h> #line 100 /* установить счетчик строк */ int main(void) /* строка 100 */ { /*строка 101 */ printf("%dn",____LINE___); /* строка 102 */ return 0; ) Глава 10. Препроцессор и комментарии 249
И Директива # pragma Директива tfpragma — это определяемая реализацией директива, которая позволя- ет передавать компилятору различные инструкции. Например, компилятор может поддерживать трассировку выполнения программы. Тогда возможность трассировки можно указывать в операторе #pragma. Возможности этой директивы и относящиеся к ней подробности должны быть описаны в документации по компилятору. В стандарте С99 директиве ^pragma есть альтернатива— оператор _Pragma. О нем рассказывается в части II этой книги. На заметку Н Операторы препроцессора # и ## Имеется два оператора препроцессора: # и ##. Они применяются в сочетании с оператором #define. Оператор #, который обычно называют оператором превращения в строку (stringize), превращает аргумент, перед которым стоит, в строку, заключенную в кавычки. Рас- смотрим, например, следующую программу: #include <stdio.h> #define mkstr(s) # s int main(void) { printf(mkstr(Мне нравится С.)); return 0; } Препроцессор превращает строку | printf(mkstr(Мне нравится С.)); в | printf (’’Мне нравится С.”); Оператор ##, который называют оператором склеивания (pasting), или конкатенации конкатенирует две лексемы. Рассмотрим, например, программу #include <stdio.h> #define concat(a, b) a ## b int main(void) { int xy = 10; printf(”%d”, concat(x, y)); return 0; } Препроцессор преобразует | printf(”%d”, concat(x, y)); в | printf("%d”, xy); 250 Часть I. Основы языка С
Если эти операторы покажутся вам незнакомыми, то надо помнить вот о чем: они не являются необходимыми и не используются в большинстве программ. В общем-то, эти операторы предусмотрены для работы препроцессора в некоторых особых случаях. В Имена предопределенных макрокоманд В языке С определены пять встроенных, предопределенных имен макрокоманд. Вот они: __LINE____ __FILE____ __DATE____ __TIME____ __STDC____ В такой же последовательности о них здесь и пойдет речь. Об именах макросов_LINE___и_____FILE__рассказывалось, когда говорилось о директиве #line. Говоря кратко, они содержат соответственно номер строки и имя файла компилируемой программы. В имени макроса___date__содержится строка в виде месяц/день/год, то есть дата перевода исходного кода в объектный. В имени макроса___time__содержится время компиляции программы. Это вре- мя представлено строкой, имеющей вид час:минута:секунда. Если ST DC определено как 1, то тогда компилятор выполняет компиляцию в соответствии со стандартом С. А что касается С99, то в этом стандарт определены еще два имени макросов: __STDC.HOSTED_____ __STDC.VERSION____ ___STDC_HOSTED__равняется 1 для тех сред, в которых выполнение происходит под управлением операционной системы, и 0 — в противном случае._stdc_version_бу- дет равно как минимум 199901 и будет увеличиваться с каждой новой версией языка С. (В С99 могут быть определены и другие имена макросов, о них рассказывается в части II.) В Комментарии В стандарте С89 определены комментарии только одного вида; такой комментарий начинается с символов /* и заканчивается символами */. Между звездочкой и сле- шем не должно быть никаких пробелов. Любой текст, расположенный между началь- ными и конечными символами комментария, компилятором игнорируется. Напри- мер, следующая программа только выведет на экран Привет: #include <stdio.h> int main(void) { printf("Привет”); /* printf("всем"); */ return 0; } Глава 10. Препроцессор и комментарии 251
Комментарий такого вида называется многострочным комментарием (multiline com- ment), потому что его текст может располагаться в нескольких строках. Например, I/* это многострочный комментарий */ Комментарии могут находиться в любом месте программы, за исключением середины ключевого слова или идентификатора. Приведенный ниже комментарий правильный: | х = 10+/* прибавлять числа */5; а комментарий | swi/*TaKoe работать не будет*/+сЬ (с) { ... не является допустимым, потому что комментарий не может разрывать ключевое сло- во. Впрочем, комментарии обычно не следует размещать и в середине выражений, по- тому что так труднее разобраться и с выражениями, и с самими комментариями. Многострочные комментарии не могут быть вложенными. То есть в одном ком- ментарии не может находиться другой. Например, при компиляции следующего фрагмента кода будет обнаружена ошибка: I/* это внешний комментарий х = у/а; /* а это внутренний комментарий, обнаружив который, компилятор выдаст сообщение об ошибке */ */ Однострочные комментарии В С99 (да и в C++) поддерживается два вида комментариев. Первым из них явля- ется /* */, или многострочный комментарий, о котором только что говорилось. А вторым — однострочный комментарий. Такой комментарий начинается с символов // и заканчивается в конце строки. Например: | // это однострочный комментарий Однострочные комментарии особенно полезны тогда, когда нужны краткие, не бо- лее чем в одну строку пояснения. Хотя версия С89 такие комментарии официально не поддерживает, зато их признает большинство компиляторов С. Однострочный комментарий может находиться внутри многострочного коммента- рия. Например, следующий комментарий является вполне допустимым: | /* это // проверка вложенных комментариев */ Комментарии должны находиться там, где требуется объяснить работу кода. На- пример, в начале всех функций, за исключением самых очевидных, должен быть комментарий, который сообщает, что именно делает функция, как она вызывается и что возвращает. 252 Часть I. Основы языка С
Полный справочник по Часть II Стандарт С99 Как известно, языки программирования непрерывно развиваются, реагируя на изменения в методологии, приложениях, общепринятой практике и используемом оборудовании. Не является исключением в этом отно- шении и язык С. Его эволюция пошла двумя путями. Первый — это продолжение разработки самого языка С. Второй путь — это язык C++, для которого С по- служил отправной точкой. И хотя последние несколь- ко лет внимание специалистов было приковано к C++,
но никогда не ослабевал их интерес к развитию языка С. Например, в ответ на интер- национализацию вычислительной среды, в 1995 году в первоначальный Стандарт С89 были введены различные двух- и многобайтовые функции. После завершения согла- сования поправок в 1995 году началось общее обновление языка. Конечным результа- том этого обновления, конечно же, является С99. При создании стандарта 1999 года были тщательно перепроверены все элементы языка С, проанализированы типичные способы его использования и сделаны попытки предугадать будущие потребности. Как и ожидалось, фоном для всего творческого процесса послужили “взаимоотношения” С и C++. Получившийся в результате Стан- дарт С99 является доказательством мощи своего первоисточника. Было изменено очень малое число ключевых элементов С. Говоря кратко, изменения заключаются в том, что было тщательно отобрано небольшое количество дополнений к языку, а также было добавлено несколько новых библиотечных функций. Так что язык С все еще остается языком С! В части I этой книги рассказывалось о тех возможностях С, которые были опреде- лены в Стандарте С89. В этой части мы обсудим возможности, которые появились в С99, а также немногочисленные отличия между С99 и С89.
Полный справочник по Глава 11 С99
Возможно, самая большая причина для беспокойства, связанного с появлением но- вого языкового стандарта, — это вопрос о совместимости со своим предшествен- ником. Устареют ли уже написанные программы после выхода новой спецификации? Были ли изменены важные конструкции? Надо ли менять методологию или техноло- гию программирования? Ответы на эти вопросы часто определяют, в какой степени будет принят новый стандарт и, в дальней перспективе, жизнеспособность самого языка. К счастью, создание С99 было управляемым, беспристрастным процессом — благодаря опытным “диспетчерам” этого процесса. Попросту говоря, если вам нра- вился С таким, каким он был, то понравится и версия С, определяемая Стандартом С99. То, что многие программисты думали о языке С как о самом элегантном в мире языке программирования, не устарело и сейчас! В этой главе мы изучим изменения в С и дополнения к С, сделанные Стандартом 1999 года. Многие из этих изменений были вскользь упомянуты еще в части I. Здесь же они будут рассмотрены более подробно. Однако не забывайте, что во время напи- сания этой книги компиляторы, которые поддерживали бы многие новые возможно- сти С99, еще не были широко распространены. Возможно, вам придется немного по- дождать перед тем, как провести “испытательные полеты” с такими новыми восхити- тельными конструкциями, которыми являются массивы переменной длины, restricted- квалифицированные указатели и тип данных long long. В Сравнение С99 с С89. Общее впечатление Отличия между С99 и С89 можно разбить на три общие категории: новые возможности, добавленные к С89 возможности, удаленные из С89 возможности, которые были изменены или расширены Многие из отличий между С89 и С99 достаточно незначительны и относятся лишь к нюансам языка. А в этой книге основное внимание уделяется достаточно заметным изменениям — заметным настолько, чтобы влиять на способ написания программ. Новые возможности Скорее всего, самыми заметными из новых средств, которых не было в С89, явля- ются те, которые связаны с использованием новых ключевых слов: inline restrict _Bool -Complex -Imaginary К другим важным новинкам относятся: массивы переменной длины поддержка арифметических операций с комплексными числами тип данных long long int комментарий / / возможность распределять код и данные добавления к препроцессору объявления переменных внутри оператора for 256 Часть II. Стандарт С99
составные литералы массивы с переменными границами в качестве членов структур назначенные инициализаторы изменения в семействе функций printf () и scanf () зарезервированный идентификатор_func___ новые библиотеки и заголовки Большинство возможностей были созданы комитетом по стандартизации, причем многие из этих возможностей созданы с учетом расширений языка, имеющихся в разных реализациях языка С. Впрочем, в некоторых случаях возможности были поза- имствованы у C++, как, например, ключевое слово inline и комментарии вида //. Важно понять, что при создании С99 не были добавлены классы в стиле C++, насле- дование и функции-члены. В том, что С должен остаться С — в этом комитет по стандартизации был единодушен. Удаленные средства Самым заметным “излишеством”, удаленным при создании С99, было правило “неявного int”. В С89 во многих случаях, когда не было явного указания типа дан- ных, подразумевался тип int. А в С99 такое не допускается. Также удалено неявное объявление функций. В С89, если функция перед использованием не объявлялась, то подразумевалось неявное объявление. А в С99 такое не поддерживается. Если про- грамма должна быть совместима с С99, то из-за двух этих изменений, возможно, при- дется немного подправить код. Измененные средства При создании С99 было сделано несколько изменений имевшихся средств. По большей части, эти изменения расширяют возможности или вносят определенную яс- ность в их значение. И только в небольшом количестве случаев изменения ограничи- вают или сужают применение средства. Многие изменения небольшие, однако неко- торые из них являются достаточно важными, в том числе: уменьшение ограничений транслятора новые целые типы расширение правил продвижения целых типов более строгие правила употребления оператора return Что касается влияния этих изменений на уже написанные программы, то самое значительный эффект имеет изменение правил употребления оператора return, из-за чего код, возможно, придется немного подправить. В оставшейся части этой главы мы изучим основные различия между С89 и С99. Ю Указатели, определенные с квалификаторами типа restrict Одной из самых важных новинок, введенных Стандартом С99, является квалифи- катор типа restrict (ограниченный). Этот квалификатор применяется только к ука- зателям. Указатель, определенный с квалификатором типа restrict1, изначально яв- 1 Указатель, определенный с квалификатором типа restrict, называется также restrict- квалифицированным указателем или указателем, квалифицированным как restrict. — Прим. ред. Глава 11. С99 257
ляется единственным средством, с помощью которого можно получить доступ к ука- зываемому объекту. Доступ к объекту с помощью другого указателя возможен лишь тогда, когда этот второй указатель основан на первом. Таким образом, доступ к объек- ту возможен только для выражений, составленных на основе указателя с квалифика- тором типа restrict. Такие указатели в основном используются как параметры функций или для указания памяти, распределенной с помощью malloc (). Квалифи- катор типа restrict семантики программы не меняет. Если указатель квалифицирован с помощью квалификатора типа restrict, то компилятор может лучше оптимизировать некоторые программы, зная, что указатель с квалификатором типа restrict является единственным средством доступа к объек- ту. Например, если функция имеет два параметра в виде указателей с квалификатором типа restrict, то компилятор может допустить, что указатели указывают на разные (причем неперекрывающиеся!) объекты. Проанализируем, например, то, что стало классическим примером применения restrict — определение функции memcpyO. В С89 у нее имеется следующий прототип: void *memcpy(void *cmpl, const void *стр2, size_t размер)', В описании memcpyO сказано, что если объекты, на которые указывают cmpl и стр2, перекрываются, то поведение этой функции непредсказуемое. Таким образом, memcpy () гарантированно будет работать только с неперекрывающимися объектами. В С99 можно использовать restrict, чтобы в прототипе memcpyO явно указать то, что в С89 приходится дополнительно объяснять словами. Вот прототип memcpy () в С99: void *memcpy(void * restrict cmpl, const void * restrict cmp2, size__t размер)', Квалифицируя cmpl и cmp2 с помощью квалификатора типа restrict, в прототи- пе явно утверждается, что они указывают на неперекрывающиеся объекты. Из-за преимуществ, которые может принести использование квалификатора типа restrict, в С99 он был добавлен в прототипы многих библиотечных функций, опре- деленных еще в С89. IS Ключевое слово inline При разработке С99 было добавлено ключевое слово inline, которое применяется к функциям. Ставя inline в начале объявления функции, вы предлагаете компилято- ру оптимизировать вызовы к этой функции. Обычно это означает, что при компиля- ции код этой функции будет вставляться на месте вызовов. Однако ключевое слово inline является всего лишь запросом к компилятору и может быть проигнорировано. В С99 особо отмечено, что использование inline “предполагает, что вызовы функ- ции должны быть максимально быстрыми”. Спецификатор inline также поддержи- вается в языке C++, и синтаксис С99 для этого ключевого слова совместим с C++. Чтобы создать встраиваемую функцию1, перед ее определением поставьте ключевое сло- во inline. Например, в следующей программе оптимизируются вызовы функции max (): #include <stdio.h> inline int max(int a, int b) { return a > b ? a : b; } 1 Называется также подставляемой функцией. — Прим. ред. 258 Часть II. Стандарт С99
int main(void) { int x=5, y=10; printf (’’Наибольшим из чисел %d и %d является %dn”, x, у, max(x, y) ) ; return 0; } При типичной реализации inline предшествующая программа эквивалентна сле- дующей: #include <stdio.h> int main(void) { int x=5, y=10; printf (’’Наибольшим из чисел %d и %d является %dn”, x, у, (x>y ? x : у)); return 0; } Причина, по которой встраиваемым функциям придается такое большое значение, состоит в том, что они помогают создавать более эффективный код, поддерживая при этом структурированный, функционально-ориентированный подход. Как вы знаете, каждый раз при вызове функции механизм ее вызова и возврата требует значитель- ного количества ресурсов. Обычно при вызове функции ее аргументы заталкиваются в стек, содержимое различных регистров заносится в память, а затем при возврате функции содержимое этих регистров восстанавливается. Беда в том, что на эти опера- ции требуется время. Однако, если код функции подставляется вместо вызова, такие операции уже не нужны. Впрочем, хотя такие подстановки функции и способствуют ускорению выполнения, но они приводят к увеличению размера кода из-за дублиро- вания кода функции. По этой причине лучше всего использовать inline только с очень небольшими функциями, т.е. подставлять код только маленьких функций. Кроме того, хорошо было бы применять это ключевое слово только к тем функциям, которые существенно влияют на производительность программы. Помните: хотя inline обычно приводит к подстановке кода функции на месте ее вызова, компилятор может проигнорировать этот запрос или использовать некоторые другие средства оптимизации вызовов функции. S Новые встроенные типы данных В стандарте С99 появились новые для С встроенные типы данных. Здесь подробно рассказывается о каждом из них. .Bool Один из новых типов данных, появившихся в С99, — это _Воо1, в котором можно хранить значения 1 и 0 (истина (true) и ложь (false)). _Воо1 представляет собой целый тип данных. Как известно многим читателям, в языке C++ определяется ключевое слово bool, которое, несмотря на сходство, все же отличается от _Воо1. Таким образом, в написании этого типа С99 и C++ несовместимы. Кроме того, в C++ определяются встроенные логи- Глава 11. С99 259
ческие константы true и false, а в С99 этого не делается. Однако в С99 имеется заголо- вок <stdbool.h>, в котором определены имена макросов bool, true и false. Таким об- разом, можно легко создавать код, совместимый с C/C++. Причина того, что в качестве ключевого слова указывается -Bool, а не bool, со- стоит в том, что во многих уже имеющихся С-программах определены их собственные варианты bool. Определяя логический тип как _Bool, С99 дает возможность не ме- нять уже написанный код. Однако в новые программы лучше всего вставлять <stdbool.h>, а затем использовать имя макроса bool. .Complex и .Imaginary Стандарт С99 появился вместе с новой для С поддержкой арифметических опера- ций с комплексными числами; эта поддержка включает в себя ключевые слова _Complex и -Imaginary, дополнительные заголовки и несколько новых библиотеч- ных функций. Однако никаких реализаций не требуется, чтобы реализовать типы мнимых чисел (imaginary types), а автономные приложения (которые обходятся без операционной системы) не обязаны поддерживать комплексные типы. Арифметиче- ские операции с комплексными числами появились в С99 для упрощения программи- рования численных методов. Определены следующие комплексные типы: float_Complex float-imaginary double_Complex double_I maginary long double_Complex long double_I maginary Причина того, что в качестве ключевых слов определены -Complex и -Imaginary, а не complex и imaginary, состоит в том, что во многих имеющихся С-программах уже определены их собственные типы комплексных данных, использующие имена complex и imaginary. Определяя ключевые слова —Complex и —Imaginary, С99 по- зволяет не менять уже написанный код. Заголовок <complex.h> определяет (кроме всего прочего) макросы complex и imaginary, которые в результате макроподстановки превращаются в -Complex и Imaginary. Таким образом, в новые программы лучше всего вставлять <complex.h>, а затем использовать макросы complex и imaginary. Типы целых данных long long В стандарте С99 появились новые для С типы данных long long int и unsigned long long int. Диапазон значений типа данных long long int не уже, чем интер- вал от -(263-1) до (263-1). А диапазон значений типа данных unsigned long long int обязан содержать интервал от 0 до 2м-1. Типы long long позволяют поддерживать 64-разрядные целые значения с помощью встроенного типа. 3 Расширение массивов В Стандарте С99 появились два новых для С и достаточно важных свойства массивов: переменная длина и возможность включать в объявлениях массивов квалификаторы типа. 260 Часть II. Стандарт С99
Массивы переменной длины В С89 размерности массивов необходимо объявлять при помощи выражений из целых констант, причем размер массива фиксируется во время компиляции. В силу определенных обстоятельств, в С99 это правило было изменено. В С99 можно объя- вить массив, размерности которого определяются любыми допустимыми целыми вы- ражениями, в том числе и такими, значения которых становятся известны только во время выполнения. Такой массив называется массивом переменной длины (variable- length array, VLA). Однако такими массивами могут быть только локальные массивы (то есть те, у которых область видимости — прототип или блок). Вот пример массива переменной длины: void f(int diml, int dim2) { int matrix[diml][dim2]; /* двумерный массив переменной длины */ /* ... */ } В данном случае размер matrix определяется значениями, передаваемыми функ- ции f () через переменные diml и dim2. Таким образом, в результате каждого вызова f () может получиться массив matrix с самыми разными измерениями. Важно понять, что массивы переменной длины за время “своей жизни” не меняют своих размеров. (Иными словами, они не являются динамическими.) На самом деле массив переменной длины создается с другим размером каждый раз, когда встречается его объявление. Можно указать массив переменной длины неуказанного размера, используя в ка- честве размера звездочку, ♦. Появление массивов переменной длины вызвало небольшое изменение в операто- ре sizeof. Вообще говоря, sizeof — это оператор, который вычисляется во время компиляции. То есть во время компиляции он обычно превращается в целую кон- станту, значение которой равно размеру типа или объекта. Однако если sizeof при- меняется к массиву переменной длины, то свое значение он получает только во время выполнения. Это изменение было необходимо потому, что размер массива перемен- ной длины нельзя узнать до времени выполнения. Одной из главных причин появления массивов переменной длины является жела- ние упростить программирование численных методов. Конечно, это средство приме- няется довольно широко. Но помните — массивы переменной длины не поддержива- ются Стандартом С89 (и в языке C++). Использование квалификаторов типов в объявлении массива В С99 при объявлении массива в качестве параметра функции, внутри квадратных скобок этого объявления можно указать ключевое слово static. Оно сообщает ком- пилятору, что в массиве, на который указывает этот параметр, всегда будет находиться как минимум названное количество элементов. Например: int f(char str[static 80]) { // здесь str всегда является указателем на массив из 80 элементов // } Здесь дается гарантия, что str будет указывать на начало массива типа char, при- чем в нем будет не менее 80 элементов. Глава 11. С99 261
Внутри квадратных скобок также допускаются ключевые слова restrict, volatile и const, но только для параметров функций. Использование restrict оз- начает, что указатель изначально является единственным средством доступа к объекту. Применение const показывает, что указатель указывает на один и тот же массив (то есть указатель всегда указывает на один и тот же объект). Можно использовать и volatile (означает “асинхронно-изменяемый”), хотя и нет смысла это делать. в Однострочные комментарии Благодаря Стандарту С99, в языке С появились однострочные комментарии. Ком- ментарий такого вида начинается с // и доходит до конца строки. Например, I// Это комментарий int i; // это другой комментарий Однострочные комментарии также поддерживаются языком C++. Они удобны тогда, когда нужны краткие замечания, помещающиеся в одной строке. Многие про- граммисты для длинных описаний используют традиционные для языка С много- строчные комментарии, оставляя однострочные комментарии только для объяснений, нужных “по ходу дела”. В Распределение кода и объявлений В соответствии со Стандартом С89 все объявления, находящиеся внутри блока, должны предшествовать первому оператору кода. Но к Стандарту С99 это правило не относится. Рассмотрим, например, программу #include <stdio.h> int main(void) { int i; i = 10; int j; // неправильно для C89, допустимо для С99 и С++ j = i; printf("%d %d", i, j); return 0; } Здесь выражение | i = 10; находится между двумя объявлениями: переменной i и переменной j. Стандарт С89 такое не разрешает. Зато это вполне допускается в С99 (да и в C++ тоже). Возмож- ность распределять объявления и код довольно широко используется в языке C++. Появление этой возможности в языке С облегчает написание кода, который можно использовать в средах обоих языков. 262 Часть II. Стандарт С99
El Изменения препроцессора Стандарт С99 внес небольшие изменения и в препроцессор. Переменные списки аргументов Возможно, самым важным изменением препроцессора является возможность об- рабатывать макросы с переменным количеством аргументов. На переменное количе- ство аргументов указывает многоточие (...), находящееся в определении макроса. Встроенный препроцессорный идентификатор_VA_ARGS___определяет, куда будут подставляться аргументы. Например, после включения в программу определения | #define МуМах(...) max (_VA_ARGS___) выражение | МуМах(а, Ь); преобразуется в | max(а, Ь) ; До обозначения переменного количества аргументов (...) макрос может иметь другие аргументы. Например, после определения | #define compare (compf unc, ...) comp f unc ( VA_ARGS ) оператор | compare(strcmp, "один", "два"); преобразуется в оператор | strcmp("один", "два"); Как видно из примера, встроенный идентификатор__VA_ARGS___заменяется всеми остальными аргументами. Оператор .Pragma С выходом С99 в языке С появился еще один способ определять прагму в про- грамме: оператор Pragma. В общем виде этот оператор выглядит таким образом: _Pragma( "директива ') Здесь директива — это вызываемая прагма1. Появление оператора _Pragma дает прагмам возможность участвовать в макрозамене. Встроенные прагмы В версии С99 определены следующие встроенные прагмы: Прагма Что означает STDC FP_CONTRACT ON/OFF/DEFAULT Во включенном состоянии (ON) выражения с плавающей точкой считаются неделимыми структу- рами, которые обрабатываются с помощью аппаратуры. Состоя- ние по умолчанию (DEFAULT) определяется реализацией. 1 Прагма называется также указанием транслятору, псевдокомментарием, директивой транс- лятора, указанием компилятору, директивой компилятора. — Прим. ред. Глава 11. С99 263
STDC FENV-ACCESS ON/OFF/DEFAULT Сообщает компилятору, что дос- тупна аппаратура для выполне- ния операций с плавающей точ- кой. Состояние по умолчанию определяется реализацией. STDC CX_LIMITED_RANGE ON/OFF/DEFAULT Во включенном состоянии (ON) сообщает компилятору, что не- которые формулы с составными значениями являются безопас- ными. Отключенное состояние (OFF) задается по умолчанию. Подробные сведения об этих прагмах должны быть приведены в документации по компилятору. Новые встроенные макросы К макросам, поддерживаемым С89, в С99 добавлены следующие: ___STDC_HOSTED____ 1, если имеется операционная система __STDC_VERSION___ не меньше, чем 199901L; представляет версию языка С __STDC_IEC_559___ 1, если поддерживаются арифметические опера- ции с плавающей запятой IEC 60559 __STDC_IEC_559_COMPLEX___ 1, если поддерживаются арифметические опера- ции с комплексными числами IEC 60559 __STDC_ISO_10646 Значение в виде ггггммЬ, которое указывает год и месяц выхода спецификации ISO/IEC 10646, поддерживаемой компилятором В Объявление переменных внутри цикла for С99 расширяет возможности цикла for, разрешая объявление одной или нескольких переменных в части инициализации цикла. Область видимости переменной, объявленной таким способом, ограничена блоком программы, управляемым выражением for. То есть переменная, объявленная внутри цикла for, будет локализована внутри этого цикла. Эта возможность появилась в языке С потому, что управляющая переменная цикла for часто необходима только внутри этого цикла. А так как эта переменная локализована внутри цикла, то удается избежать ненужных побочных эффектов. Вот пример, в котором переменная объявляется в части инициализации цикла for: #include <stdio.h> int main(void) { // объявить i внутри for for (int i=0; i < 10; i++) printf("%d", i); return 0; } Здесь переменная i объявляется внутри цикла for, а не до начала его работы. 264 Часть II. Стандарт С99
Как уже говорилось, переменная, объявленная внутри цикла for, локализуется внутри этого цикла. Проанализируйте следующую программу. Обратите внима- ние, что переменная i объявляется дважды: в начале main () и внутри цикла for. #include <stdio.h> int main(void) { int i « -99; // объявить i внутри цикла for for (int i=0; i < 10; i++) printf("%d ’’, i); printf(”n”); printf (’’Значение i равно %d’’, i) ; // выводит -99 return 0; } Эта программа выводит следующее: 1 0123456789 Значение i равно -99 Как показывает вывод, как только заканчивается цикл for, заканчивается и область видимости переменной i, объявленной внутри этого цикла. Таким образом, последнее вы- ражение printf () выводит -99, то есть значение i, объявленное в начале main (). Возможность объявлять управляющие переменные внутри цикла for, уже довольно- таки долгое время имеется в языке C++, и теперь такая возможность используется доста- точно широко. Есть надежда, что большинство С-программистов будут делать то же самое. В Составные литералы С99 дает возможность определять составные литералы, которые являются выраже- ниями, состоящими из массивов, структур или объединений; эти выражения и обо- значают объекты данного типа. Составной литерал создается путем указания имени типа в круглых скобках, за которым следует список инициализации, обязательно за- ключенный в фигурные скобки. Когда именем типа является массив, то размер ука- зывать нельзя. Создается безымянный объект. Вот пример составного литерала: (double *fp = (double []) {1.0, 2.0, 3.0}; В данном случае создается указатель на double, который называется fp и указыва- ет на первый элемент массива, состоящего из трех элементов типа double. Составной литерал, созданный в области видимости файла, существует все время жизни программы. А составной литерал, созданный внутри блока, является локаль- ным объектом, который разрушится, как только при выполнении программы про- изойдет выход из этого блока. Глава 11. С99 265
S Массивы с переменными границами в качестве членов структур С99 дает возможность в качестве последнего члена структуры указывать массив без размера. (В структуре перед гибким массивом-членом должен стоять как минимум еще один член.) Он называется членом-массивом с переменными границами. Таким образом, структура может иметь в качестве члена массив переменного размера. В размере такой структуры, возвращаемом sizeof, память для гибкого массива не учитывается. Обычно память для структуры с членом-массивом с переменными границами распре- деляется автоматически, с помощью malloc (). Кроме размера структуры, необходимо еще выделить дополнительную память, чтобы разместить член-массив с переменными грани- цами нужного размера. Например, если имеется следующее определение структуры I struct mystruct { int а; int b; float fa[]; // массив с переменными границами }; то при выполнении следующего кода будет выделяться место для массива из 10 эле- ментов: I struct mystruct *р; р = (struct mystruct *) malloc(sizeof(struct mystruct) + 10 * sizeof(float)); Так как sizeof (struct mystruct) дает значение, в котором не учтен размер памяти для fa, то при вызове malloc () с помощью выражения | 10 * sizeof(float) дополнительно выделяется место для размещения массива из 10 элементов типа float. Назначенные инициализаторы В С99 появилась новая для С возможность, которая будет особенно полезна для программистов, работающих с разреженными массивами. Это назначенные инициали- заторы. Такие инициализаторы бывают двух видов: одного вида — для массивов, а другого — для структур и объединений. Для массивов используется назначенные ини- циализаторы такого вида: [индекс] = знач где индекс указывает элемент, инициализируемый с помощью значения знач (то есть тот элемент, которому присваивается начальное значение знач). Например, | int а[10] = { [0] = 100, [3] = 200 }; В данном случае инициализируются только элементы с индексами 0 и 3. Для членов структур или объединений используется назначенные инициализаторы такого вида: .имя-члена Применение к структуре назначенного инициализатора позволяет легко инициали- зировать только нужные члены структуры. Например, 266 Часть II. Стандарт С99
struct mystruct { int a; int b; int c; } ob = { . c = 3 0, . a = 10 }; В данном случае член Ь остается неинициализированным. Кроме того, применение назначенных инициализаторов дает возможность ини- циализировать структуру, даже не зная порядка расположения ее членов. Это полезно для предопределенных структур, таких как div_t, или для структур, определенных некоторыми независимыми производителями. g Новые возможности семейства функций printf() и scanf () В С99 для семейства функций printf () и scanf () предусмотрена новая возмож- ность: они могут манипулировать с типами данных long long int и unsigned long long int. Модификатором формата для long long является 11. Например, в сле- дующем фрагменте показано, как выводить значения типа long long int и unsigned long long int: I long long int val; I unsigned long long int u_val; I printf("%lld %llu", val, u_val) ; Модификатор 11 можно применять к спецификаторам формата: d, i, о, и и х — как для printf (), так и для scanf (). В С99 добавлен модификатор hh, который применяется для указания char- аргумента вместе со спецификаторами формата: d, i, о, и и х. Оба модификатора, 11 и hh, можно использовать также вместе со спецификатором п. Спецификаторы формата а и А, которые были добавлены к printf (), заставляют выводить значение с плавающей точкой в шестнадцатеричном формате. Формат зна- чения получается следующий: [-]Oxh.hhhhp+d Если используется А, то х и р будут выводиться на верхнем регистре. Специфика- торы формата а и А были также добавлены к scanf () и они читают значение с пла- вающей точкой. В С99 разрешается при вызове printf () к спецификатору %f добавлять модифи- катор 1 (тогда получится % If), но от этого нет никакой пользы1. В С89 % If для printf () не определяется. ® Новые библиотеки С99 В С99 добавлены новые библиотеки и заголовки. Вот они: Заголовок Назнааение Назнааение <complex.h> Поддерживает арифметические операции с комплексными числами. <fenv.h> Дает доступ к флажкам состояния вычислителя, выполняющего опе- рации с плавающей точкой и другим сигналам этого вычислителя. 1 Иногда есть: если строка формата используется и для других целей. — Прим. ред. Глава 11. С99 267
<inttypes.h> Определяет стандартный, переносимый набор имен целых типов. Также поддерживает функции, которые работают с целыми значе- ниями наибольшей разрядности. <iso646.h> Добавлен в 1995 году Поправкой 1. Определяет имена макросов, соответствующие разным операторам, таким как && и Л. <stdbool.h> Поддерживает логические типы данных. Определяет имена макросов bool, true и false, что помогает обеспечивать совместимость с C++. <stdint.h> Определяет стандартный, переносимый набор имен целых типов. Этот заголовок входит в состав <inttypes. h>. <tgmath.h> Определяет макросы для родового (абстрактного) типа чисел с пла- вающей точкой. <wchar.h> Добавлен в 1995 году Поправкой 1. Поддерживает многобайтовые и двухбайтовые функции. <wctype.h> Добавлен в 1995 году Поправкой 1. Поддерживает многобайтные и двухбайтовые функции классификации. О содержимом этих заголовков и поддерживаемых ими функциях рассказывается части III. S Зарезервированный идентификатор __func__ В С99 определен идентификатор func , который указывает (в виде строко- вого литерала) имя функции, в которой встречается_f unc_. Например, void strUpper(char *str) { static int i = 0; printf (’’Функция %s была вызвана %d раз(а).п",____func____, i) ; while(*str) { *str = toupper(*str); str++; При первом вызове функции StrUpper () появится следующий вывод: | Функция StrUpper была вызвана 1 раз(а). В Расширение граничных значений трансляции Термин “граничные значения трансляции” означает минимальное число разнооб- разных элементов, которые должен обрабатывать компилятор С. Сюда входит длина идентификаторов, количество уровней вложенности, количество выражений case и допустимое количество членов структуры или объединения. В С99 увеличены некото- рые из предельных значений для количества этих элементов несмотря на то, что они и так были достаточно щедро определены Стандартом С89. Вот некоторые примеры: Граничное значение для количества С89 С99 уровней вложенности блоков 15 127 уровней вложенности условных включений 8 63 значащих символов во внутреннем идентификаторе 31 63 268 Часть II. Стандарт С99
значащих символов во внешнем идентификаторе 6 членов структуры или объединения 127 аргументов при вызове функции 31 31 1023 127 В Неявный int больше не поддерживается Несколько лет назад язык C++ отменил правило неявного int, а с приходом С99 это- му примеру последовал и язык С. В С89 правило неявного int гласит, что если явный спецификатор типа отсутствует, то подразумевается тип int. Больше всего это правило применялось к возвращаемому типу функций. В прошлом С-программисты часто пропус- кали int при объявлении функций, которые возвращали значение такого типа. Например, в ранние времена языка С функцию main () часто писали примерно так: Imain () { /*...*/ } При таком подходе возвращаемым типом по умолчанию просто считался int. В С99 (да и в C++) такого правила присвоения типа по умолчанию больше нет, и int приходит- ся указывать явно, что и делается во всех программах, приведенных в этой книге. А вот другой пример. В прошлом функция, такая как Iint isEven(int val) ( return !(val%2); } часто писалась примерно так: /* по умолчанию используется целый тип */ isEven(int val) { return !(val%2); } В первом экземпляре кода возвращаемый тип int указывается явно. Во втором — это подразумевается по умолчанию. Правило неявного int применялось не только к возвращаемым значениям функ- ций (хотя здесь оно применялось чаще всего). Например, в С89 и более ранних вер- сиях функцию isEven () можно было писать примерно еще и так: IisEven(const val) return !(val%2); } Здесь параметр val также имеет по умолчанию тип int — в этом случае const int. И опять, это присвоение типа по умолчанию int не поддерживается в Стандарте С99. На заметку В действительности компилятор, совместимый с С99, может принять код, содержащий неявные типы int, даже после того, как выдаст предупреждение об ошибке. Так что иногда можно компилировать и старый код. Однако компи- лятор, совместимый с С99, не обязан принимать такой код. Глава 11. С99 269
В Удалены неявные объявления функций Если в С89 встречался вызов функции до явного объявления, то создавалось неяв- ное объявление этой функции. Это неявное объявление имеет такой вид: extern int имя() В С99 неявные объявления функций не поддерживаются. На заметку В действительности компилятор, совместимый с С99, может принять код, содержащий неявные объявления функций, даже после того, как выдаст преду- преждение об ошибке. Так что можно компилировать и старый код. Однако компилятор, совместимый с С99, не обязан принимать такой код. И Ограничения на return В С89 в функции, которая имеет возвращаемый тип, отличный от void (т.е. предполагается, что такая функция возвращает значение), может встретиться опе- ратор return без выражения. Хотя в результате этого теоретически поведение программы было неопределенным, технически в этом не было ничего “незаконного”. Но в С99 в функции, тип которой отличен от void, оператор return обязан иметь выражение. То есть в С99 внутри функции, которая согласно определению возвращает значение, любой оператор return обязан иметь ассо- циированное с ним значение, которое и будет возвращено этой функцией. Таким образом, следующая функция является синтаксически допустимой в С89, но не- допустима в С99: Iint f(void) { /* ... */ return ; // в С99 этот оператор должен возвращать значение } Расширенные целые типы С99 в <stdint.h> определяет несколько расширенных целых типов. Расши- ренные типы включают в себя типы с точной разрядностью, минимальной раз- рядностью, максимальной разрядностью и самый быстрый целый тип. Вот под- борка таких типов: Расширенный тип int!6_t int_Jeastl6_t int_fast32_t intmax_t uintmax_t Что означает Тип 16-разрядных целых Тип целых, содержащий не менее 16 разрядов Самый быстрый тип целых, содержащий не менее 32 разрядов Тип самых больших целых Тип самых больших целых без знака Расширенные типы облегчают написание переносимого кода. Более подробно они описаны в части III. 270 Часть II. Стандарт С99
S Изменения в правилах продвижения целых типов В С99 расширены правила продвижения целых типов. В С89 значение типа char, short int или битового поля int можно было использовать в выражении вместо int или unsigned int. Если продвинутое значение помещалось в int, то продвижение выполнялось до int; в противном же случае первоначальное значение продвигалось до unsigned int. В С99 каждому целому типу присвоен ранг. Например, ранг long long int выше, чем ранг int, который в свою очередь выше, чем ранг char и так далее. В выражении любой целый тип, ранг которого ниже, чем ранг int или unsigned int, может ис- пользоваться вместо int или unsigned int. Глава 11. С99 271
Полный справочник по Часть III Стандартная библиотека С В части III книги рассматривается стандартная библи- отека языка С. В главе 12 обсуждаются вопросы редак- тирования связей, использования библиотек и заголов- ков. В главах 13—20 описаны функции стандартной библиотеки, причем каждая глава посвящена отдельно- му разделу библиотеки. В этой книге дается описание стандартных функций, определенных как для С89, так и для С99. В С99 входят все функции, заданные для С89. Поэтому при наличии
компилятора, поддерживающего стандарт С99, можно пользоваться всеми функция- ми, описанными в данной части. Для компилятора, поддерживающего стандарт С89, функции, определенные только в С99, недоступны. Кроме того, в стандарт С+4- вхо- дят функции, определенные для С89, но не входят те из них, что определены в С99. Если функция определена только в С99, то об этом будет сказано в ее описании. При изучении стандартной библиотеки следует помнить, что большинство создате- лей компиляторов стараются сделать свою библиотеку как можно более полной. Биб- лиотека конкретного компилятора может содержать большое количество дополни- тельных функций, не рассматриваемых здесь. Например, стандартная библиотека языка С не содержит никаких графических функций, а также функций вывода на эк- ран, потому что они зависят от вычислительной среды. Тем не менее, в большинстве конкретных компиляторов такие функции есть, поэтому всегда полезно просматри- вать документацию по используемому компилятору.
Полный справочник по Глава 12 Редактирование связей, использование библиотек и заголовков
Работа по написанию компилятора языка С фактически состоит из двух частей. Первая часть — это написание самого компилятора, который преобразует исходный файл в объектный файл. Вторая часть — разработка стандартной библиотеки. Как ни удивитель- но, но создать компилятор сравнительно просто. Чаще всего большую часть времени и усилий забирает именно работа над библиотечными функциями. Одна из причин этого заключается в том, что многие функции (такие как функции ввода/вывода) должны взаи- модействовать с той операционной системой, для которой написан компилятор. Кроме того, стандартная библиотека С содержит большое количество самых разнообразных функций. Действительно, язык С выделяется среди других именно благодаря богатству и гибкости своих возможностей, заложенных в стандартной библиотеке. В последующих главах будут описаны библиотечные функции С, а в этой главе речь пойдет о фундаментальных концепциях их использования; мы обсудим процесс редактирования связей1, библиотеки и заголовки. В Редактор связей Редактор связей1 2 выполняет две функции. Во-первых, как можно заключить по его названию, он комбинирует (компонует, редактирует) различные объектные файлы. Вторая его функция — разрешать адреса вызовов и инструкций загрузки, найденных в редактируемых объектных файлах. Чтобы понять принцип работы редактора связей, рассмотрим подробнее процесс раздельной компиляции. Раздельная компиляция Раздельная компиляция — это возможность, позволяющая разбить программу на не- сколько файлов, скомпилировать каждый из этих файлов отдельно, а потом скомпоно- вать3 4 их, чтобы в конечном итоге создать исполняемый файл. Результатом работы компилятора является объектный файл, а результатом работы редактора связей — ис- полняемый файл*. Редактор связей физически связывает файлы, внесенные в список компоновки, в один программный файл и разрешает внешние ссылки. Внешняя ссылка создается каждый раз, когда программа из одного файла ссылается на код из другого файла. Это происходит при вызове функции и при ссылке на глобальную перемен- ную. Например, при компоновке двух приведенных ниже файлов, должна быть раз- решена ссылка в файле 2 на идентификатор count, объявленный в файле 1. Редактор связей сообщает программе из файла 2, где найти count. Файл 1 Файл 2 int count; #include <stdio.h> void display(void); extern int count; int main(void) { count = 10; void display(void) { printf("%d", count); display(); } return 0; } 1 Называется также компоновщиком. — Прим. ред. 2 Иногда называется также компоновщиком. Впрочем, компоновщиками обычно называют программы, обладающие несколько меньшими возможностями, чем развитые редакторы свя- зей. — Прим. ред. 3 Этот процесс называется редактированием связей. — Прим. ред. 4 Называется также загрузочным модулем. — Прим. ред. 276 Часть III. Стандартная библиотека С
Аналогично, редактор связей укажет файлу 1, где находится функция display(), чтобы можно было ее вызвать. При генерации объектного кода функции di splay О, компилятор подставляет в него вместо адреса идентификатора count “заполнитель”, т.е. ссылку на внешнее имя, потому что он не располагает информацией о том, где находится count. Нечто подобное происходит при компиляции main(). Адрес функции display() не извес- тен, поэтому вместо него используется “заполнитель”, т.е. ссылка на внешнюю про- грамму. При компоновке этих двух файлов содержащиеся в них внешние ссылки за- меняются адресами соответствующих элементов. Являются ли эти адреса абсолютны- ми или переместимыми, — зависит от среды1. Переместимые коды и абсолютные коды В результате работы редактора связей для большинства видов вычислительной сре- ды получается переместимый код. Так называют объектный код, который может рабо- тать в любой свободной области памяти, способной его уместить. В переместимом объектном файле адрес каждой инструкции вызова или загрузки является не фикси- рованным, а относительным. Таким образом, адреса в переместимом коде отсчитыва- ются от адреса начала программы. При загрузке программы в память для выполнения, загрузчик преобразует относительные адреса в физические адреса, соответствующие адресам ячеек памяти, в которую загружается программа. В некоторых вычислительных средах, таких как специализированные устройства управления, в которых для всех программ используется одно и то же адресное про- странство, редактор связей подставляет в конечный результат своей работы физиче- ские адреса. В этом случае он генерирует абсолютный код1 2. Редактирование связей с оверлеями Хотя в наше время эта возможность применяется редко, следует отметить, что компиляторы С некоторых вычислительных сред в дополнение к обычным компо- новщикам предоставляют компоновщики оверлеев. Компоновщик оверлеев работает так же, как и обычный, но он также может создавать оверлеи3. Оверлей — это фрагмент объектного4 кода, который хранится в файле на диске и загружается для работы толь- ко по мере необходимости. Место в памяти, которое отводится для загрузки оверлеев, называется оверлейной областью памяти. Оверлеи позволяют создавать и запускать программы, которые занимали бы большую область памяти, чем имеющаяся в нали- чии, потому что в каждый момент времени в памяти находится только та часть про- граммы, которая нужна. Чтобы понять, как работают оверлеи, представим, что имеется программа, состоя- щая из семи объектных файлов, которые называются Fl, F2, ..., F7. Допустим, имею- щейся свободной памяти недостаточно для загрузки программы, скомпонованной обычным образом из всех объектных файлов. Есть возможность скомпоновать только первые пять файлов, а при большем количестве наступит переполнение памяти. Вый- 1 Редакторы связей обычно располагают широким набором возможностей, и при необходи- мости пользователь может указать необходимые параметры. — Прим. ред. 2 Называется также программой в абсолютных адресах. — Прим. ред. 3 Создание оверлейных программ — стандартная функция для редакторов связей. Кроме того, редакторы связей обычно могут создавать даже загрузочные модули, загружаемые “вразброс”, т.е. в несмежные участки памяти. Все это имеет огромное значение для систем, в которых отсутствует виртуальная память. Но в системах с виртуальной памятью эти возможно- сти часто выглядят как ненужные излишества. — Прим. ред. 4 Согласно оригиналу. Все же точнее часть загрузочного модуля. — Прим. ред. Глава 12. Редактирование связей, использование библиотек и заголовков 277
ти из подобной ситуации можно, дав компоновщику команду создать оверлеи из фай- лов F5, F6 и F7. При каждом вызове функции, которая содержится в одном из этих файлов, администратор оверлейной загрузки (программа, предоставляемая компонов- щиком или редактором связей) находит необходимый файл и помещает его в овер- лейную область памяти, создавая условия для работы программы. Коды, которые по- лучились при компиляции файлов Fl — F4, остаются резидентными. Данная схема проиллюстрирована на рис. 12.1. Как читатель, возможно, уже догадался, принципиальным преимуществом оверлеев яв- ляется возможность создавать с их помощью очень большие программы. Основной же их недостаток и причина редкого использования состоит в том, что процесс загрузки занима- ет определенное время и в значительной мере влияет на быстродействие программы. По- этому при использовании оверлеев следует взаимосвязанные между собой функции груп- пировать вместе, чтобы свести к минимуму число загрузок оверлеев. Рис. 12.1 Программа с оверлеями, загруженными в память Например, если приложение обрабатывает список рассылки, то имеет смысл по- местить все подпрограммы сортировки в один оверлей, подпрограммы печати — в другой и т.д. Как уже было сказано, в современных вычислительных средах оверлеи применя- ются редко. Связывание с динамически подсоединяемыми библиотеками (DLL) Операционная система Windows предоставляет другой вид связывания, так назы- ваемое динамическое связывание. Динамическое связывание — это процесс, при кото- ром объектный код функции остается в отдельном файле на диске до тех пор, пока не запустится использующая его программа. При запуске такой программы динамически загружаются затребованные, связанные с ней функции. Динамически связанные функции помещаются в специальный тип библиотек, которые называются динамиче- ски подсоединяемыми библиотеками (DLL — Dynamic-Link Library). Основным преимуществом таких библиотек является возможность значительно сокра- тить размер исполняемых программ, потому что отпадает необходимость в том, чтобы ка- ждая программа содержала в себе копию используемых библиотечных функций. Другим положительным моментом является то, что при обновлении функций DLL, использующие их программы автоматически используют и все улучшения новых версий. 278 Часть III. Стандартная библиотека С
Стандартная библиотека С не содержится в динамически подсоединяемой библио- теке, но многие другие типы функций там есть. Например, при написании приложе- ний для Windows, в DLL хранится полный набор функций программного интерфейса приложений (API — Application Program Interface). Нужно отметить, что для програм- мы, написанной на языке С, обычно не имеет значения, хранятся ли библиотечные функции в DLL или в обычном файле библиотек. В Стандартная библиотека С Содержимое и форма стандартной библиотеки С задается Стандартом ANSI/ISO. Т.е. Стандарт С определяет тот набор функций, который должен поддерживать любой стандартный компилятор. Однако при этом большинство компиляторов предоставля- ют дополнительные функции, которые не специфицированы в Стандарте. Например, многие компиляторы имеют функции работы с графикой, подпрограммы, управляе- мые с помощью мышки, и другие им подобные, которых нет в Стандарте С. Пока программа не переносится в другую вычислительную среду, эти нестандартные функ- ции можно использовать без каких-либо негативных последствий. Но если программа должна быть переносимой, применение таких программ нужно ограничить. На самом деле практически все нетривиальные программы С используют нестандартные функ- ции, так что не нужно пугаться и избегать их только из-за того, что они не входят в стандартную библиотеку функций. Библиотечные файлы и объектные файлы Хотя библиотеки похожи на объектные файлы, между библиотеками и объектными файлами есть одно важное различие. При компоновке объектных файлов все содер- жимое каждого объектного файла становится частью конечной исполняемой про- граммы. При этом не важно, используется на самом деле этот код или нет. В случае с файлами библиотек ситуация иная. Библиотека представляет собой набор функций. В отличие от объектных файлов, в библиотеке каждая функция хранится отдельно. Когда программа использует библио- течную функцию, редактор связей находит эту функцию и добавляет ее код в про- грамму. Таким образом, исполняемый файл содержит только те функции, которые используются программой, а не все библиотечные функции. Поэтому лучше хранить стандартные функции С не в объектных файлах, а в библиотеках, из которых они до- бавляются в программу избирательно. И Заголовки С каждой функцией стандартной библиотеки С связан свой заголовок. Соответст- вующие заголовки используемых функций должны быть включены в программу с по- мощью директивы #include. Заголовки выполняют две важные функции. Во-первых, многие функции стандартной библиотеки работают с данными собственного опреде- ленного типа, к которым должна иметь доступ основная программа, использующая эти функции. Эти типы данных задаются в заголовках, связанных с каждой функцией. Одним из наиболее распространенных примеров является заголовок файловой систе- мы <stdio.h>, определяющий тип file, который необходим для выполнения опера- ций с файлами на диске. Второй причиной включения заголовков является необходимость получения прототи- пов библиотечных функций. Прототипы функций позволяют компилятору производить Глава 12. Редактирование связей, использование библиотек и заголовков 279
более строгую проверку типов. Хотя прототипы технически являются необязательными, они необходимы для всех практических целей. Кроме того, они нужны для C++. Все программы, содержащиеся в этой книге, подразумевают наличие полного прототипа. Список стандартных заголовков, определенных Стандартом С89, приведен в таб- лице 12.1. В таблице 12.2 приведены заголовки, добавленные в Стандарте С99. Стандартом С для заголовков зарезервированы идентификаторы, начинающиеся сим- волом подчеркивания, за которым следует символ подчеркивания либо заглавная буква. Как уже было сказано в части 1, заголовки — это, как правило, файлы, но не все- гда. Компилятор может предопределить содержимое заголовка внутренним образом. Однако в практических целях содержимое стандартных заголовков С находится в файлах, имена которых совпадают с именами самих заголовков. В следующих главах части III, описывающих все стандартные библиотечные функ- ции, для каждой функции указаны соответствующие ей заголовки. Таблица 12.1. Заголовки, определенные в С89 Заголовок Назначение <assert.h> <ctype.h> <errno.h> <float.h> <limits.h> <locate.h> <math.h> <setjmp.h> <signal.h> <stdarg.h> <stddef.h> <stdio.h> <stdlib.h> <string.h> <time.h> Определяет макрос assert () Обработка символов Выдача сообщения об ошибках Задает пределы значений с плавающей точкой, зависящие от реализации Задает различные ограничения, зависящие от реализации Поддерживает локализацию Различные определения, используемые математической библиотекой Поддерживает нелокальные переходы Поддерживает обработку сигналов Поддерживает списки входных параметров функции с переменным числом аргументов Определяет некоторые наиболее часто используемые константы Поддерживает систему ввода/вывода Смешанные объявления Поддерживает функции обработки строк Поддерживает функции, обращающиеся к системному времени Таблица 12.2. Заголовки, добавленные в С99 Заголовок <complex.h> <fenv.h> Назначение Поддерживает арифметические операции с комплексными числами Предоставляет доступ к флажкам состояния вычислителя, выполняющего опера- ции с плавающей точкой, а также доступ к другим сигналам этого вычислителя <inttypes.h> Определяет стандартный, переносимый набор имен целочисленных типов, поддержи- вает функции, которые работают с целыми значениями наибольшей разрядности <iso646.h> Добавлено в 1995 году Поправкой 1; определяет макросы, соответствующие раз- личным операторам, например && и А <stdbool.h> Поддерживает логические типы данных; определяет макрос bool, способствую- щий совместимости с языком C++ <stdint.h> Задает стандартный переносимый набор имен целочисленных типов; этот файл включен в заголовок <inttypes. h> <tgmath.h> <wchar.h> Определяет макросы для родового (абстрактного) типа чисел с плавающей точкой Добавлен в 1995 году Поправкой 1; поддерживает функции обработки многобайто- вых слов и двухбайтовых символов <wctype.h> Добавлен в 1995 году Поправкой 1; поддерживает функции классификации много- байтовых слов и двухбайтовых символов 280 Часть III. Стандартная библиотека С
Макросы в заголовках Многие стандартные функции С можно ввести либо как собственно функции, ли- бо как подобные функциям макросы, заданные в заголовке. Например, функцию abs(), которая возвращает абсолютную величину целочисленного аргумента, можно также задать как макрос: | #define abs(i) (i)<0 ? —(i):(i) Обычно не имеет значения, определена ли стандартная функция как макрос или как обычная функция С. Однако в редких случаях, когда макросы неприменимы, — например, если размер программы должен быть минимальным, или если аргумент нельзя вычислять больше одного раза, —нужно создавать обычные функции и под- ставлять их вместо макроса. Иногда в самой библиотеке С содержатся функции, кото- рые можно использовать для замены ими макросов. Чтобы компилятор использовал истинную функцию, необходимо предпринять ме- ры против подстановки им макроса на место имени функции. Для этого есть несколь- ко способов, но, безусловно, лучший из них — просто пометить имя макроса как не- определенное с помощью ttundef. Например, чтобы заставить компилятор подставить вместо ранее определенного макроса истинную функцию abs (), можно в начало программы вставить следующую строку: | #undef abs Теперь, когда abs больше не является макросом, будет использоваться функция. На заметку Н Переопределение библиотечных функций Хотя реализация редакторов связей может иметь некоторые различия, все они в основном работают одинаково. Например, если программа состоит из трех файлов с именами Fl, F2 и F3, команда редактора связей выглядит примерно так: | LINK Fl F2 F3 LIBC где LIBC — это имя стандартной библиотеки. Некоторые редакторы связей используют стандартную библиотеку автома- тически и не требуют, чтобы она была задана явно. Кроме того, часто соот- ветствующие библиотечные файлы автоматически включены в интегриро- ванную среду программирования. С началом процесса редактирования связей редактор связей обычно пытается разре- шить все внешние ссылки, ограничившись только файлами Fl, F2 и F3. Если это сделано, и еще остались неразрешенные внешние ссылки, редактор связей ищет их в библиотеке. Пользуясь тем, что большинство редакторов связей работает так, как описано вы- ше, стандартную библиотечную функцию можно переопределить. Например, можно создать свою собственную версию функции fwrite (), которая производит обработку выходных данных каким-то определенным образом. В этом случае, при компоновке программы, содержащей определенную программистом версию fwrite (), редактор связей находит эту реализацию раньше и использует ее для разрешения всех соответ- ствующих ссылок. Поэтому ко времени просмотра библиотеки неразрешенных ссылок на fwrite () не останется, и она не загрузится из библиотеки. При переопределении библиотечных функций нужно быть очень осторожным, по- тому что могут возникнуть неожиданные побочные эффекты. Может случиться, что какая-то часть программы использует библиотечную функцию, а эта функция уже пе- реопределена. В этом случае эта часть вместо ожидаемой библиотечной функции по- лучит переопределенную функцию. Например, если функция переопределена для ис- Глава 12. Редактирование связей, использование библиотек и заголовков 281
пользования в одной части программы, а другая часть этой программы использует стандартную библиотечную функцию, то, как минимум, это может привести к не- предсказуемому поведению программы. Поэтому лучше просто использовать другие имена для функций, чем переопределять библиотечные функции. 282 Часть III. Стандартная библиотека С
Полный справочник по Глава 13 Функции ввода/вывода
В этой главе описаны стандартные функции ввода/вывода в языке С. Сюда вошли функции, определенные как в Стандарте С89, так и в Стандарте С99. С функция- ми ввода/вывода ассоциирован заголовок <stdio.h>. Этот заголовок определяет не- которые макросы и типы, которые используются файловой системой. Наиболее важ- ным из них является тип FILE, который используется для объявления указателя на файл. Два других часто используемых типа — size_t и fpos_t. Тип size_t, пред- ставляющий собой некоторую разновидность целых без знака, — это тип результата, возвращаемого функцией sizeof. Тип fpos_t определяет объект, который однознач- но задает каждую позицию в файле. Самым популярным макросом, определенным в этом заголовке, является макрос EOF, значение которого указывает на конец файла. Другие типы данных и макросы, определенные в заголовке <stdio.h>, описаны вме- сте с функциями, с которыми они связаны. Многие функции ввода/вывода при возникновении ошибки присваивают встроен- ной глобальной переменной целого типа errno определенное значение. Анализ этой переменной поможет программе получить более подробную информацию о возник- шей ошибке. Значения, которые может принимать переменная errno, зависят от конкретной реализации компилятора. В версии С99 введен квалификатор restrict, который применяется к некоторым параметрам нескольких функций, первоначально определенных в версии С89. При рассмотрении каждой такой функции будет приведен ее прототип, используемый в среде С89 (который одновременно является прототипом в C++), а параметры с атри- бутом restrict будут отмечены в описании этой функции. Обзор системы ввода/вывода приведен в главах 8 и 9 части I. На заметку В этой главе описаны функции познакового ввода/вывода. Эти функции были введены в Стандарт С с самого начала и, безусловно, являются наиболее часто используемыми. В 1995 году было добавлено несколько функций, позво- ляющих обрабатывать символы в расширенном 16-битном алфавите (wchar_t); эти функции кратко описаны в главе 19. а Функция clearerr |#include <stdio.h> void clearerr(FILE ★stream); Функция clearerr () сбрасывает (т.е. устанавливает равным нулю) признак ошибки, связанный с потоком, на который указывает элемент stream. При этом также сбрасывается признак конца файла. При успешном обращении к функции f open () признаки ошибок для каждого потока первоначально устанавливаются равными нулю. При работе с файлами ошибки могут воз- никать по различным причинам, многие из которых зависят от конкретной системы. Ис- тинную природу ошибки можно определить в результате вызова функции реггог (), кото- рая выводит сообщение, описывающее ошибку (см. описание функции реггог ()). Пример Приведенная ниже программа копирует один файл в другой. При возникновении ошибки выводится сообщение, поясняющее ее природу. I/* Копирование одного файла в другой. */ #include <stdio.h> ^include <stdlib.h> 284 Часть III. Стандартная библиотека С
int main (int argc, char *argv [ ] ) { FILE *in, *out; char ch; if (argc!=3) { printf("He введено имя файла.n"); exit (1); if((in=fopen(argv[1], "rb")) == NULL) { printf (’’Невозможно открыть входной файл. n”) ; exit (1); if((out=fopen(argv[2], ”wb”)) == NULL) { printf (’’Невозможно открыть выходной файл. n”) ; exit (1); while(!feof(in)) { ch = getc(in); if(ferror(in)) { printf("Ошибка чтения"); clearerr(in); break; } else { if(!feof(in)) putc(ch, out); if(ferror(out)) { printf("Ошибка записи"); clearerr(out); break; } } } fclose(in); fclose(out); return 0; } Зависимые функции feof (), ferror () иреггогО H Функция fclose |#include <stdio.h> int fclose(FILE ★stream); Функция fclose () закрывает файл, связанный с потоком stream, и дозаписывает его буфер. После обращения к функции fclose () элемент stream больше не связан с файлом, и все автоматически выделенные буфера освобождаются. При успешном выполнении функция fclose () возвращает нуль; в противном случае возвращается значение EOF. Попытка закрыть уже закрытый файл расценивается как ошибка. В случае прекращения доступа к носителю данных до закрытия файла, как и при недостатке свободного пространства на диске, будет сгенерирована ошибка. Глава 13. Функции ввода/вывода 285
Пример Следующая программа открывает и закрывает файл. tfinclude <stdio«h> ^include <stdlib«h> int main(void) { FILE *fp; if ( (fp=fopen (’’test" r "rb"))==NULL) { printf ("He удается открыть файл.п’’); exit (1); } if (fclose(fp))printf("Ошибка при закрытии файла.n"); return 0; } Зависимые функции fopen (), freopen () HfflushO [] Функция feof |#include <stdio.h> int feof(FILE ★stream); Функция f eof () проверяет, достигнут ли конец файла, связанного с потоком stream. Если указатель текущей позиции файла установлен на конец файла, возвраща- ется ненулевое значение; в противном случае возвращается нуль. При достижении конца файла последующие операции чтения будут возвращать значение EOF до тех пор, пока не будет вызвана функция rewind () или пока указа- тель текущей позиции файла не будет установлен на новую позицию с помощью функции fseek (). Функция feof () особенно полезна при работе с двоичными файлами, поскольку маркер конца файла также является полноценным двоичным целым. Например, что- бы определить момент достижения конца двоичного файла вместо простой проверки значения, возвращаемого функцией getc (), следует явным образом обратиться к функции feof (). Пример Данный фрагмент программы показывает один из способов определения конца файла. I/* Предполагается, что файл fp был открыт для чтения. ★/ while(!feof(fp)) getc(fp); Зависимые функции clearerr(), ferror(), perror(), putc() и getc() 286 Часть III. Стандартная библиотека С
El Функция ferror |#include <stdio.h> int ferror(FILE ★stream); Функция ferror () проверяет наличие ошибки при работе с файлом, связанным с потоком stream. Нулевое значение, возвращаемое этой функцией, говорит о том, что никакой ошибки не обнаружено, а ненулевое значение означает ее наличие. Чтобы определить природу ошибки, нужно воспользоваться функцией реггог (). Пример Следующий фрагмент программы приводит к аварийному прекращению ее работы при возникновении ошибки. /* Предполагается, что fp указывает на поток, открытый для записи. */ while(!done) { putc(info, fp) ; if(ferror(fp)) { printf("Ошибка при работе с файломп"); exit(1); } } Зависимые функции clearerr (), feof() и реггог() EI Функция ff lush |#include <stdio.h> int fflush(FILE ★stream); Если поток stream связан с файлом, открытым для записи, то при обращении к функции fflushO в этот файл будет физически записано содержимое выходного буфера. При этом файл остается открытым. Нулевое значение, возвращаемое функцией, свидетельствует о ее успешном вы- полнении, а значение EOF — о возникновении ошибки при записи. При нормальном завершении программы или при заполнении буферов все их со- держимое автоматически дозаписывается в файл. Кроме того, буфер дозаписывается в файл при закрытии файла. Пример Приведенный фрагмент программы дозаписывает в файл содержимое буфера после каждой операции записи. I /* I Предполагается, что fp связан с выходным файлом I */ Глава 13. Функции ввода/вывода 287
|for(i=0; i<MAX; i++) { fwrite(buf, sizeof(some_type), 1, fp) ; fflush(fp); ) Зависимые функции fclose (), fopen (), freadO, fwriteO, getc () и putc () Hl Функция fgetc |#include <stdio.h> int fgetc(FILE ★stream); Функция fgetc () возвращает символ, взятый из входного потока stream и нахо- дящийся сразу после текущей позиции, а также увеличивает указатель текущей пози- ции файла. Этот символ читается как значение типа unsigned char, преобразован- ное в целое. При достижении конца файла функция fgetc () возвращает значение EOF. Но по- скольку значение EOF является действительным целым значением, при работе с дво- ичными файлами для обнаружения конца файла необходимо использовать функцию feof (). Если функция fgetc () обнаруживает ошибку, она возвращает значение EOF. Для выявления ошибок, возникающих при работе с двоичными файлами, необходимо использовать функцию ferror (). Пример Следующая программа читает и выводит на экран содержимое текстового файла. #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { FILE *fp; char ch; if((fp=fopen(argv[1],"r"))==NULL) { printf("Невозможно открыть файл.п"); exit (1) ; } while((ch=fgetc(fp)) != EOF) { printf("%c", ch); } fclose(fp); return 0; } Зависимые функции fputc(), getc(), putc() и fopen() 288 Часть III. Стандартная библиотека С
в Функция fgetpos |#include <stdio.h> int fgetpos(FILE ★stream, fpos_t ★position); Функция f getpos () сохраняет в объекте, на который указывает параметр position, текущее значение указателя позиции файла из заданного потока. Объект, адресуемый элементом position, должен иметь тип fpos t. Сохраняемое значение может быть по- лезно только для последующего обращения к функции f setpos (). Отметим, что в версии С99 к параметрам stream и position применяется квалифика- тор restrict. При возникновении ошибки функция fgetpos () возвращает ненулевое значение; в противном случае возвращается нуль. Пример Следующий фрагмент программы присваивает переменной f ile_loc текущее зна- чение положения файла. FILE *fp; fpos_t file_loc; fgetpos(fp, &file_loc); Зависимые функции fsetpos (), fseek() HftellO ISI Функция fgets |#include <stdio.h> char *fgets(char ★str, int num, FILE *stream); Функция fgets () читает из входного потока stream не более пит-1 символов и помещает их в массив символов, адресуемый указателем str. Символы читаются до тех пор, пока не будет прочитан символ новой строки или значение EOF, либо пока не будет достигнут заданный предел. По завершении чтения символов сразу же за по- следним из них размещается нулевой символ. Символ новой строки сохраняется и становится частью массива, адресуемого элементом str. В версии С99 к параметрам str и stream применен квалификатор restrict. При успешном выполнении функция fgets () возвращает значение str, а в случае сбоя — нулевой указатель. В случае ошибки содержимое массива, к которому отсылает указатель str, не определено. Поскольку функция fgets () возвращает нулевой указатель и при возникновении ошибки, и при достижении конца файла, то для выяснения, что же произошло на самом деле, необходимо использовать функцию feof () или ferror (). Пример Приведенная программа использует функцию fgets () для вывода содержимого текстового файла, имя которого задано первым аргументом командной строки. I #include <stdio.h> I ttinclude <stdlib.h> Глава 13. Функции ввода/вывода 289
int main(int argc, char *argv[]) { FILE *fp; char str[128]; if((fp=fopen(argv[1],"r"))==NULL) { printf("He удается открыть файл.п"); exit(1); while(!feof(fp)) { if(fgets(str, 126, fp)) printf("%s", str); } fclose(fp); return 0; Зависимые функции fputs (), fgetc (), gets() Hputs() В Функция fopen |#include <stdio.h> FILE *fopen(const char ★fname, const char ★mode); Функция fopen () открывает файл, имя которого задается параметром fname, и возвращает указатель на поток, связанный с этим файлом. Типы операций, которые разрешено выполнять с файлом, определяются параметром mode. Возможные значе- ния параметра mode приведены в таблице 13.1. Строка символов, которая будет играть роль имени реального файла, должна определять его имя, допустимое в данной опе- рационной системе. Эта строка может включать спецификацию пути, если среда под- держивает такую возможность. В версии С99 к параметрам fname и mode применен квалификатор restrict. Если функция fopen () успешно открыла заданный файл, она возвращает указа- тель FILE. Если файл открыть не удается, возвращается нулевой указатель. Таблица 13.1. Допустимые значения параметра mode функции fopen() Режим Назначение "Г Открывает текстовый файл для чтения ”w" Создает текстовый файл для записи ••а" Дописывает в текстовый файл "rb" Открывает двоичный файл для чтения "wb" Создает двоичный файл для записи "ab" Дописывает в двоичный файл "Г+" Открывает текстовый файл для чтения и записи "w+" Создает текстовый файл для чтения и записи "а+" Открывает текстовый файл для чтения и записи "rb+" или "г+Ь" Открывает двоичный файл для чтения и записи "wb+" или "w+b" Создает двоичный файл для чтения и записи "ab+" или "а+Ь" Открывает двоичный файл для чтения и записи 290 Часть III. Стандартная библиотека С
Как видно из таблицы, файл можно открывать либо в текстовом, либо в двоичном режиме. В текстовом режиме выполняются преобразования некоторых символов. На- пример, символы новой строки преобразуются в комбинацию кодов возврата каретки (ASCII 13) и конца строки (ASCII 10). В двоичном режиме подобные преобразования не выполняются. В следующем фрагменте программы иллюстрируется корректный способ от- крытия файла. FILE *fp; if ((fp = fopen("test", ”w"))==NULL) { printf("He удается открыть файл.п"); exit (1); } Благодаря такому методу перед записью в файл выявляется любая ошибка, возни- кающая при его открытии, например, использование защищенного от записи или за- полненного диска. Если с помощью функции fopen () открывается файл для вывода (записи), то лю- бой уже существующий файл с заданным именем удаляется, а вместо него создается новый. Если файл с таким именем не существует, он будет создан. Чтобы открыть файл для выполнения операций чтения, нужно, чтобы этот файл уже существовал. В противном случае функция возвратит значение ошибки. Чтобы добавить данные в ко- нец файла, необходимо использовать режим "а". Если окажется, что указанный файл не существует, он будет создан. Осуществляя доступ к файлу, который открыт для чтения и записи, не следует сра- зу за операцией ввода выполнять операцию вывода, не прибегнув прежде к промежу- точному вызову одной из следующих функций: fflushO, fseekO, fsetposO или rewind (). Нельзя также сразу за операцией вывода выполнять операцию ввода, не прибегнув прежде к промежуточному вызову одной из перечисленных выше функций. Исключением является момент достижения конца файла во время операции ввода, т.е. в конце файла вывод может непосредственно следовать за вводом. Максимальное количество файлов, которые могут быть открыты одновременно, ограничивается значением FOPEN_MAX, определенным в заголовке <stdio.h>. Пример Следующий фрагмент открывает файл с названием TEST для чтения-записи в дво- ичном режиме. FILE *fp; if ((fp = fopen("test", "rb+"))==NULL) { printf("He удается открыть файл.п"); exit (1); } Зависимые функции fclose (), freadO, fwriteO, putc () и getc () SI Функция fprintf |#include <stdio.h> int fprintf(FILE ★stream, const char *format,...); Глава 13. Функции ввода/вывода 291
Функция fprintfO выводит в поток, адресуемый параметром stream, значения аргументов, составляющих список аргументов, в соответствии с заданной строкой формата format. Возвращаемое значение равно количеству реально выведенных симво- лов. Если при выводе возникла ошибка, возвращается отрицательное число. В версии С99 к параметрам stream и format применен квалификатор restrict. Операции преобразования, заданные в строке формата, и команды вывода анало- гичны операциям и командам, используемым в функции printf О ; их полное описа- ние приводится в разделе, посвященном функции printf. Пример Приведенная программа создает файл с названием TEST и записывает в него стро- ку это тест 10 20.01 В формате, заданном функцией fprintf (). #include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; if((fp=fopen("test","wb"))==NULL) { printf("He удается открыть файл.Хп"); exit(1); } fprintf(fp, "это тест %d %f", 10, 20.01); fclose(fp); return 0; } Зависимые функции printf() и fscanf() В Функция fputc |#include <stdio.h> int fputc(int ch, FILE ★stream); Функция fputc () записывает символ ch в текущую позицию потока stream, а затем увеличивает указатель текущей позиции файла. Хотя на практике при объ- явлении символа ch он всегда имеет тип int, функцией fputc () тип символа преобразуется в unsigned char. Поскольку в момент вызова символьный аргу- мент преобразуется к целому типу, в качестве аргументов обычно можно исполь- зовать и символьные переменные. При использовании целого значения, старший байт попросту отбрасывается. Значением, возвращаемым функцией fputc (), является значение записанного символа. При возникновении ошибки возвращается значение EOF. Если файл от- крыт для выполнения операций в двоичном режиме, значение EOF тоже может оказаться символом. Поэтому, чтобы определить, возникла ли ошибка на самом деле, в таких случаях придется использовать функцию ferror (). 292 Часть III. Стандартная библиотека С
Пример Приведенная функция записывает в заданный поток содержимое строки. Ivoid write_string(char *str, FILE *fp) { while(*str) if(!ferror(fp)) fputc(*str++, fp); } Зависимые функции fgetc(), fopen(), fprintf(), fread() и fwrite() В Функция fputs |#include <stdio.h> int fputs(const char ★str, FILE ★stream}; Функция fputs () записывает в заданный поток stream содержимое строки, адре- суемой указателем str. При этом завершающий нулевой символ (т.е. символ конца строки ( ’0’ )) не записывается. В версии С99 к параметрам str к stream применен квалификатор restrict. При успешном выполнении функция fputs () возвращает неотрицательное значе- ние, а при неудачном — значение EOF. Если поток открыт в текстовом режиме, могут произойти преобразования некото- рых символов. Это значит, что однозначного отображения строки в файл может и не быть. Однако если поток открыт в двоичном режиме, никаких преобразований симво- лов не будет и строка отобразится в файл “один к одному”. Пример Приведенный фрагмент программы записывает в поток, связанный с файлом fp, строку это тест. | fputs (’’это тест’’, fp) ; Зависимые функции fgets (), gets(), puts(), fprintf() и fscanf() В Функция fread |#include <stdio.h> size_t fread(void ★buf, size_t size, size_t count, FILE ★stream}; Функция fread () читает из потока, адресуемого указателем stream, count объектов длиной size байт и размещает их в массиве buf. Затем указатель текущей позиции фай- ла увеличивается на число, равное прочитанному количеству символов. В версии С99 к параметрам buf и stream применен квалификатор restrict. Функция fread () возвращает число реально прочитанных элементов. Если оказа- лось, что прочитано меньше элементов, чем требовалось при вызове, значит, либо произошла ошибка при выполнении операции, либо был достигнут конец файла. Оп- ределить, что именно произошло, можно с помощью функции feof () или ferror (). Глава 13. Функции ввода/вывода 293
Если поток открывается для операций в текстовом режиме, могут выполняться преоб- разования некоторых последовательностей символов, например, комбинация кодов возвра- та каретки (ASCII 13) и конца строки (ASCII 10) преобразуется в разделитель строк. Пример Следующая программа записывает в дисковый файл с названием TEST пять чисел с плавающей запятой, взяв их из массива bal. Затем она читает их из файла и записы- вает обратно в массив. #include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; float bal[5] = { 1.1F, 2.2F, 3.3F, 4.4F, 5.5F }; int i; /* запись значений */ if((fp=fopen("test", "wb"))==NULL) { printf("He удается открыть файл.Хп"); exit(1); } if(fwrite(bal, sizeof(float), 5, fp) !=5) printf("Ошибка при записи файла."); fclose(fp); /* чтение значений */ if((fp=fopen("test", "rb"))==NULL) { printf("He удается открыть файл.Хп"); exit(1); } if(fread(bal, sizeof(float), 5, fp) != 5) { if(feof(fp)) printf("Преждевременное достижение конца файла."); else printf("Ошибка при чтении файла."); } fclose(fp); for(i=0; i<5; i++) printf("%f", bal[i]); return 0; } Зависимые функции fwriteO, fopen(), f scanf (), fgetc () Hgetc() В Функция freopen |#include <stdio.h> FILE *freopen(const char ★fname, const char ★mode, FILE ★stream); 294 Часть III. Стандартная библиотека С
Функция f reopen () связывает существующий поток с другим файлом. Имя но- вого файла задается параметром frame, режим доступа — параметром mode, а перена- значаемый поток определяется указателем stream. Возможные значения строки mode — те же, что и для функции fopen() (полное их описание можно найти в разделе, по- священном описанию fopen). В версии С99 к параметрам frame, mode и stream применен квалификатор restrict. При вызове функция freopenO сначала пытается закрыть файл, который в дан- ный момент связан с потоком stream. Но даже если этот файл закрыть не удается, f reopen () открывает другой файл. При успешном выполнении функция freopenO возвращает указатель на поток, а в противном случае — нулевой указатель. Чаще всего функция freopenO используется для перенаправления таких опреде- ленных системой файлов, как stdin, stdout и stderr, в какой-то другой. Пример Приведенная здесь программа использует функцию freopenO, чтобы перенаправить поток stdout в файл с названием OUT. Первое сообщение программы выводится на эк- ран, а второе, перенаправленное, записывается функцией f reopen (), в файл на диске. #include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; printf("Это сообщение появится на дисплее.n"); if((fp=freopen("OUT", "w", stdout))==NULL) { printf("He удается открыть файл.п"); exit(1); } printf("Это сообщение будет записано в файл OUT.n"); fclose(fp); return 0; } Зависимые функции fopen() и fclose () И Функция fscanf |#include <stdio.h> int fscanf(FILE ★stream, const char ★format,...); Функция fscanf () работает подобно функции scanf (), но читает информацию не из стандартного потока ввода stdin, а из потока, заданного указателем stream. Подробности рассматриваются в разделе этой главы, посвященном функции scanf. В версии С99 к параметрам stream и format применен квалификатор restrict. Глава 13. Функции ввода/вывода 295
Функция fscanf () возвращает количество аргументов, которым действительно присвоены значения. Это число не включает опущенные поля. Если возвращаемое функцией значение равно EOF, то это свидетельствует о том, что до выполнения пер- вого присваивания произошел сбой. Пример Приведенный фрагмент программы читает из потока fp строку и значение пере- менной f с плавающей точкой (типа float). Ichar str[80]; float f; fscanf(fp, ”%s%f”, str, &f) ; Зависимые функции scanf() и fprintf() H Функция fseek |#include <stdio.h> int fseek(FILE ★stream, long int offset, int origin}; Функция fseek () устанавливает указатель текущей позиции файла, связанного с потоком stream, в соответствии со значениями начала отсчета origin и смещения offset. Назначение этой функции — поддерживать операции ввода/вывода с произвольным доступом. Параметр offset равен количеству байтов, на которые будет смещен внут- ренний указатель файла относительно начала отсчета, заданного параметром origin. В качестве значения для параметра origin должен быть взят один из следующих макросов (определенных в заголовке <stdio.h>). Имя_________________________________Назначение_________________________ SEEK_SET Поиск с начала файла SEEK.CUR Поиск с текущей позиции SEEK END Поиск с конца файла Нулевое значение возврата свидетельствует об успешном выполнении функции fseek (), а ненулевое — о возникновении сбоя. Вообще говоря, функцию fseek () следует использовать только при работе с дво- ичными файлами. При использовании же с текстовым файлом параметр origin должен иметь значение seek_set, а параметр offset — значение, полученное в результате вы- зова функции ftell () для того же файла, или нуль (чтобы установить указатель те- кущей позиции файла на начало). Функция fseek () очищает признак конца файла, связанный с заданным потоком. Более того, она аннулирует любой символ, ранее возвращенный в тот же поток через вызов функции ungetc () (см. раздел ungetc). Пример Приведенная функция производит поиск заданной структуры, имеющей тип addr. Обратите внимание на использование оператора sizeof для получения размера структуры. 296 Часть III. Стандартная библиотека С
struct addr { char name<[40]; char street[40]; char city[40]; char state[3]; char zip[10]; } info; void find(long int client_num) { FILE *fp; if((fp=fopen("mail", ”rb’’)) == NULL) { printf("He удается открыть файл.Хп"); exit(1); } /* поиск подходящей структуры */ fseek(fp, client_num*sizeof(struct addr), SEEK_SET); /* считывание данных в память */ fread(&info, sizeof(struct addr), 1, fp); fclose(fp); Зависимые функции ftell(), rewind (), fopen (), fgetposO и fsetpos() Ш Функция fsetpos |#include <stdio.h> int fsetpos(FILE ★stream, const fpos_t ★position); Функция fsetpos () перемещает указатель текущей позиции файла в место, за- данное объектом, к которому отсылает указатель position. Это значение должно быть предварительно получено путем обращения к функции fgetpos (). После выполне- ния функции fsetpos () признак конца файла сбрасывается. Кроме того, аннулиру- ется любой предыдущий результат обращения к функции unget с (). При неудачном выполнении функции fsetpos () возвращается ненулевое значе- ние, а при успешном — нуль. Пример Данный фрагмент программы устанавливает указатель текущей позиции файла в новое положение, соответствующее значению переменной file_loc. | fsetpos(fp, &file_loc); Зависимые функции fgetposO, f seek () HftellO Глава 13. Функции ввода/вывода 297
В Функция ftell |#include <stdio.h> long int ftell(FILE ★stream); Функция ftell () возвращает текущее значение указателя позиции файла для за- данного потока. В случае двоичных потоков это значение равно количеству байтов, которые отделяют указатель от начала файла. Для текстовых потоков возвращаемое значение может не иметь определенной интерпретации за исключением случая, когда оно является аргументом функции fseek(). Все дело в возможных преобразованиях символов, когда, например, комбинация кодов возврата каретки (ASCII 13) и конца строки (ASCII 10) заменяются разделителем строк, что влияет на размер файла. При возникновении ошибки функция ftell () возвращает значение -1. Пример Данный фрагмент программы считывает текущее значение указателя позиции файла, связанного с потоком, который задается параметром f р. Ilong int i; if((i=ftell(fp)) == -IL) printf("Возникла ошибка при обработке файла.п") Зависимые функции fseek() и fgetpos() И функция fwrite |#include <stdio.h> size_t fwrite(const void ★buf, size_t size, size_t count, FILE *stream); Функция fwrite () записывает в поток, адресуемый указателем stream, count объектов длиной size байтов каждый из массива символов, адресуемого указателем buf Затем указа- тель текущей позиции файла перемещается вперед на записанное количество символов. В версии С99 к параметрам buf и stream применен квалификатор restrict. Функция fwrite () возвращает число реально записанных элементов, которое при успешном выполнении функции будет равно числу затребованных элементов. Если же элементов записано меньше, чем указано при вызове, произошла ошибка. Пример Данная программа записывает в файл TEST число с плавающей точкой (значение переменной f). Обратите внимание, что оператор sizeof используется и для опреде- ления количества байтов, занимаемых переменной с плавающей точкой, а также что- бы обеспечить переносимость. |#include <stdio.h> #include <stdlib.h> int main(void) { 298 Часть III. Стандартная библиотека С
FILE *fp; float f=12.23; if((fp=fopen("test", "wb"))==NULL) { printf("He удается открыть файл.п"); exit(1) ; } fwrite(&f, sizeof(float), 1, fp); fclose(fp); return 0; } Зависимые функции fread(), fscanf (), getc() HfgetcO В Функция getc |#include <stdio.h> int getc(FILE ★stream); Функция getc () возвращает из входного потока stream символ, следующий за ука- зателем текущей позиции, а затем увеличивает значение указателя текущей позиции. При чтении символа предполагается, что он имеет тип unsigned char, который по- том преобразуется в целый. При достижении конца файла функция getc () возвращает значение EOF. Но по- скольку значение EOF само является целым значением, при работе с двоичными фай- лами проверять условие достижения конца файла необходимо с помощью функции feof (). При обнаружении ошибки функция getc() также возвращает значение EOF. Поэтому для выявления ошибок при работе с двоичными файлами необходимо ис- пользовать функцию ferror(). Функции getc () и fgetc() идентичны, но в большинстве реализаций функция getc () определена как макрос. Пример Следующая программа читает содержимое текстового файла и выводит его на экран. #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { FILE *fp; char ch; if((fp=fopen(argv[l] , "r"))==NULL) { printf("He удается открыть файл.п"); exit(1); } while((ch=getc(fp))!=EOF) { Глава 13. Функции ввода/вывода 299
printf(”%c", ch); } fclose(fp); return 0; } Зависимые функции fputc(), fgetc(), putc() и fopen() В Функция getchar |#include <stdio.h> int getchar(void); Функция getcharO возвращает из стандартного потока stdin следующий символ. При чтении символа предполагается, что символ имеет тип unsigned char, который потом преобразуется в целый. При достижении конца файла, как и при обнаружении ошибки, функция getchar () возвращает значение EOF. Функция getchar () чаще всего реализована как макрос. Пример Данная программа считывает в массив s символы из стандартного входного потока stdin, пока пользователь не нажмет клавишу ENTER. Затем введенная строка выво- дится на экран. #include <stdio.h> int main(void) { char s [256], *p; p = s; while ( (*p++ = getcharO)!- 'n'); *p = ’'; /* добавляем символ конца строки */ printf(s); return 0; } Зависимые функции fputc (), fgetc(), putc() и fopen () |НЯМКС| В Функция gets i#include <stdio.h> char *gets(char ★str) ; 300 Часть III. Стандартная библиотека С
Функция gets () читает символы из стандартного потока stdin и помещает их в массив символов, адресуемый указателем str. Символы читаются до тех пор, пока не встретится разделитель строк или значение EOF. Вместо разделителя строк в конец строки вставляется нулевой символ, свидетельствующий о ее завершении. При успешном выполнении функция gets() возвращает указатель str, а при сбое — нулевой указатель. Если произошла ошибка, содержимое массива, адресуемого параметром str, не определено. Поскольку функция gets () возвращает нулевой указа- тель и при возникновении ошибки, и при достижении конца файла, то для выясне- ния, что же произошло на самом деле, необходимо использовать функцию feof() или ferror (). Следует учесть, что нет способа ограничить число символов, которое прочитает функ- ция gets (). Это означает, что массив, адресуемый указателем str, может переполниться. Следовательно, данная функция опасна по своей природе. Ее следует использовать только в пробных программах или утилитах “внутреннего” назначения, т.е. для себя. В коммерче- ских программах эту функцию использовать не рекомендуется. Пример В данной программе функция gets () используется для чтения названия файла. ttinclude <stdio.h> ttinclude <stdlib.h> int main(void) { FILE *fp; char fname [128]; printf("Введите имя файла: ") ; gets(fname); if((fp=fopen(fname, "r"))==NULL) { printf("He удается открыть файл.п"); exit(1); } fclose(fp); return 0; } Зависимые функции fputs(), fgetc(), fgets () Hputs() В функция perror |#include <stdio.h> void perror(const char *str); Функция perror () преобразует значение глобальной переменной errno в строку и записывает эту строку в поток ошибок stderr. Если значение параметра str не рав- но нулю, то сначала записывается сама строка, за ней ставится двоеточие, а затем сле- дует сообщение об ошибке, определяемое конкретной реализацией. Глава 13. Функции ввода/вывода 301
Пример Этот фрагмент выдает сообщение о любой ошибке ввода/вывода, которая может произойти в потоке, связанном с файлом fp. | if(ferror(fp)) реггог(" Ошибка при работе с файлом "); Я Функция printf |#include <stdio.h> int printf(const char ★format, Функция printf () записывает в стандартный поток stdout значения аргументов из заданного списка аргументов в соответствии со строкой форматирования, адресуе- мой параметром format. В версии С99 к параметру format применен квалификатор restrict. Строка форматирования состоит из элементов двух типов. К элементам первого типа относятся символы, которые выводятся на экран. Элементы второго типа содержат специ- фикации формата, определяющие способ отображения аргументов. Спецификация форма- та начинается символом процента, за которым следует код формата. Количество аргумен- тов должно в точности совпадать с количеством спецификаций формата, причем соответ- ствие устанавливается в порядке их следования. Например, при вызове следующей функции printf () на экране будет отображено “Hi с 10 there!”. | printf ("Hi %с %d %s", ’с’, 10, "there!"); Если заданных аргументов меньше, чем спецификаций формата, результат не оп- ределен. Если аргументов больше, чем спецификаций формата, оставшиеся аргументы отбрасываются. Спецификаторы формата перечислены в таблице 13.2. Функция printf () возвращает число реально выведенных символов. Если функция возвратит отрицательное значение, то это будет свидетельствовать о наличии ошибки. На спецификации формата могут воздействовать модификаторы, задающие шири- ну поля, точность и признак выравнивания по левому краю. Целое значение, распо- ложенное между знаком % и командой форматирования, играет роль спецификации минимальной ширины поля. Наличие этого спецификатора приводит к тому, что резуль- тат будет заполнен пробелами или нулями, чтобы выводимое значение занимало поле, ширина которого не меньше заданной минимальной ширины. Если длина выводи- мого значения (строки или числа) больше этого минимума, оно будет выведено пол- ностью несмотря на превышение минимума. По умолчанию в качестве заполнителя используется пробел. Для заполнения нулями перед спецификацией ширины поля нужно поместить 0. Например, спецификация формата %05d дополнит нулями выво- димое число, в котором менее пяти цифр, чтобы общая длина равнялась 5 символам. Действие модификатора точности зависит от кода формата, к которому он приме- няется. Чтобы добавить модификатор точности, поставьте за спецификацией ширины поля десятичную точку, а после нее — требуемое значение точности. Для форматов а, А, е, Е, f и F модификатор точности определяет число выводимых десятичных зна- ков. Например, спецификация формата %10.4f обеспечит вывод числа с четырьмя знаками после запятой в поле шириной не меньше десяти символов. Если модифика- тор точности применяется к коду формата g или G, то он определяет максимальное число выводимых значащих цифр. Применительно к целым, модификатор точности задает минимальное количество выводимых цифр. При необходимости перед числом будут добавлены нули. Если модификатор точности применяется к строкам, число, следующее за точкой, задает максимальную длину поля. Например, спецификация формата %5.7s выведет 302 Часть III. Стандартная библиотека С
строку длиной не менее пяти, но не более семи символов. Если выводимая строка окажется длиннее максимальной длины поля, конечные символы будут отсечены. По умолчанию все выводимые значения выравниваются по правому краю: если ширина поля больше выводимого значения, оно будет выровнено по правому краю поля. Чтобы установить выравнивание по левому краю, нужно поставить знак “минус” сразу после знака %. Например, спецификация формата %-10.2f обеспечит выравнивание вещественного числа с двумя десятичными знаками в 10-символьном поле по левому краю. Существуют два модификатора формата, позволяющие функции printf () ото- бражать короткие и длинные целые. Эти модификаторы могут применяться к специ- фикаторам типа d, i, о, и, х и X. Модификатор 1 уведомляет функцию printf () о длинном типе значения. Например, спецификация % Id означает, что выводится длинное целое число. Модификатор h сообщает функции printf (), что нужно вы- вести число короткого целого типа. Следовательно, строка %hu означает, что выводи- мое данное имеет тип short unsigned int. Таблица 13.2. Спецификаторы формата функции printf() Код Формат___________________________________________________________________________ %а Выводит шестнадцатеричное число в форме Gxh.hhhhp+d (только С99) %А Выводит шестнадцатеричное число в форме GXh.hhhhP+d (только С99) %с Символ %d Десятичное целое число со знаком %i Десятичное целое число со знаком %е Экспоненциальное представление числа (в виде мантиссы и порядка) (е на нижнем регистре) %Е Экспоненциальное представление числа (в виде мантиссы и порядка) (Е на верхнем регистре) %f Десятичное число с плавающей точкой %F Десятичное число с плавающей точкой (только С99; если применяется к бесконечности или к нечисловому значению, то выдает надписи INF, INFINITY или NAN на верхнем реги- стре. Спецификатор %f выводит их эквиваленты на нижнем регистре.) %д Использует более короткий из форматов %е или %f %G Использует более короткий из форматов %е или %f %о Восьмеричное число без знака %з Символьная строка %и Десятичное целое число без знака %х Шестнадцатеричное без знака (строчные буквы) %Х Шестнадцатеричное без знака (прописные буквы) %р Выводит указатель %п Соответствующий аргумент должен быть указателем на целое число. (Этот спецификатор указывает, что в целочисленной переменной, на которую указывает ассоциированный с данным спецификатором указатель, будет храниться число символов, выведенных к мо- менту обработки спецификации %п.) % % Выводит знак процента При использовании современного компилятора, поддерживающего добавленные в 1995 году средства работы с двухбайтовыми символами, можно к спецификации с применить модификатор 1, чтобы указать на использование двухбайтовых символов. Модификатор 1 можно также использовать с командой формата s для вывода строки двухбайтовых символов. Кроме того, модификатор 1 можно поставить перед командами форматирования вещественных чисел а, А, е, Е, f, F, g и G. В этом случае он уведомит о выводе значения типа long double. Глава 13. Функции ввода/вывода 303
Команда п сохраняет в целой переменной, указатель на которую задан в списке аргументов, число символов, которые были записаны в поток вывода к моменту обна- ружения спецификатора п. Например, следующий фрагмент программы после строки “Это тест” выведет число 8. Iint i; printf("Это тест%п", &i); printf("%d", i); Чтобы обозначить, что соответствующий аргумент указывает на длинное целое, к спецификации п можно применить модификатор 1. Для указания на короткое целое примените к спецификации п модификатор h. Символ # при использовании с некоторыми кодами формата функции printf () приобретает специальное значение. Поставленный перед кодами a, A, g, G, f, е и Е, он гарантирует наличие десятичной точки даже в случае отсутствия десятичных цифр. Если поставить символ # перед кодами формата х и х, то шестнадцатеричное число будет выведено с префиксом Ох. Если же его поставить перед кодами формата о и о, то восьмеричное число будет выведено с префиксом 0. Символ # нельзя приме- нять ни к каким другим спецификациям формата. Спецификации минимальной ширины поля и точности могут задаваться не кон- стантами, а аргументами функции printf (). Для этого в строке форматирования ис- пользуется символ “звездочка” (*). При сканировании строки форматирования функ- ции printf () каждый символ * будет сопоставляться с соответствующими аргумен- тами в порядке их следования. Модификаторы формата функции printf(), добавленные стан- дартом C99 В версии С99 для использования в функции printf () добавлены модификаторы формата hh, 11, j, z и t. Модификатор hh можно применять к спецификаторам преобразования d, i, о, и, х, хи п. Он означает, что соответствующий аргумент яв- ляется значением типа signed char или unsigned char, а в случае спецификации п — указателем на переменную типа signed char. Модификатор 11 также можно применять к спецификаторам преобразования d, i, о, u, х, х и п. Он означает, что соответствующий аргумент является значением типа signed long long int или unsigned long long int, а в случае спецификатора n — указателем на переменную типа long long int. Версия С99 также позволяет применять модификатор 1 к спе- цификаторам преобразования чисел с плавающей точкой а, А, е, Е, f, F, g, и G, но это не дает никакого эффекта. Применение модификатора формата j. к спецификаторам преобразования d, i, о, и, х, х и п устанавливает для соответствующего аргумента тип intmax_t или uintmax_t. Эти типы объявлены в заголовке <stdint.h> и служат для хранения це- лых самой большой разрядности. Применение к спецификаторам преобразования d, i, о, u, х, X и п модификато- ра формата z устанавливает для соответствующего аргумента тип size_t. Этот тип объявлен в заголовке <stddef . h> и служит для хранения результата выполнения опе- ратора sizeof. Применение к спецификаторам преобразования d, i, о, u, х, х и п модификато- ра формата t устанавливает для соответствующего аргумента тип ptrdiff_t. Этот тип объявлен в заголовке <stddef . h> и служит для хранения значения разности между двумя указателями. 304 Часть III. Стандартная библиотека С
Пример Данная программа выводит то, что указано в комментариях. #include <stdio.h> int main(void) { /* Этот фрагмент печатает строку "это тест", которая выравнивается по левому краю поля шириной в 20 символов. */ printf("%-20s", "это тест"); /* Этот фрагмент печатает в поле шириной в 10 символов число с плавающей точкой с тремя десятичными разрядами после запятой. В результате получится " 12.235". */ printf("%10.3f", 12.234657); return 0; } Зависимые функции scanf() и fprintf() В Функция putc |#include <stdio.h> int putc(int ch, FILE ★stream); Функция putc () записывает в поток вывода, адресуемый параметром stream, сим- вол, содержащийся в младшем байте параметра ch. Поскольку в момент вызова сим- вольные аргументы преобразуются в целые, их вполне можно использовать в качестве аргументов функции putc (). Функция putc () часто реализуется как макрос. При успешном выполнении функция putc () возвращает записанный символ, а в случае ошибки — значение EOF. Если поток вывода был открыт в двоичном режиме, EOF тоже может быть воспринято как ch. Поэтому в данном случае для выявления ошибки необходимо использовать функцию f error (). Пример Следующий цикл записывает символы в строку str потока, заданного идентифи- катором fp. Символ конца строки не записывается. | for(; *str; str++) putc(*str, fp); Зависимые функции f getc (), fputc(), getcharO И putchar () Ш Функция putchar |#include <stdio.h> int putchar(int ch); Глава 13. Функции ввода/вывода 305
Функция putchar () записывает символ, содержащийся в младшем байте парамет- ра ch, в стандартный поток вывода stdout. По выполняемому действию она эквива- лентна putc(ch, stdout). Поскольку в момент вызова символьные аргументы пре- образуются к целому типу, их вполне можно использовать в качестве аргументов функции putchar (). При успешном выполнении функция putchar () возвращает записанный символ, а в случае ошибки — значение EOF. Пример Следующий цикл записывает символы в строку str стандартного потока вывода stdout. Символ конца строки не записывается. | for(; *str; str++) putchar(*str); Зависимые функции putc() И Функция puts Ittinclude <stdio.h> int puts(const char ★str); Функция puts () записывает строку, адресуемую параметром str, в стандартное вы- ходное устройство. Символ конца строки преобразуется в разделитель строк. При успешном выполнении функция puts () возвращает неотрицательное значе- ние, а в случае сбоя — значение EOF. Пример Следующая программа записывает в стандартный поток вывода stdout строку это пример. #include <stdio.h> #include <string.h> int main(void) { char str[80] ; strcpy(str, "это пример"); puts(str) ; return 0; } Зависимые функции putc(), gets () и printf() 306 Часть III. Стандартная библиотека С
S Функция remove |#include <stdio.h> int remove(const char *fname); Функция remove () удаляет файл, заданный параметром fname. При успешном удалении файла функция возвращает нуль, а в случае ошибки — ненулевое значение. Пример Данная программа удаляет файл, имя которого задается в командной строке. tfinclude <stdio.h> int main(int argc, char *argv[]) { if(remove(argv[1])) printf("Ошибка при удалении"); return 0; } Зависимые функции rename () И Функция rename Iftinclude <stdio.h> int rename (const char ★oldfname, const char ★newfname) ; Функция rename () переименовывает файл; она заменяет имя файла, заданное пара- метром oldfname, именем, заданным параметром newfname. Имя, заданное параметром newfname, не должно совпадать ни с одним из существующих в каталоге имен файлов. При успешном выполнении функция rename () возвращает нуль, а в случае ошиб- ки — ненулевое значение. Пример Данная программа заменяет имя файла, заданное первым (нумерация аргументов начинается с нуля! — Прим, ред.) аргументом командной строки, именем, которое за- дается вторым аргументом командной строки. Учитывая, что программа называется CHANGE, командная строка CHANGE THIS THAT приведет к переименованию файла THIS в файл THAT. tfinclude <stdio.h> int main(int argc, char *argv[]) { if(rename (argv[1], argv[2]) !=0) printf("Ошибка при переименовании"); return 0; } Глава 13. Функции ввода/вывода 307
Зависимые функции remove() И Функция rewind |#include <stdio.h> void rewind(FILE ★stream); Функция rewind () перемещает указатель текущей позиции файла в начало за- данного потока. Она также очищает связанные с потоком stream признаки конца файла и ошибок. Пример Данная функция дважды читает поток, адресованный указателем fp, и каждый раз выводит файл на экран. void re_read(FILE *fp) { /* первое чтение */ while(!feof(fp)) putchar(getc(fp)); rewind(fp); /* второе чтение */ while(’feof(fp)) putchar(getc(fp)); } Зависимые функции fseek() В Функция scanf |#include <stdio.h> int scanf(const char ★format, ...); Функция scanf () представляет собой процедуру ввода общего назначения, кото- рая читает поток stdin и сохраняет информацию в переменных, перечисленных в списке аргументов. Она может читать все встроенные типы данных и автоматически преобразовывать их в соответствующий внутренний формат. В версии С99 к параметру format применен квалификатор restrict. Управляющая строка, задаваемая параметром format, состоит из символов трех категорий: спецификаторов формата; пробельных символов; символов, отличных от пробельных. Спецификации формата, начинаются знаком % и сообщают функции scanf () тип данного, которое будет прочитано. Спецификации формата приведены в таблице 13.3. Например, по спецификации %s будет прочитана строка, а по спецификации %d — 308 Часть III. Стандартная библиотека С
целое значение. Строка форматирования читается слева направо, и спецификации формата сопоставляются аргументам в порядке их перечисления в списке аргументов. Таблица 13.3. Спецификации формата функции scanf() Код Назначение %а %А %с %d %i %е %Е %f %F %g %G %o %s %x %X %p %n %u %[] %% Читает значение с плавающей точкой (только С99) Аналогично коду % а (только С99) Читает один символ Читает десятичное целое Читает целое в любом формате (десятичное, восьмеричное или шестнадцатеричное) Читает число с плавающей точкой Аналогично коду %е Читает число с плавающей точкой Аналогично коду %f (только С99) Читает число с плавающей точкой Аналогично коду %д Читает восьмеричное число Читает строку Читает шестнадцатеричное число Аналогично коду %х Читает указатель Принимает целое значение, равное количеству прочитанных до сих пор символов Читает десятичное целое без знака Просматривает набор символов Читает знак процента По умолчанию спецификации a, f, е и g заставляют функцию scanf () присваи- вать данные переменным типа float. Если перед одной из этих спецификаций по- ставить модификатор 1, функция scanf () присвоит прочитанные данные переменной типа double. Использование же модификатора L означает, что полученное значение присвоится переменной типа long double. Современные компиляторы, поддерживающие добавленные в 1995 году средства работы с двухбайтовыми символами, позволяют к спецификации с применить моди- фикатор 1; тогда будет считаться, что соответствующий указатель указывает на двух- байтовый символ (т.е. на данное типа whcar_t). Модификатор 1 также можно ис- пользовать с кодом формата s; тогда будет считаться, что соответствующий указатель указывает на строку двухбайтовых символов. Кроме того, модификатор 1 можно ис- пользовать для того, чтобы указать, что набор сканируемых символов состоит из двух- байтовых символов. Если в строке форматирования встретится разделитель, то функция scanf () пропустит один или несколько разделителей во входном потоке. Под разделителем, или пробельным символом, подразумевается пробел, символ табуляции или разделитель строк (символ но- вой строки). По сути, наличие одного разделителя в управляющей строке приведет к тому, что функция scanf () будет читать, не сохраняя, любое количество (возможно, даже нуле- вое) разделителей до первого символа, отличного от разделителя. Если в строке форматирования встретился символ, отличный от разделителя, то функция scanf () прочитает и отбросит его. Например, если в строке форматирова- ния встретится %d, %d, то функция scanf () сначала прочитает целое значение, затем прочитает и отбросит запятую и, наконец, прочитает еще одно целое. Если заданный символ не найден, функция scanf () завершает работу. Глава 13. Функции ввода/вывода 309
Все переменные, получающие значения с помощью функции scanf (), должны передаваться посредством своих адресов. Это значит, что все аргументы должны быть указателями на переменные. Элементы входного потока должны быть разделены пробелами, символами табуля- ции или разделителями строк. Такие символы, как запятая, точка с запятой и т.п., не распознаются в качестве разделителей. Это означает, что оператор | scanf("%d%d", &r, &с); примет значения, введенные как 10 2 0, но откажется от последовательности сим- волов 10,20. Символ *, стоящий после знака % и перед кодом формата, прочитает данные за- данного типа, но запретит их присваивание. Следовательно, оператор | scanf("%d%*c%d", &х, &у); при вводе данных в виде 10/20 поместит значение 10 в переменную х, отбросит знак деления и присвоит значение 20 переменной у. Команды форматирования могут содержать модификатор максимальной длины по- ля. Он представляет собой целое число, располагаемое между знаком % и кодом фор- мата, которое ограничивает количество читаемых для всех полей символов. Например, если в переменную address нужно прочитать не более 20 символов, используется следующий оператор. | scanf("%20s”, address); Если входной поток содержит более 20 символов, то при последующем обращении к операции ввода чтение начнется с того места, в котором “остановился” предыдущий вызов функции scanf (). Если разделитель встретится раньше, чем достигнута макси- мальная длина поля, ввод данных завершится. В этом случае функция scanf () пере- ходит к чтению следующего поля. Хотя пробелы, символы табуляции и разделители строк используются в качестве разде- лителей полей, при чтении одиночного символа они читаются подобно любому другому символу. Например, если входной поток состоит из символов х у, то оператор | scanf("%с%с%с”, &а, &Ь, &с); поместит символ х в переменную а, пробел — в переменную Ь, а символ у — в переменную с. Помните, что любые символы управляющей строки (включая пробелы, символы табуляции и новой строки), не являющиеся спецификациями формата, используются для установки соответствия и отбрасывания символов из входного потока. Любой со- ответствующий им символ отбрасывается. Например, если поток ввода выглядит, как 10t20, оператор | scanf(”%dt%d", &х, &у); присвоит переменной х значение 10, а переменной у — значение 20. Символ t отбра- сывается, так как он присутствует в управляющей строке. Функция scanf () поддерживает спецификатор формата общего назначения, называе- мый набором сканируемых символов (scanset). В этом случае определяется набор символов, которые могут быть прочитаны функцией scanf () и присвоены соответствующему масси- ву символов. Для определения такого набора символы, подлежащие сканированию, необ- ходимо заключить в квадратные скобки. Открывающая квадратная скобка должна следо- вать сразу за знаком процента. Например, следующий набор сканируемых символов ука- зывает на то, что необходимо читать только символы А, вис. | %[АВС] 310 Часть III. Стандартная библиотека С
При использовании набора сканируемых символов функция scanf () продолжает читать символы и помещать их в соответствующий массив символов до тех пор, пока не встретится символ, отсутствующий в заданном наборе. Соответствующая набору переменная должна быть указателем на массив символов. При возврате из функции scanf () этот массив будет содержать строку из прочитанных символов, завершаю- щуюся символом конца строки. Если первый символ в наборе является знаком Л, то получаем обратный эффект: входное поле читается до тех пор, пока не встретится символ из заданного набора сканируемых символов, т.е. знак Л заставляет функцию scanf () читать только те символы, которые отсутствуют в наборе сканируемых символов. Во многих реализациях допускается задавать диапазон с помощью дефиса. Напри- мер, функция scanf (), встречая набор сканируемых символов в виде % [A—Z], будет читать символы, попадающие в диапазон от А до Z. Важно помнить, что в наборе сканируемых символов различаются прописные и строчные буквы. Следовательно, чтобы сканировать как прописные, так и строчные буквы, в наборе сканируемых символов придется задать их отдельно. Функция scanf () возвращает число, равное количеству полей, для которых ус- пешно присвоены значения. К этим полям не относятся поля, которые были прочи- таны, но присвоение не состоялось в связи с использованием модификатора *, подав- ляющего присваивание. При обнаружении ошибки до присвоения значения первого поля функция scanf () возвращает значение EOF. Модификаторы формата, добавленные к функции scant() Стандартом С99 В версии С99 для использования в функции scanf () добавлены модификаторы формата hh, 11, j, z и t. Модификатор hh можно применять к спецификациям d, i, о, и, х и п. Он означает, что соответствующий аргумент является указателем на значение типа signed char или unsigned char. Модификатор 11 также можно применять к спецификациям d, i, о, и, х и п. Он означает, что соответствующий аргумент является указателем на значение типа signed long long int или unsigned long long int. Модификатор формата j, который применяется к спецификациям d, i, о, и, х и п, означает, что соответствующий аргумент является указателем на значение типа intmax_t или uintmax_t. Эти типы объявлены в заголовке <stdint.h> и служат для хранения целых максимально возможной разрядности. Модификатор формата z, который применяется к спецификациям d, i, о, и, х и п, означает, что соответствующий аргумент является указателем на объект типа size_t. Этот тип объявлен в заголовке <stddef . h> и служит для хранения результата операции sizeof. Модификатор формата t, который применяется к спецификациям d, i, о, и, х и п, означает, что соответствующий аргумент является указателем на объект типа ptrdiff_t. Этот тип объявлен в заголовке <stddef . h> и служит для хранения значе- ния разности между двумя указателями. Пример Действие данных операторов scanf () объясняется в комментариях. |#include <stdio.h> ‘ int main (void) ( Глава 13. Функции ввода/вывода 311
char str[80], str2[80]; int i; /* читается строка и целое значение */ scanf(”%s%d”, str, &i); /* в переменную str считывается не более 79 символов */ scanf(”%79s”, str); /* целое, расположенное между двумя строками, пропускается */ scanf(”%s%*d%s”, str, str2); return 0; } Зависимые функции printf() и fscanf() H Функция setbuf |#include <stdio.h> void setbuf(FILE ★stream, char ★buf); Функция setbuf () задает буфер, которым будет пользоваться поток stream, либо отключает буферизацию, если параметр buf установлен равным нулю. Если необходи- мо задать буфер, определенный программистом, его длину следует установить равной BUFSIZ символам. Идентификатор BUFSIZ определяется в заголовке <stdio.h>. В версии С99 к параметрам stream и buf применен квалификатор restrict. Пример Следующий фрагмент связывает буфер, определенный программистом, с потоком, адресуемым указателем fp. Ichar buffer[BUFSIZ]; setbuf(fp, buffer); Зависимые функции fopen (), fclose () и setvbuf() H Функция setvbuf |#include <stdio.h> int setvbuf(FILE ★stream, char ★buf, int mode, size_t size); Функция setvbuf () позволяет программисту задать буфер, его размер и ре- жим работы с указанным потоком. Массив символов, адресуемый параметром buf используется в качестве буфера потока для операций ввода/вывода. Размер буфера устанавливается с помощью параметра size, а режим mode определяет, как будет 312 Часть III. Стандартная библиотека С
выполняться буферизация. Если параметр buf равен нулю, функция setvbuf() выделяет собственный буфер. В версии С99 к параметрам stream и buf применен квалификатор restrict. Возможными значениями параметра mode являются _iofbf, _ionbf и _iolbf, ко- торые определены в заголовочном файле <stdio.h>. Если параметр mode равен IOFBF, для буферизации используется полный объем буфера. Если mode равен IOLBF, поток будет буферизирован построчно, т.е. содержимое буфера будет дозапи- сываться в поток при каждой записи в поток вывода символа новой строки. Содер- жимое буфера также дозаписывается в поток при заполнении буфера. При чтении из входного потока появление разделителя строк приведет к прекращению подкачки в буфер. Если установлен режим _IONBF, поток не буферизируется. Функция setvbuf () возвращает нуль при успешном выполнении, а в противном случае ненулевое значение. Пример Данный фрагмент программы устанавливает построчный режим вывода потока fp в буфер размером 128 символов. ^include <stdio-h> char buffer[128]; setvbuf(fp/ buffer/ _IOLBF/ 128); Зависимые функции setbuf() Функция snprintf |#include <stdio.h> int snprintf(char * restrict buf, size_t num, const char * restrict format, . . . ) ; Функция snprintf () добавлена в версии C99. Она идентична функции sprintf () за исключением того, что в массиве, адресуемом указателем buf будет сохранено максимум пит-1 символов. По окончании работы функ- ции этот массив будет завершаться символом конца строки (нуль-символом). Таким обра- зом, функция snprintf () позволяет предотвратить переполнение буфера buf. Зависимые функции printf(), sprintf() И fsprintf () В Функция sprintf |#include <stdio.h> int sprintf(char ★buf, const char ★format,...); Функция sprintf () идентична функции printf () за исключением того, что по- ток вывода записывается в массив, адресуемый указателем buf а не в стандартный по- Глава 13. Функции ввода/вывода 313
ток stdout. По окончании работы функции этот массив будет завершаться символом конца строки (нуль-символом). Подробности рассматриваются в разделе, посвящен- ном описанию функции printf. В версии С99 к параметрам buf и format применен квалификатор restrict. Возвращаемое значение равно числу символов, действительно помещенных в массив. Важно понимать, что функция sprintf () не обеспечивает никакой проверки пе- реполнения массива, адресуемого указателем buf Это значит, что массив будет пере- полнен, если объем выводимых символов превысит длину массива. В качестве альтер- нативного решения рассмотрите применение функции snprintf (). Пример После выполнения этого фрагмента программы элементам массива str значения будут присвоены таким образом, что получится строка один 2 3. I char str[80]; I sprintf(str, "%s %d %c", "один", 2, ’3’); Зависимые функции printf() и fsprintf() В Функция sscanf |#include <stdio.h> int sscanf(const char ★buf, const char ★format, ...); Функция sscanf () идентична функции scanf (), но данные читаются из массива, адресуемого параметром buf а не из стандартного потока ввода stdin. Подробности приводятся в разделе scanf. В версии С99 к параметрам buf и format применен квалификатор restrict. Значение, возвращаемое функцией, равно количеству переменных, которым ре- ально были присвоены значения. К ним не относятся поля, опущенные из-за исполь- зования модификатора команды форматирования *. Нулевое значение свидетельству- ет о том, что ни одно поле не было присвоено, а значение EOF сигнализирует об ошибке, обнаруженной до первого присваивания. Пример Данная программа выводит на экран сообщение привет 1. #include <stdio.h> int main(void) { char str[80]; int i ; sscanf ("привет 1^ 2 3 4 5", "%s%d", str, &i) ; printf ("%s %d", .str1, i) ; return 0; } 314 Часть III. Стандартная библиотека С
Зависимые функции scanf() и fscanf() И Функция tmpfile Ittinclude <stdio.h> FILE *tmpfile(void); Функция tmpfile () открывает временный двоичный файл для операций чтения- записи и возвращает указатель на связанный с ним поток. Она автоматически исполь- зует уникальное имя файла, чтобы избежать конфликтов с существующими файлами. Функция tmpfile () при неудачном выполнении возвращает нулевой указатель, а при успешном — указатель на поток. Временный файл, созданный функцией tmpfile (), автоматически удаляется при закрытии файла или по завершении программы. Количество временных файлов, которые можно открыть, равно значению тмр мах (которое не превышает предел, определяемый значением FOPEN_MAX). Пример Следующий фрагмент создает временный файл. FILE *temp; if((temp=tmpfile())==NULL) { printf("He удается открыть временный файл.п"); exit (1); } Зависимые функции tmpnam() В Функция tmpnam |#include <stdio.h> char *tmpnam(char ★name); Функция tmpnam () генерирует уникальное имя файла и сохраняет его в массиве, адре- суемом указателем пате. Длина этого массива должна составлять не меньше L_tmpnam символов. (Константа L_tmpnam определена в заголовочном файле <stdio.h>.) Основное назначение функции tmpnam () — сгенерировать имя временного файла, которое не сов- падало бы ни с одним из имен файлов в текущем каталоге диска. Эту функцию можно вызвать не более тмр_мах раз. Константа тмр_мах определе- на в заголовочном файле <stdio.h>, и ее значение больше либо равно 25. При каж- дом вызове функция tmpnam () будет генерировать новое имя временного файла. При успешном выполнении функция возвращает указатель на массив пате, в про- тивном случае — нулевой указатель. Если значение параметра пате равно нулю, имя временного файла сохраняется в статическом массиве, принадлежащем функции tmpnam (), которая в этом случае возвращает указатель на этот массив. При после- дующем вызове функции tmpnam () этот массив будет перезаписан. Глава 13. Функции ввода/вывода 315
Пример В данной программе генерируются и выводятся на экран три уникальных имени временных файлов. ^include <stdio.h> int main(void) { char name[40]; int i ; for(i=0; i<3; i++) { tmpnam(name); printf("%s ", name); } return 0; } Зависимые функции tmpfile() Si Функция ungetc i#include <stdio.h> int ungetc(int ch, FILE ★stream); Функция ungetc () возвращает в поток ввода stream символ, заданный младшим байтом параметра ch. Затем этот символ будет получен при последующей операции чтения потока stream. Обращение к таким функциям, как fflushO, fseek() и rewind (), аннулирует действие ungetc () и сбрасывает этот символ. Гарантируется, что в поток можно возвратить один символ, однако некоторые реа- лизации допускают возврат большего числа символов. Попытка вернуть в поток ввода значение EOF игнорируется. Обращение к функции ungetc () очищает признак конца файла, связанный с за- данным потоком. Значение указателя текущей позиции файла для текстового потока не определено до тех пор, пока не будут прочитаны все возвращенные обратно в по- ток символы, в этом случае оно остается таким же, каким было до первого вызова функции ungetc (). При работе с потоками в двоичном режиме каждый вызов функ- ции ungetc () уменьшает указатель текущей позиции файла. При успешном завершении функция возвращает значение ch, в противном слу- чае — значение EOF. Пример Данная функция читает слова из входного потока, адресуемого указателем fp. Раз- делитель возвращается в поток для последующего использования. Например, если входные данные имеют вид count/10, то при первом обращении к функции read_word () она возвратит count, а символ направит обратно во входной поток. Ivoid read_word(FILE *fp, char *token) { while(isalpha(*token=getc(fp))) token++; ungetc(*token, fp); } 316 Часть III. Стандартная библиотека С
Зависимые функции getc() 3 Функции vprintf, vfprintf, vsprintf и vsnprintf #include <stdarg.h> #include <stdio.h> int vprintf(char ★format, va_list arg_ptr}; int vfprintf(FILE ★stream, const char ★format, va_list arg_ptr) ; int vsprintf(char ★buf, const char ★format, va_list arg_ptr}; int vsnprintf(char * restrict buf, size_t num, const char * restrict format, va_list arg_ptr) ; Действия функций vprintf (), vfprintf (), vsprintf () и vsnprintf () эквива- лентны действиям функций printf (), fprintfO, sprintf () и snprintf () соот- ветственно, но список аргументов заменяется указателем на список аргументов. Этот указатель должен иметь тип va_list, который определен в заголовке <stdarg.h>. В версии С99 к параметрам buf и format применен квалификатор restrict. Функ- ция vsnprintf () добавлена в версии С99. Пример Данный фрагмент программы иллюстрирует, как нужно вызывать функцию vprintf (). Вызов функции va_start () приводит к созданию указателя на список аргу- ментов переменной длины, причем этот указатель указывает на начало списка аргументов. Этот указатель должен быть использован при вызове функции vprintf (). Вызов функции va end () очищает указатель на список аргументов переменной длины. ttinclude <stdio.h> #include <stdarg.h> void print_message(char *format, ...); int main(void) { print_message(”He удается открыть файл %s.", "test"); return 0; } void print_message(char *format, ...) { va_list ptr; /* извлечение аргумента ptr */ /★ инициализация ptr, он становится указателем на первый аргумент, следующий за строкой форматирования */ va_start(ptr, format); /★ вывод сообщения */ vprintf(format, ptr); va_end(ptr); } Глава 13. Функции ввода/вывода 317
Зависимые функции vscanf(), vf scanf (), vsscanf(), va_arg(), va_start() и va_end() В Функции vscanf j vfscanf и vsscanf ttinclude <stdarg.h> ttinclude <stdio.h> int vscanf (char * restrict format, va_list arg__ptr) ; int vfscanf(FILE * restrict stream, const char * restrict format, va_list arg_ptr} ; int vsscanf(char * restrict buf, const char * restrict format, va_list arg_ptr) ; Эти функции добавлены в версии С99. Действия функций vscanf (), vfscanf () и vsscanf () эквивалентны действиям функций scanf (), fscanf () и sscanf () соответственно, но список аргументов за- менен указателем на этот список. Данный указатель должен иметь тип va_list, ко- торый определен в заголовке <stdarg.h>. Зависимые функции vprintfO, vfprintfO, vsprintfO, vsnprintfO, va_arg(), va_start() и va_end() 318 Часть III. Стандартная библиотека С
Полный с ] раво чн ик ] ( Глава 14 Строковые и символьные функции
Стандартная библиотека языка С обладает богатым и разнообразным набором функций для обработки строк и символов. Строковые функции работают с мас- сивами символов (строками), заканчивающимися символом конца строки. В языке С для работы со строковыми функциями используется заголовок <string.h>, для сим- вольных функций — заголовочный файл <ctype.h>. Поскольку в С не предусмотрен автоматический контроль нарушения границ мас- сивов, вся ответственность за их переполнение ложится на программиста. Не следует этим пренебрегать, так как при переполнении массива может произойти аварийное завершение программы. В С печатаемыми символами являются те, которые можно отобразить на терминале. В ASCII-средах они расположены между пробелом (0x20) и тильдой (OxFE). Управ- ляющие символы имеют значения, лежащие в диапазоне между нулем и 0x1 F; в ASCII- средах к ним также относится символ DEL (0x7F). Исторически сложилось так, что аргументами символьных функций являются це- лые значения, из которых используется только младший байт. Символьные функции автоматически преобразуют свои аргументы в тип unsigned char. Безусловно, эти функции можно вызывать с символьными аргументами, поскольку в момент вызова функции символы автоматически преобразуются к целому типу. В заголовке <string.h> определен тип size_t; это тип результата, который полу- чается после применения оператора1 sizeof и представляет собой разновидность це- лого без знака. В этой главе описаны только те функции, которые работают с символами типа char. Эти функции были определены стандартом С с самого начала, и, безуслов- но, они являются наиболее популярными и поддерживаются большинством ком- пиляторов. Двухбайтовые функции, работающие с символами типа wchar_t, опи- саны в главе 19. В версии С99 к некоторым параметрам нескольких функций, первоначально определенных в версии С89, добавлен квалификатор restrict. При рассмотре- нии каждой такой функции будет приведен ее прототип, используемый в среде С89 (а также в среде C++), а параметры с атрибутом restrict будут отмечены в описании этой функции. Ш Функция isalnum |#include <ctype.h> int isalnum(int ch) Если аргумент ch функции isalnum() является либо буквой, либо цифрой, она возвращает ненулевое значение. Если же тестируемый символ не относится к алфа- витно-цифровым, возвращается нуль. Пример Данная программа читает из стандартного входного потока stdin символы, прове- ряет их и выдает сообщение о каждом алфавитно-цифровом символе. |#include <ctype.h> #include <stdio.h> int main(void) { 1 В данном случае под оператором подразумевается знак функции. — Прим. ред. 320 Часть III. Стандартная библиотека С
char ch; for(;;) { ch getc(stdin); if(ch — •.•) break; if(isalnum(ch)) printf("Символ %c является алфавитно-цифровымп", ch) ; } return 0; } Зависимые функции isalphaO, iscntrlO, isdigitO, isgraphO, isprintO, ispunctO и isspace () Н Функция isalpha |#include <ctype.h> int isalpha(int ch); Функция isalpha () возвращает ненулевое значение, если ее аргумент ch является буквой, в противном случае возвращается нуль. Принадлежность символа к буквам зависит от конкретного языка. Для английского языка таковыми являются прописные и строчные буквы от А до Z. Пример Данная программа делает проверку каждого символа, прочитанного из стандарт- ного входного потока stdin, и выдает сообщение, если этот символ окажется буквой. #include <ctype.h> #include <stdio.h> int main(void) { char ch; f or (; ; ) { ch = getcharO; if(ch == '.') break; if(isalpha(ch)) printf("%c является буквойп", ch); } return 0; } Зависимые функции isalnum(), iscntrlO, isdigitO, isgraphO, isprintO, ispunctO и isspace () Глава 14. Строковые и символьные функции 321
В Функция isblank i#include <ctype.h> int isblank(int ch); Функция isblank () добавлена в версии C99. Она возвращает ненулевое значение, если ее аргумент ch является символом, для которого функция isspaceO возвращает значение “истина”. Этот символ использу- ется в качестве разделителя слов. Так, для английского языка пробельными символа- ми являются пробел и символ горизонтальной табуляции. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом пробельном символе. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if(ch == break; if(isblank(ch)) printf("%c является разделителем словп", ch); } return 0; } Зависимые функции isalnumO, isalpha(), iscntrl(), isdigit(), isgraph(), ispunct() и isspace() а Функция iscntrl i#include <ctype.h> int iscntrl(int ch); Функция iscntrl () возвращает ненулевое значение, если ее аргумент ch является управляющим символом, значение которого в ASCII-средах лежит в диапазоне между нулем и 0x1 F или равно 0x7F (символ DEL). В противном случае возвращается нуль. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом управляющем символе. |#include <ctype.h> #include <stdio.h> int main(void) 322 Часть HI. Стандартная библиотека С
{ char ch; for(;;) { ch = getchar(); if(ch ~ ’) break; if(iscntrl(ch)) printf("%c является управляющим символомп", ch); } return 0; } Зависимые функции isalnumO, isalpha (), isdigitO, isgraphO, isprintO, ispunctO и isspace () В Функция isdigit |#include <ctype.h> int isdigit(int ch); Функция isdigit О возвращает ненулевое значение, если ее аргумент ch является цифрой, т.е. попадает в диапазон 0-9. В противном случае возвращается нуль. Пример Данная программа проверяет каждый символ, прочитанный из стандартного вход- ного потока stdin, и выдает сообщение, если этот символ окажется цифрой. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if (ch == '.’) break; if(isdigit(ch)) printf("%c является цифройп", ch); } return 0; } Зависимые функции isalnumO, isalpha (), iscntrlO, isgraphO, isprintO, ispunctO и isspace () Глава 14. Строковые и символьные функции 323
Н Функция isgraph |#include <ctype.h> int isgraph(int ch); Функция isgraph () возвращает ненулевое значение, если ее аргумент ch яв- ляется любым печатаемым символом, но не пробелом. В противном случае воз- вращается нуль. Для ASCII-сред значения печатаемых символов лежат в диапазо- не от 0x21 до 0x7 Е. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом печатаемом символе. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch - getchar(); if(isgraph(ch)) printf(”%c является печатаемым символомп", ch); if(ch == *.•) break; } return 0; } Зависимые функции isalnum(), isalphaO, iscntrl (), isdigitO, isprintO, ispunctO и isspace() В Функция islower |#include <ctype.h> int islower(int ch); Функция islower () возвращает ненулевое значение, если аргумент ch является строчной буквой. В противном случае возвращается нуль. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждой строчной букве. #include <ctype.h> #include <stdio.h> int main(void) { char ch; 324 Часть III. Стандартная библиотека С
f or (;;) { ch = getchar(); if(ch == ’.’) break; if(islower(ch)) printf("%c является строчной буквойп", ch); } return 0; } Зависимые функции isuper() И Функция isprint |#include <ctype.h> int isprint(int ch); Функция isprintO возвращает ненулевое значение, если аргумент ch является печатаемым символом, включая пробел. В противном случае возвращается нуль. В ASCII-средах значения печатаемых символов лежат в диапазоне от 0x20 до 0х7Е. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом печатаемом символе. #include <ctype.h> #include <stdio.h> int main(void) { char ch; f or (;;) { ch = getchar() ; if (islower (ch) ) printf (’’символ %c является печатаемымп”, ch); if(ch == ’.’) break; } return 0; } Зависимые функции isalnumO, isalpha (), iscntrlO, isdigitO, isgraphO, ispunctO и isspace () И Функция ispunct Ittinclude <ctype.h> int ispunct(int ch); Функция ispunctO возвращает ненулевое значение, если аргумент ch является знаком пунктуации. В противном случае возвращается нуль. Под знаками пунктуации Глава 14. Строковые и символьные функции 325
подразумеваются все печатаемые символы за исключением пробела, которые не отно- сятся к алфавитно-цифровым. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом знаке пунктуации. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if(ispunct(ch)) printf("%c является знаком пунктуациип"f ch); if(ch == ’.•) break; } return 0; } Зависимые функции isalnum(), isalphaO, iscntrlO, isdigitO, isgraphO и isspace() H Функция isspace |#include <ctype.h> int isspace(int ch) ; Функция isspace () возвращает ненулевое значение, если аргумент ch является пробельным символом. (К пробельным символам, помимо пробела, относятся симво- лы горизонтальной и вертикальной табуляции, перевода страницы, возврата каретки и новой строки1.) В противном случае возвращается нуль. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждом пробельном символе. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if(isspace(ch)) printf("%c является пробельным символомп", ch); if (ch == ’.’) break; 1 При локализации к этому списку могут быть добавлены и другие символы. — Прим. ред. 326 Часть III. Стандартная библиотека С
I return 0; I} Зависимые функции isalnumO, isalpha (), isblankO, iscntrlO, isdigitO, isgraphO и ispunct () В Функция isupper |#include <ctype.h> int isupper(int ch); Функция isupper() возвращает ненулевое значение, если аргумент ch является прописной буквой. В противном случае возвращается нуль. Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждой прописной букве. #include <ctype.h> #include <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if(ch == ’.’) break; if(isupper(ch)) printf("%c является прописной буквойп", ch); } return 0; } Зависимые функции islower() В Функция isxdigit Itfinclude <ctype.h> int isxdigit(int ch); Функция isxdigit () возвращает ненулевое значение, если аргумент ch является шестнадцатеричной цифрой. В противном случае возвращается нуль. Шестнадцате- ричная цифра должна попадать в один из следующих диапазонов: A-F, a-f или 0-9. Глава 14. Строковые и символьные функции 327
Пример Данная программа проверяет все символы, прочитанные из стандартного входного потока stdin, и выдает сообщение о каждой шестнадцатеричной цифре. tfinclude <ctype.h> tfinclude <stdio.h> int main(void) { char ch; for(;;) { ch = getchar(); if(ch == break; if(isxdigit(ch)) printf("%c является шестнадцатеричной цифройп", ch); } return 0; } Зависимые функции isalnumO, isalphaO, iscntrlO, isdigitO, isgraphO, ispunctO и isspaceO В Функция memchr Itfinclude <string.h> void *memchr(const void ★buffer, int ch, size_t count); Функция memchr() просматривает массив, адресуемый параметром buffer, чтобы отыскать первое вхождение символа ch в первых count символах. Эта функция возвращает указатель на первый из символов ch, входящих в массив buffer, или нулевой указатель, если символ ch не найден. Пример Данная программа выводит на экран сообщение из примера, tfinclude <stdio.h> tfinclude <string.h> int main(void) { char *p; p = memchr("строка из примера", ’ 17); printf(p); return 0; } 328 Часть III. Стандартная библиотека С
Еще один пример Некоторые примеры, приведенные в качестве иллюстрации применения строковых функций, иногда носят “несколько учебный” характер и не всегда могут быть реко- мендованы для профессионального программирования. Например, указание констан- ты 17 в операторе р = memchr("строка из примера", • ’, 17); предыдущего примера едва ли может служить хорошим примером для программиста. (Кстати ска- зать, против подобного употребления констант автор предупреждал читателей в пре- дыдущих главах. Просто в данной программе автор не хотел “затенять” основную идею посторонними деталями.) Подумайте, что будет, если изменить строку? Опять подсчитывать количество символов? Чтобы избежать этого, во многих руководствах предлагается использовать для этой цели функцию strlen. Вот какой пример приме- нения приводится, например, в документации по Borland C++ Builder 5: #include <string.h> ttinclude <stdio.h> int main(void) { char str[17]; char *ptr; strcpy(str, "This is a string"); ptr = (char *) memchr(str, ’ r’, strlen(str)); if (ptr) printf("The character ’r’ is at position: %dn", ptr - str) ; else printf("The character was not foundn"); return 0; В данном примере, правда, константа указана в объявлении массива str. Однако этого можно было легко избежать, проинициализировав массив в объявлении. Прав- да, остается еще один недостаток: некоторое снижение эффективности кода из-за вы- зова функции strlen. Но и его можно устранить, если длина строки может быть вы- числена при компиляции (как в наших примерах). Действительно, тогда ведь можно воспользоваться операцией sizeof и записать не константу 17, а константное выра- жение sizeof (str) /sizeof (char). (Отсюда вывод: никогда не верьте тем, кто гово- рит, что последовательное применение технологии программирования приводит к ухудшению характеристик программы. Если язык программирования хорошо сконст- руирован, то ничего подобного быть не может. Правда, если язык продуман плохо... то едва ли стоит на нем писать программы!)1 Зависимые функции memcpy() и isspace() В Функция memcmp |#include <string.h> int memcmp (const void ★bufl, const void *buf2f size_t count); 1 Этот раздел добавлен редактором перевода. — Прим. ред. Глава 14. Строковые и символьные функции 329
Функция memcmp () сравнивает первые count символов массивов, адресуемых пара- метрами bufl и buf2. Функция memcmp () возвращает целое значение, которое интерпретируется сле- дующим образом. Значение_________________________________Результат сравнения______________________ Меньше нуля bufl меньше buf2 Нуль bufl равен buf2 Больше нуля bufl больше buf2 Пример Данная программа выдает результат сравнения двух своих аргументов, которые за- даются в командной строке. #include <stdio.h> #include <string.h> #include <stdlib.h> int main(int argc, char *argv[]) { int outcome, len, 11, 12; if(argc!=3) { printf("Неверно задано число аргументов."); exit(1); } /* определение длины более короткой строки */ 11 = strlen(argv[1]); 12 = strlen(argv[2]); len = 11 < 12 ? 11:12; outcome = memcmp(argv[1], argv[2], len); if(!outcome) printf("Равны"); else if(outcome<0) printf("Первый меньше второго."); else printf("Первый больше второго."); return 0; } Зависимые функции memchrO, memcpyO HstrcmpO И Функция memcpy |#include <string.h> void *memcpy(void ★to, const void ★from, size_t count); Функция memcpy () копирует count символов из массива, адресуемого параметром from, в массив, адресуемый параметром to. Если заданные массивы перекрываются, поведение функции memcopy () не определено. В версии С99 к параметрам to и from применен квалификатор restrict. Функция memcpy () возвращает значение указателя to. 330 Часть III. Стандартная библиотека С
Пример Данная программа копирует содержимое массива bufl в массив buf2 и выводит результат на дисплей. #include <stdio.h> #include <string.h> tfdefine SIZE 80 int main(void) { char bufl[SIZE], buf2[SIZE]; strcpy(bufl, "Когда, в случае если..."); memcpy(buf2, bufl, SIZE); printf(buf2); return 0; } Зависимые функции memmove() Функция memmove |#include <string.h> void *memmove(void ★to, const void ★from, size_t count); Функция memmove () копирует count символов из массива, адресуемого параметром from, в массив, адресуемый параметром to. Если заданные массивы перекрываются, процесс копирования проходит корректно, т.е. соответствующее содержимое будет помещено в массив to, но содержимое массива from при этом изменится. Функция memmove () возвращает значение указателя to. Пример Данная программа сдвигает содержимое массива str на 10 позиций в сторону младших адресов и выводит результат на дисплей. #include <stdio.h> tfinclude <string.h> #define SIZE 80 int main(void) { char str[SIZE], *p; strcpy(str, "Когда, в случае если..."); р = str +10; memmove(str, p, SIZE); printf("результат сдвига: %s", str); return 0; } Глава 14. Строковые и символьные функции 331
Зависимые функции memcpy() Функция memset |#include <string.h> void *memset(void ★buf, int ch, size_t count); Функция memset () копирует младший байт параметра ch в первые count символов массива, адресуемого параметром buf. Функция возвращает значение указателя buf Чаще всего функция memset () используется для инициализации области памяти некоторым известным значением. Пример Данный фрагмент инициализирует первые 100 байтов массива, адресуемого указа- телем buf, нулями. Затем он помещает символы X в первые 10 байтов этого массива и выводит строку хххххххххх. I memset(buf, '?, 100); memset (buf, ’X’, 10); printf(buf); Зависимые функции memcmp (), memcpy () и memmove () Я Функция strcat |#include <string.h> char *strcat(char ★strl, const char ★str?); Функция strcat () присоединяет к строке strl копию строки str2 и завершает строку strl нулевым символом. Конечный нуль-символ, первоначально завершающий строку strl, перезаписывается первым символом строки str2. Строка str2 при этом не изменяется. Если заданные массивы перекрываются, поведение функции strcat () не определено. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Функция strcat () возвращает значение указателя strl. Помните, что при выполнении операций с массивами символов контроль наруше- ния их границ не выполняется, поэтому программист должен сам позаботиться о дос- таточном размере массива strl, позволяющем вместить как его исходное содержимое, так и содержимое массива str2. Пример Данная программа дописывает первую строку, прочитанную из стандартного вход- ного потока stdin, ко второй строке. Например, если пользователь введет строки привет и всем, то программа выведет сообщение всемпривет. I #include <stdio.h> I #include <string.h> 332 Часть III. Стандартная библиотека С
int main(void) t { char si[80], s2[80]; gets(si) ; gets (s2) ; strcat (s2, si) ; printf (s2) ; return 0; } Зависимые функции strchrO, strcmp () HstrcpyO И Функция strchr |#include <string.h> char *strchr(const char ★str, int ch) ; Функция strchrO возвращает указатель на первое вхождение младшего байта пара- метра ch в строку str. Если указанный символ не найден, возвращается нулевой указатель. Пример Данная программа выводит строку из примера. #include <stdio.h> #include <string.h> int main(void) { char *p; p = strchr("строка из примера", ’ ’); printf(p); return 0; } Зависимые функции strpbrkO, strspn(), strstr () и strtokQ I функция strcmp |#include <string.h> int strcmp(const char ★strl, const char ★str2); Функция strcmp () сравнивает в лексикографическом порядке две строки и воз- вращает целое значение, зависящее следующим образом от результата сравнения. Глава 14. Строковые и символьные функции 333
Значение___________________________________Результат сравнения строк Меньше нуля str1 меньше str2 Нуль str1 равна str2 Больше нуля str1 больше str2 Пример Следующую функцию можно использовать для проверки пароля. В случае неудачи она возвращает нуль, а при успешном выполнении — единицу. int password(void) { char s [80]; printf("Введите пароль: "); gets(s); if(strcmp(s, "пароль")) { printf("Неверный парольn"); return 0; } return 1; } Зависимые функции strchrO, strcpyO и strcmp () H Функция strcoll |#include <string.h> int strcoll(const char ★strl, const char *str2); Функция strcoll () сравнивает строку, адресуемую указателем strl, со строкой, адресуемой указателем str2. Сравнение выполняется с учетом значения параметра lo- cale, заданного с помощью функции setlocale () (подробности приводятся в описа- нии функции setlocale ()). Функция strcoll () возвращает целое значение, которое интерпретируется сле- дующим образом. Значение Меньше нуля Нуль Больше нуля Результат сравнения strl меньше str2 strl равно str2 strl больше str2 Пример Данный фрагмент программы выводит на экран сообщение Равно. | if(strcoll("привет", "привет")) printf("Равно"); Зависимые функции memcmp () и strcmp () 334 Часть III. Стандартная библиотека С
В Функция strcpy |#include <string.h> char *strcpy(char ★strl, const char *str2); Функция strcpy () копирует содержимое строки str2 в строку strl. Параметр str2 должен указывать на строку с завершающим нулевым символом. Функция strcpy () возвращает значение указателя strl. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Если символьные массивы strl и str2 перекрываются, поведение функции strcpy () не определено. Пример Следующий фрагмент программы копирует строку привет в строку str. Ichar str[80]; strcpy(str, "привет"); Зависимые функции memcpyO, strchrO, strcmp О и strncmp () В Функция strcspn |#include <string.h> size_t strcspn(const char ★strl, const char ★str2); Функция strcspn О возвращает длину начальной подстроки в строке, адресуемой параметром strl, которая не содержит ни одного символа из строки, адресуемой пара- метром str2. Другими словами, функция strcspn О возвращает индекс первого сим- вола в строке strl, который совпадает с любым из символов в строке str2. Пример Следующая программа выводит число 6. #include <string.h> #include <stdio.h> int main(void) { int len; len = strcspn("это тест", "сл"); printf("%d", len); return 0; } Зависимые функции strrchrO, strpbrkO, strstr () и strtokO Глава 14. Строковые и символьные функции 335
В Функция strerror Ittinclude <string.h> char *strerror(int errnum); Функция strerror () возвращает указатель на строку, содержащую системное со- общение об ошибке, связанной со значением егтит. Эту строку не следует менять ни при каких обстоятельствах. Пример Данный фрагмент программы выводит на экран системное сообщение об ошибке. | printf(strerror(10)); Функция strlen I#include <string.h> size_t strlen(const char ★str); Функция strlen () возвращает длину строки, адресуемой параметром str, причем строка должна заканчиваться символом конца строки. Символ конца строки ( ’0’ ) не учитывается. Пример Данный фрагмент программы выводит на экран число 6. | printf("%d", strlen("привет")); Зависимые функции memcpy (), strchr(), strcmp () HstrncmpO И Функция strncat |#include <string.h> char *strncat(char ★strl, const char *str2, size_t count); Функция strncat () присоединяет к строке, адресуемой параметром strl, не более count символов строки, адресуемой параметром str2, завершая “результирующую” строку strl нулевым символом. Конечный нуль-символ, первоначально завершающий строку strl, перезаписывается первым символом строки str2. Строка str2 в результате этой операции конкатенации не меняется. Если строки перекрываются, поведение функции strncat () не определено. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Функция strncat () возвращает значение указателя strl. При выполнении операций с массивами символов контроль нарушения их границ не выполняется, поэтому программист должен сам позаботиться о достаточном разме- ре массива strl, позволяющем вместить как его исходное содержимое, так и содержи- мое присоединяемого массива str2. 336 Часть III. Стандартная библиотека С
Пример Данная программа конкатенирует первую строку, прочитанную из стандартного входного потока stdin, ко второй строке и предотвращает переполнение массива si, в который записывается результат. Например, если пользователь введет сначала при- вет, а затем всем, то программа выведет сообщение всемпривет. #include <stdio.h> #include <string.h> int main(void) { char si[80] , s2 [80]; unsigned int len; gets(si); gets (s2); /* вычисление подходящей длины */ len = 79-strlen(s2); strncat(s2, si, len); printf(s2); return 0; } Зависимые функции strcat (), strnchrO, strncmpO HstrncpyO В Функция strncmp |#include <string.h> int strncmp(const char ★strl, const char ★str2, size_t count); Функция strncmp () сравнивает в лексикографическом порядке не более count символов из двух строк, заканчивающихся символом конца строки, и возвращает це- лое значение, зависящее от результата сравнения следующим образом: Значение Меньше нуля Нуль Больше нуля Результат сравнения строк strl меньше str2 strl равна str2 strl больше str2 Если в какой-нибудь из заданных строк меньше count символов, сравнение закан- чивается при обнаружении первого нулевого символа. Пример Следующая функция сравнивает первые восемь символов двух своих аргументов, взятых из командной строки, и выдает сообщение в случае их равенства. |#include <stdio.h> #include <string.h> #include <stdlib.h> Глава 14. Строковые и символьные функции 337
int main(int argc, char *argv[]) { if(argc!=3) { printf("Неверное количество аргументов."); exit(1); if(!strncmp(argv[1] , argv[2], 8)) printf("Строки одинаковые.n"); return 0; Зависимые функции strcmpO, strnchrO иstrncpy() И функция strncpy |#include <string.h> char *strncpy(char ★strl, const char ★str2f size_t count); Функция strncpy () копирует не более count символов из строки, адресуемой па- раметром str2, в массив, адресуемый параметром strl. Параметр str2 должен указывать на строку, заканчивающуюся символом конца строки. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Если заданные массивы символов перекрываются, поведение функции strncpy () не определено. Если длина строки, адресуемой параметром str2, меньше значения count, то в ко- нец строки-результата strl добавляются “недостающие” нулевые символы. Если же длина строки, адресуемой параметром str2, больше значения count, то строка- результат, адресуемая параметром strl, не будет заканчиваться символом конца строки1. Функция strncpy () возвращает значение указателя strl. Пример Следующий фрагмент программы копирует не более 79 символов из строки strl в строку str2, тем самым гарантируется, что массив не переполнится. Ichar strl[128], str2[80]; gets(strl); strncpy(str2, strl, 79); Зависимые функции memcpyO, strchrO, strncat () и strncmp () 1 Обратите внимание, что такую “строку ” считать полноценной нельзя, ведь некоторые строковые функции работать с подобными строками могут некорректно. — Прим. ред. 338 Часть III. Стандартная библиотека С
В Функция strpbrk |#include <string.h> char *strpbrk(const char ★strl, const char ★str2}; Функция strpbrk () возвращает указатель на первый символ в строке, адресуемой параметром strl, который совпадает с любым символом в строке, адресуемой парамет- ром str2. Символы конца строки, которыми должны оканчиваться данные строки, в расчет не берутся. Если совпадений нет, возвращается нулевой указатель. Пример Данная программа выводит на экран сообщение о тест. #include <stdio.h> #include <string.h> int main(void) { char *p; p = strpbrk("3TO тест”, " есою”); printf(p); return 0; } Зависимые функции strspnO, strrchr(), strstr () и strtokO IS Функция strrchr |#include <string.h> char *strrchr(const char *str, int ch); Функция strrchr () возвращает указатель на последнее вхождение младшего бай- та параметра ch в строку, адресуемую параметром str. Если совпадение не обнаруже- но, возвращается нулевой указатель. Пример Данная программа выводит сообщение пыль летит. #include <stdio.h> #include <string.h> int main(void) { char *p; p = strrchr("по полю пыль летит”, ’ п’); printf(р); return 0; } Глава 14. Строковые и символьные функции 339
Зависимые функции strpbrkO, strspnO, strstr () и strtokO И Функция strspn |#include <string.h> size_t strspn(const char ★strl, const char ★str2); Функция strspnO возвращает длину начальной подстроки строки, адресуемой пара- метром strl, которая состоит только из символов, содержащихся в строке, адресуемой па- раметром str2. Другими словами, функция strspn () возвращает индекс первого символа в строке strl, который не совпадает ни с одним из символов в строке sflr27. Пример Эта программа выводит число 11. #include <string.h> #include <stdio.h> int main(void) { int len; len=strspn("это строка из примера", "акортэ с"); printf("%d”f len); return 0; } Зависимые функции strpbrkO, strrchr(), strstr () и strtokO H Функция strstr |#include <string.h> char *strstr(const char ★strl, const char ★str2); Функция strstr О возвращает указатель на первое вхождение подстроки, адре- суемой параметром str2, в строку, адресуемую параметром strl. Если совпадение не обнаружено, возвращается нулевой указатель. Пример Данная программа выводит сообщение от невпроворот. |#include <string.h> #include <stdio.h> 1 Или (что то же самое) функция strspnO возвращает индекс первого символа в строке strl, который не входит в строку str2. — Прим. ред. 340 Часть III. Стандартная библиотека С
int main(void) { char *p; p = strstr("хлопот невпроворот", "от"); printf(p); return 0; } Зависимые функции strchrO, strcspn(), strpbrk (), strspn(), strtokO и strrchr () В Функция strtok |#include <string.h> char *strtok(char ★strl, const char ★str2); Функция strtokO возвращает указатель на следующую лексему в строке, адре- суемой параметром strl. Символы, образующие строку, адресуемую параметром str2, представляют собой разделители, которые определяют лексему. При отсутствии лек- семы, подлежащей возврату, возвращается нулевой указатель. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Чтобы разделить некоторую строку на лексемы, при первом вызове функции strtokO параметр strl должен указывать на начало этой строки. При последующих вызовах функции в качестве параметра strl нужно использовать нулевой указатель. Этим способом вся строка разбивается на лексемы. При каждом обращении к функции strtokO можно использовать различные на- боры разделителей. Пример Эта программа разбивает строку ’’Травка зеленеет, солнышко блестит’’ на лексемы, разделителями которых служат пробелы и запятые. В результате получится Травка|зеленеет|солнышко|блестит #include <stdio.h> #include <string.h> int main(void) { char *p; p = strtok (’’Травка зеленеет, солнышко блестит”, ” ”); printf(р); do { р = strtok('', ”, ”); if(p) printf(”|%s”, p); } while(p); return 0; } Глава 14. Строковые и символьные функции 341
Зависимые функции strchrO, strcspn О, strpbrk(), strrchr О Hstrspn() И Функция strxfrm Ittinclude <string.h> size_t strxfrm(char *strlr const char ★str2r size_t count); Функция strxfrm () преобразует строку, адресуемую параметром str2, таким обра- зом, чтобы ее можно было использовать функцией strcmp (), и помещает результат преобразования в строку, адресуемую параметром strl. После преобразования резуль- тат вызова функции strcmp (), использующей параметр strl, будет совпадать с резуль- татом вызова функции strcoll (), использующей исходную строку, на которую ука- зывает параметр str2. В массив, адресуемый параметром strl, записывается не более count символов. В версии С99 к параметрам strl и str2 применен квалификатор restrict. Функция strxf rm () возвращает длину преобразованной строки1. Пример Данный фрагмент программы (одна строка) преобразует первые 10 символов стро- ки, адресуемой указателем s2, и помещает результат преобразования в строку, адре- суемую указателем si. | strxfrm(sl, s2, 10); Зависимые функции strcoll() В Функция tolower Ittinclude <ctype.h> int tolower(int ch); Функция tolower () возвращает строчный эквивалент параметра ch, если он явля- ется буквой; в противном случае возвращается ch без изменений. Пример Этот фрагмент программы выводит букву а. | putchar(tolower(’А’)); 1 В связи с этим может возникнуть вопрос: что это за таинственное преобразование, кото- рому подвергается строка str2, и зачем оно нужно? Действительно, в списке зависимых функций автор не приводит каких-либо функций, задающих такое преобразование, или способных вли- ять на него. Все дело в том, что это преобразование предназначено для строк, выводимых лока- лизованными программами, в которых учитывается местная специфика, т.е. национальная и культурная среда, в которой функционирует система или программа. Установка местной специ- фики может выполняться геополитической функцией setlocale (). Ее вызов в случае, напри- мер, кодовой страницы 850 для французского языка может выглядеть так: setlocale ( LC_ALL, "French_France .850" ) ;. — Прим. ред. 342 Часть III. Стандартная библиотека С
Зависимые функции toupper() И Функция toupper |#include <ctype.h> int toupper(int ch) ; Функция toupper () возвращает прописной эквивалент параметра ch, если ch — буква; в противном случае ch возвращается без изменений. Пример Этот фрагмент программы выводит букву А. | putchar(toupper(’а’)); Зависимые функции tolower() Глава 14. Строковые и символьные функции 343
Полный справочник по Глава 15 Математические функции
В версии С99 математическая библиотека была значительно пополнена; при этом число ее функций увеличилось более чем в три раза (стандарт С89 определял всего лишь 22 математические функции). Одной из основных целей комитета по вер- сии С99 было повышение применимости языка С для численных расчетов. Теперь с уверенностью можно сказать, что эта цель достигнута! Для использования математических функций в программу необходимо включить заголовок <math.h>. Помимо объявления математических функций, этот заголовок определяет один или несколько макросов. В версии С89 заголовком <math.h> опре- деляется только макрос huge_val, который представляет собой значение типа double, сигнализирующее о возникшем переполнении. В версии С99 кроме него оп- ределены следующие макросы. HUGE_VALF HUGE_VALL INFINITY math_errhandling MATH_ERRNO версия макроса huge_val с типом float версия макроса huge_val с типом long double Значение, представляющее бесконечность Содержит макросы math_errno и/или math_errexcept Встроенная глобальная переменная errno, используемая для вывода сообщений об ошибках MATH_ERREXCEPT Исключение, возбуждаемое при выполнении операций над веществен- ными числами, с целью вывода сообщения об ошибках NAN Не число В версии С99 определены следующие макросы (подобные функциям), классифи- цирующие значение. int fpclassify(fpva/) В зависимости от значения аргумента fpval возвращает fp_infinity, fp_nan, FP-NORMAL, fp_subnormal или fp_zero. Эти макросы опреде- ляются заголовком <math. h> int isfinite(fpvaZ) int isinf(fpvaZ) int isnan(fpvaZ) int isnormal(fpva/) Возвращает ненулевое значение, если fpval конечное Возвращает ненулевое значение, если fpval представляет бесконечность Возвращает ненулевое значение, если fpval— не является числом Возвращает ненулевое значение, если fpval представляет собой норма- лизованное число int signbit(fpvaZ) Возвращает ненулевое значение, если fpval отрицательно (т.е. установ- лен его знаковый разряд) В версии С99 определены следующие макросы сравнения, аргументы которых (а и Ь) должны иметь числовые значения в формате с плавающей точкой. int isgreater(a, Ь) int isgreaterequal(a, Ь) int isless(a, Ь) int islessequal(a, b) int islessgreater(a, b) int isunordered(a, b) Возвращает ненулевое значение, если а больше b Возвращает ненулевое значение, если а больше или равно b Возвращает ненулевое значение, если а меньше b Возвращает ненулевое значение, если а меньше или равно b Возвращает ненулевое значение, если а больше или меньше b Возвращает 1, если а и b не упорядочены одно по отношению к другому Возвращает 0, если а и b упорядочены Эти макросы введены, так как они прекрасно обрабатывают значения, которые не являются числами, не вызывая при этом исключений вещественного типа. Макросы EDOM и ERANGE также используются математическими функциями. Эти макросы определены в заголовке <errno.h>. Ошибки в версиях С89 и С99 обрабатываются по-разному. Так, в версии С89, если аргумент математической функции не попадает в область определения, возвращается некоторое значение, зависящее от конкретной реализации, а встроенная глобальная 346 Часть III. Стандартная библиотека С
целая переменная errno устанавливается равной значению EDOM. В версии С99 нару- шение области определения также приводит к возврату значения, зависящего от кон- кретной реализации. Однако по значению math_errhandling можно судить о вы- полнении других действий. Если math_errhandling содержит значение MATH_ERRNO, то встроенная глобальная целая переменная errno устанавливается равной значению EDOM. Если же math_errhandling содержит значение MATH_ERREXCEPT, возбуждается исключение вещественного типа. В версии С89, если функция генерирует результат, который слишком велик и потому не может быть представлен в машинном формате, то происходит переполнение. В этом случае функция возвращает значение HUGE_VAL, а переменная errno устанавливается равной значению ERANGE, сигнализирующему о выходе за пределы диапазона. При потере значимости функция возвращает нуль и устанавливает переменную errno рав- ной значению ERANGE. В версии С99 ошибка переполнения также приводит к тому, что функция возвращает значение HUGE_VAL, а при потере значимости — нуль. Если math_e г г handling содержит значение MATH_ERRNO, глобальная переменная errno устанавливается равной значению ERANGE, свидетельствующему об ошибке диапазона. Если же math_errhandling содержит значение MATH_ERREXCEPT, возбуждается ис- ключение вещественного типа. В версии С89 аргументами математических функций должны быть значения типа double, причем значения, возвращаемые функциями, тоже имеют тип double. В вер- сии С99 добавлены варианты этих функций, работающие с типами float и long double. В этих функциях используются суффиксы f и 1 соответственно. Например, в версии С89 функция sin () определена следующим образом. double sin(double arg); Версия С99 поддерживает приведенное выше определение функции sin(), но в ней добавлены еще две ее модификации — sinf () и sinl (). float sinf(float arg); long double sinl(long double arg); Операции, выполняемые всеми тремя функциями, одинаковы; различаются лишь типы данных, над которыми выполняются эти операции. Добавление модификаций f и 1 математических функций позволяет использовать версию, которая наиболее точно соответствует данным, с которыми работают функции. Поскольку в версии С99 добавлено так много новых функций, стоит отдельно пе- речислить те из них, которые поддерживаются версией С89. Это следующие функции. acos cos fmod modf tan asin cosh frexp pow tanh atan exp Idexp sin atan2 fabs log sinh ceil floor loglO sqrt И последнее замечание: все углы задаются в радианах. Н Семейство функций acos |#include <math.h> float acosf(float arg); double acos(double arg); long double acosl(long double arg); Функции acosf () и acosl () добавлены в версии C99. Глава 15. Математические функции 347
Каждая функция семейства acos () возвращает значение арккосинуса от аргумента arg. Значение аргумента должно находиться в диапазоне от -1 до 1; в противном слу- чае возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определения). Пример Данная программа выводит значения арккосинусов последовательности аргумен- тов, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. программа составляет таблицу арккосинуса. #include <math.h> #include <stdio.h> int main(void) { double val = -1.0; do { printf("Арккосинус %f равен %f.n", val, acos(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции asin(), atan(), atan2 (), sin(), cos (), tan(), sinh(), cosh () HtanhO в Семейство функций acosh I#include <math.h> float acoshf(float arg); double acosh(double arg); long double acoshl(long double arg); Функции acosh (), acoshf () и acoshl () добавлены в версии C99. Каждая функция семейства acosh () возвращает значение гиперболического арк- косинуса от аргумента arg. Значение аргумента должно быть больше или равно нулю; в противном случае возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определения). Зависимые функции asinh(), atanh(), sinh(), cosh() HtanhO В Семейство функций asin I#include <math.h> float asinf(float arg) ; double asin(double arg); long double asinl(long double arg); Функции as inf () и as ini () добавлены в версии C99. 348 Часть III. Стандартная библиотека С
Каждая функция семейства asin() возвращает значение арксинуса от аргумента arg. Значение аргумента должно находиться в диапазоне от -1 до 1; в противном слу- чае возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определения). Пример Данная программа выводит значения арксинусов последовательности аргументов, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. состав- ляет таблицу арксинуса. tfinclude <math.h> ttinclude <stdio.h> int main(void) { double val = -1.0; do { printf (’’Арксинус %f равен %f.n”, val, asin(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции acos (), atan(), atan2 (), sin(), cos (), tan (), sinh(), cosh () Htanh() И Семейство функций asinh 1#include <math.h> float asinhf(float arg) ; double asinh(double arg) ; long double asinhl(long double arg); Функции asinh (), asinhf () и asinhl () добавлены в версии C99. Каждая функция семейства asinh () возвращает значение гиперболического арк- синуса от аргумента arg. Зависимые функции acosh(), atanh(), sinh(), cosh() и tanh() Ш Семейство функций atan 1#include <math.h> float atanf(float arg) ; double atan(double arg); long double atanl(long double arg) ; Функции atanf () и atanl () добавлены в версии C99. Каждая функция семейства atan () возвращает значение арктангенса от аргумента arg. Глава 15. Математические функции 349
Пример Данная программа выводит значения арктангенсов последовательности аргументов, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. состав- ляет таблицу арктангенса. #include <math.h> #include <stdio.h> int main(void) { double val = -1.0; do { printf (’’Арктангенс %f равен %f.n", val, atan(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции asin(), acos(), atan2 (), tan(), cos (), sin(), sinh(), cosh() Htanh() S Семейство функций atanh |#include <math.h> float atanhf(float arg); double atanh(double arg); long double atanhl(long double arg); Функции atanh (), atanhf () и atanhl () добавлены в версии C99. Каждая функция семейства atanh () возвращает значение гиперболического арк- тангенса от аргумента arg. Значение аргумента должно находиться в диапазоне от -1 до 1 (не включая границы); в противном случае возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определе- ния). Если arg равен 1 или -1, возможен выход за пределы допустимого диапазона. Зависимые функции acosh(), asinh(), sinh(), cosh() и tanh() И Семейство функций atan2 I#include <math.h> float atan2f(float a, float b); double atan2(double a, double b); long double atan21(long double a, long double b); Функции atan2f () и atan21 () добавлены в версии C99. Каждая функция семейства atan2 () возвращает значение арктангенса отноше- ния а/b. Для вычисления квадранта возвращаемого значения используются знаки аргументов функции. 350 Часть III. Стандартная библиотека С
Пример Данная программа выводит значения арктангенсов последовательности аргументов у, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. со- ставляет таблицу арктангенса. #include <math.h> #include <stdio.h> int main(void) { double val =» -1.0; do { printf (’’Арктангенс %f равен %f.n”, val, atan2(val, 1.0)); val +« 0.1; } while(val<=l.0); return 0; }. Зависимые функции asin(), acos(), atan(), tan(), cos(), sin(), sinh(), cosh() HtanhO В Семейство функций cbrt Ittinclude <math.h> float cbrtf(float num); double cbrt(double num); long double cbrtl(long double num); Функции cbrt (), cbrtf () и cbrtl () добавлены в версии C99. Каждая функция семейства cbrt() возвращает значение кубического корня от аргумента пит. Пример Данный фрагмент программы выводит на экран число 2. | printf("%f", cbrt(8)); Зависимые функции sqrt() В Семейство функций ceil |#include <math.h> float ceilf(float num); double ceil(double num); long double ceill(long double num); Функции ceilf () и ceill () добавлены в версии C99. Глава 15. Математические функции 351
Каждая функция семейства ceil () возвращает наименьшее целое (представленное в виде значения с плавающей точкой), которое больше значения аргумента пит или равно ему. Например, если пит равно 1.02, функция ceil() вернет значение 2.0, а при пит, равном -1.02, — значение -1. Пример Данный фрагмент программы выводит на экран число 10. | printfceil(9.9)); Зависимые функции floor() и fmod() Семейство функций copysign I#include <math.h> float copysignf(float val, float signval); double copysign (double val, double signval)*, long double copysignl(long double val, long double signval); Функции copysign (), copysignf () и copysignl () добавлены в версии C99. Каждая функция семейства copysignO наделяет аргумент val знаком, который имеет аргумент signval, и возвращает полученный результат. Таким образом, возвра- щаемое значение имеет величину, равную величине аргумента val, а его знак совпада- ет со знаком аргумента signval. Зависимые функции fabs () Семейство функций cos I#include <math.h> float cosf(float arg); double cos(double arg); long double cosl(long double arg); Функции cosf () и cosl () добавлены в версии C99. Каждая функция семейства cos () возвращает значение косинуса аргумента arg. Значение аргумента должно быть выражено в радианах. Пример Данная программа выводит значения косинусов последовательности аргументов, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. состав- ляет таблицу косинуса. I#include <math.h> #include <stdio.h> int main(void) { 352 Часть III. Стандартная библиотека С
double val = -1.0; do { printf("Косинус %f равен %f.n", val, cos(val)); val += 0.1; } while(val<=l.0) ; return 0; } Зависимые функции asin(), acos(), atan2(), atan(), tan (), sin(), sinh(), cos() Htanh() Семейство функций cosh |#include <math.h> float coshf(float arg); double cosh(double arg); long double coshl(long double arg); Функции coshf () и coshl () добавлены в версии C99. Каждая функция семейства cosh() возвращает значение гиперболического коси- нуса аргумента arg. Пример Данная программа выводит значения гиперболических косинусов последователь- ности аргументов, лежащих в интервале от -1 до 1 и увеличивающихся с шагом одна десятая, т.е. составляет таблицу гиперболического косинуса. ttinclude <math.h> ttinclude <stdio.h> int main (void) { double val = -1.0; do { printf("Гиперболический косинус %f равен %f.n", val, cosh(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции asin(), acos(), atan2(), atan(), tan (), sin() Htanh() В Семейство функций erf Ittinclude <math.h> float erff(float arg); Глава 15. Математические функции 353
I double erf(double arg); long double erf1(long double arg); Функции erf (), erff () и erf 1 () добавлены в версии C99. Каждая функция семейства erf() возвращает значение функции ошибок1 от аргумента arg. Зависимые функции erfс() В Семейство функций erfc I#include <math.h> float erfcf(float arg); double erfc(double arg); long double erfcl(long double arg); Функции erfc (), erfcf () и erf cl () добавлены в версии C99. Каждая функция семейства erf с () возвращает функцию ошибок дополнительную2 от аргумента arg. Зависимые функции erf () В Семейство функций ехр |#include <math.h> float expf(float arg); double exp(double arg) ; long double expl(long double arg); Функции expf () и expl () добавлены в версии C99. Каждая функция семейства ехр () возвращает значение экспоненты от аргумента arg (число е, возведенное в степень, которая равна значению аргумента arg). 1 Интеграл (вероятности) ошибок: «Дх)=-2- • W о Иногда называется просто интегралом ошибок или интегралом вероятности. В теории веро- ятности чаще используется не интеграл вероятности, а интеграл вероятности Гаусса, или функ- ция нормального распределения = | [1 + — Прим. ред. 2 Дополнительный интеграл вероятности: erfc(x)= -1 dt =l-erf(x). — Прим. ред. 354 Часть III. Стандартная библиотека С
Пример Данный фрагмент программы выводит число е, округленное до значения 2.718282. I printf("е, возведенное в первую степень, приблизительно равно: %f.", exp(1.0)); Зависимые функции ехр2 () и log () В Семейство функций ехр2 |#include <math.h> float exp2f(float arg); double exp2(double arg); long double exp21(long double arg); Функции exp2 (), exp2f () и exp21 () добавлены в версии C99. Каждая функция семейства ехр2 () возвращает число 2, возведенное в степень arg. Зависимые функции ехр() И log() В Семейство функций expml |#include <math.h> float expmlf(float arg); double expml(double arg); long double expmil(long double arg); Функции expml (), expmlf () и expml 1 () добавлены в версии C99. Каждая функция семейства expml () возвращает уменьшенное на единицу значе- ние числа е, возведенного в степень arg (т.е. возвращаемое значение равно е“ч-7). Зависимые функции ехр () и log () В Семейство функций fabs |#include <math.h> float fabsf(float num); double fabs(double num); long double fabsl(long double num); Функции fabsf () и fabsl () добавлены в версии C99. Каждая функция семейства fabs () возвращает абсолютное значение аргумента пит. Глава 15. Математические функции 355
Пример Данная программа дважды выводит на экран число 1.0. #include <math.h> #include <stdio.h> int main(void) { printf("%1.If %l.lf", fabs(l.O), fabs(-l.O)); return 0; } Зависимые функции abs () В Семейство функций fdim I#include <math.h> float fdimf(float ar float b) ; double fdim(double ar double b); long double fdiml(long double ar long double b); Функции fdim (), fdimf () и fdiml () добавлены в версии C99. Каждая функция семейства fdim() возвращает нуль, если значение аргумента а меньше значения аргумента b или равно ему. В противном случае возвращается ре- зультат вычисления разности а-Ь. Зависимые функции remainder() и remquo() И Семейство функций floor I#include <math.h> float floorf(float num); double floor(double num); long double floorl(long double num); Функции floorf () и floorl () добавлены в версии C99. Каждая функция семейства floor () возвращает наибольшее целое (представленное в виде значения с плавающей точкой), которое меньше значения ар- гумента пит или равно ему. Например, при пит, равном 1.02, функция floor () вер- нет значение 1.0, а при пит, равном -1.02, — значение -2.0. Пример Данный фрагмент программы выводит на экран число 10. | printf("%f", floor(10.9)); 356 Часть III. Стандартная библиотека С
Зависимые функции ceil () и fmod () В Семейство функций fma Ittinclude <math.h> float fmaf(float a, float b, float c); double fma(double a, double b, double c); long double fmaldong double af long double br long double c) ; Функции fma (), fmaf () и fmal () определены в версии C99. Каждая функция семейства fma() возвращает значение выражения а*Ь+с. Округ- ление выполняется только один раз, после завершения всей операции. Зависимые функции round(), 1 round () HllroundO В Семейство функций fmax |#include $math.h> float fmaxf(float a, float b) ; double fmax(double a, double b) ; long double fmaxl(long double a, long double b); Функции fmax (), fmaxf () и fmaxl () определены в версии C99. Каждая функция семейства fmax () возвращает больший из аргументов а и Ь. Зависимые функции fmin () В Семейство функций fmin 1#include <math.h> float fminf(float a, float b); double fmin(double a, double b); long double fminldong double a, long double b) ; Функции fmin (), fminf () и fminl () определены в версии C99. Каждая функция семейства fmin () возвращает меньший из аргументов а и Ь. Зависимые функции fmax () Глава 15. Математические функции 357
В Семейство функций fmod |#include <math.h> float fmodf(float a, float b) ; double fmod(double a, double b) ; long double fmodl(long double a, long double b); Функции fmodf () и fmodl () определены в версии C99. Каждая функция семейства fmod () возвращает остаток от деления аргументов а/Ь. Пример Данная программа выводит на экран число 1.0, являющееся остатком деления 10/3. #include <math.h> #include <stdio.h> int main(void) { printf(”%1.If", fmod(10.0,3.0)); return 0; } Зависимые функции ceil (), floor () и fabs() В Семейство функций frexp I#include <math.h> float frexpf(float num, int ★exp); double frexp(double num, int ★exp); long double frexpl(long double num, int *exp); Функции frexpf () и frexpl () добавлены в версии C99. Каждая функция семейства frexp () разбивает число пит на мантиссу mantissa, значе- ние которой удовлетворяет неравенствам Q.5<mantissa<, и целый показатель степени числа 2 (он обозначен через ехр), притом числа mantissa и ехр выбираются так, чтобы выполня- лось равенство пит = mantissa * 2otp. Значение мантиссы возвращается функцией, а значе- ние показателя1 присваивается переменной, адресуемой указателем ехр. 1 Напомним, что представление числа пит в виде пит = mantissa * If9 (здесь b — основание сис- темы счисления) называется представлением с плавающей точкой (запятой) или полулогарифмическим представлением, и что целая часть логарифма называется характеристикой. Так что ехр=х2(пит)+1, где X2(flww)=Llc)g2(rtww)J — характеристика двоичного логарифма. Число ехр часто называется порядком числа пит (в нормализованном представлении). Заметим также, что терминам мантисса и характери- стика часто придается и иной смысл. Так, по историческим причинам под мантиссой часто подразу- мевают дробную часть логарифма', иногда ее называют также мантиссой логарифма. Что же касается ха- рактеристики, то под ней иногда понимают просто число, которое представляет порядок в представле- нии с плавающей запятой. (В этом смысле в большинстве машин характеристика равна порядку, если он положительный; отличия между ними, как правило, обусловлены тем, что представление порядка, который может быть также и неположительным числом, при реализации операций над числами в по- лулогарифмическом представлении рассматривают как представление неотрицательного числа.) Так что можно сказать, что характеристика в этом смысле —- машинное представление порядка числа. По- рядок в этом контексте называется также иногда экспонентой. (Не путайте с экспонентой- функцией!) — Прим. ред. 358 Часть III. Стандартная библиотека С
Пример Данный фрагмент программы выводит число 0.625 в качестве мантиссы и число 4 — в качестве показателя степени. Iint е; double f; f = frexp(10.0, &e); printf("%f %d", f, e); Зависимые функции Idexp() S Семейство функций hypot |#include <math.h> float hypotf(float sidel, float side2) ; double hypot(double sidel, double side2); long double hypotl(long double sidel, long double side2) ; Функции hypot (), hypotf () и hypot 1 () определены в версии C99. Каждая функция семейства hypot () возвращает длину гипотенузы при заданных длинах двух катетов (т.е. функция возвращает значение квадратного корня из суммы квадратов значений аргументов sidel и side2)[. Зависимые функции sqrt() S Семейство функций ilogb |#include <math.h> int ilogbf(float num); int ilogb(double num); int ilogbl(long double num); Функции ilogb (), ilogbf () и ilogbl () добавлены в версии C99. Каждая функция семейства ilogb() возвращает порядок аргумента пит. Возвра- щаемое значение имеет тип int. Зависимые функции logb () И Семейство функций Idexp |#include <math.h> float Idexpf(float num, int exp); 1 Или расстояние точки с координатами (sidel; side2) от начала координат. — Прим. ред. Глава 15. Математические функции 359
I double Idexp(double num, int exp); long double ldexpl(long double num, int exp); Функции Idexpf () и Idexpl () добавлены в версии C99. Каждая функция семейства Idexp () возвращает значение выражения пит * 2"Л Пример Данная программа выводит число 4. #include <math.h> #include <stdio.h> int main(void) { printf("%f", Idexp(1, 2)); return 0; } Зависимые функции f rexp () и modf () И Семейство функций Igamma |#include <math.h> float Igammaf(float arg); double Igamma(double arg); long double lgammal(long double arg); Функции Igamma (), Igammaf () и Igammal () добавлены в версии C99. Каждая функция семейства Igamma () вычисляет абсолютное значение гамма- функции1 от аргумента arg и возвращает ее натуральный логарифм. Зависимые функции tgamma() В Семейство функций llrint |#include <math.h> long long int llrintf(float arg); long long int llrint(double arg); long long int llrintldong double arg); Функции llrint (), llrintf () и llrintl () добавлены в версии C99. Каждая функция семейства llrint () возвращает значение аргумента arg, округ- ленного до ближайшего целого, которое имеет тип long long int. Зависимые функции Irint() и rint() 1 Другие названия: Г-функция, Г-функция Эйлера, эйлеров интеграл второго рода. — Прим. ред. 360 Часть III. Стандартная библиотека С
S Семейство функций llround |#include <math.h> long long int llroundf(float arg); long long int llround(double arg); long long int llroundl(long double arg); Функции llround (), llroundf () и llroundl () добавлены в версии C99. Каждая функция семейства llround () возвращает значение аргумента arg, округ- ленное до ближайшего целого, которое имеет тип long long int. Значения, отстоя- щие от большего и меньшего целых на одинаковую величину (например, число 3.5), округляются в сторону большего целого. Зависимые функции IroundO и round () Семейство функций log |#include <math.h> float logf(float num); double log(double num); long double logl(long double num); Функции logf () и logl () добавлены в версии C99. Каждая функция семейства log () возвращает значение натурального логарифма от аргумента пит. Если значение аргумента пит отрицательно, возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определения). Если же значение пит равно нулю, возможна ошибка из-за выхода за пределы диапазона представимых значений. Пример Следующая программа выводит на экран значения натуральных логарифмов чисел от 1 до 10 (с шагом 1), т.е. составляет таблицу натуральных логарифмов целых чисел от 1 до 10. #include <math.h> #include <stdio.h> int main(void) { double val = 1.0; do { printf("%f %fn", val, log(val)); val++; } while (val<11.0); return 0; } Зависимые функции loglO() и log2() Глава 15. Математические функции 361
B Семейство функций loglp |#include <math.h> float loglpf(float num); double loglp(double num); long double loglpl(long double num); Функции loglp (), loglpf () и loglpl () добавлены в версии C99. Каждая функция семейства loglp () возвращает значение натурального логарифма от аргумента пит+1. Если значение аргумента пит отрицательно, возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения об- ласти определения). Если же значение пит равно -1, возможна ошибка из-за выхода за пределы диапазона представимых значений. Зависимые функции 1од() И Семейство функций log 10 |#include <math.h> float loglOf(float num); double loglO(double num); long double logl01(long double num); Функции loglOf () и loglOl () добавлены в версии C99. Каждая функция семейства loglOO возвращает значение логарифма по основа- нию 10 от аргумента пит. Если значение аргумента пит отрицательно, возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нару- шения области определения). Если же значение пит равно нулю, возможна ошибка из-за выхода за пределы диапазона представимых значений. Пример Данная программа выводит значение десятичных логарифмов чисел, изменяющихся от 1 до 10 с шагом 1, т.е. составляет таблицу десятичных логарифмов целых чисел от 1 до 10. #include <math.h> #include <stdio.h> int main(void) { double val - 1.0; do { printf("%f %fn", val, loglO(val)); val++; } while (val<11.0); return 0; Зависимые функции log() и log2() 362 Часть III. Стандартная библиотека С
В Семейство функций 1од2 Itfinclude <math.h> float log2f(float num); double log2(double num); long double log21(long double num); Функции log2 (), log2f () и 1од21 () добавлены в версии C99. Каждая функция семейства 1од2 () возвращает значение логарифма по основанию 2 от аргумента пит. Если значение аргумента пит отрицательно, возникает ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения об- ласти определения). Если же значение пит равно нулю, возможна ошибка из-за выхо- да за пределы диапазона представимых значений1. Зависимые функции log() и loglO () В Семейство функций logb |#include <math.h> float logbf(float num); double logb(double num); long double logbl(long double num) ; Функции logb (), logbf () и logbl () добавлены в версии C99. Каждая функция семейства logb () возвращает показатель аргумента пит. Возвращае- мое значение является числом с плавающей точкой. Если значение аргумента пит равно нулю, возможна ошибка из-за выхода за пределы диапазона представимых значений. Зависимые функции ilogb() В Семейство функций Irint Ittinclude <math.h> long int Irintf(float arg); long int Irint(double arg) ; long int lrintl(long double arg); Функции Irint (), Irintf () и Irintl () добавлены в версии C99. Каждая функция семейства Irint () возвращает значение аргумента arg, округ- ленное до ближайшего целого, которое имеет тип long int. Зависимые функции llrint () и rint () 1 Как известно, в нуле логарифм не определен, но из-за трудностей представления близких к ну- лю положительных чисел автор придерживается столь осторожных формулировок. — Прим. ред. Глава 15. Математические функции 363
В Семейство функций Iround |#include <math.h> long int Iroundf(float arg); long int Iround(double arg); long int lroundl(long double arg); Функции Iround (), Iroundf () и Iroundl () добавлены в версии C99. Каждая функция семейства Iround () возвращает значение аргумента arg. округ- ленное до ближайшего целого, которое имеет тип long int. Значения, отстоящие от большего и меньшего целых на одинаковую величину (например, число 3.5), округ- ляются в сторону большего целого. Зависимые функции llround() и round() в Семейство функций modf |#include <math.h> float modff(float num, float *1); double modf(double num, double *1) ; long double modfl(long double num, long double *1) ; Функции modff () и modf 1 () добавлены в версии C99. Каждая функция семейства modf () разбивает аргумент пит на целую и дробную части. Функция возвращает дробную часть и размещает целую часть в переменной, адресуемой параметром L Пример Данный фрагмент программы выводит на экран числа 10 и 0.123. I double i; double f; f = modf(10.123, &i); printf("%f %f", i, f); Зависимые функции frexp() И Idexp() В Семейство функций nan Itfinclude <math.h> float nanf(const char ★content); double nan(const char ★content); long double nanl(const char ★content); Функции nan (), nanf () и nanl () добавлены в версии C99. Каждая функция семейства пап () возвращает значение, которое не является чис- лом и которое содержит строку, адресуемую параметром content. 364 Часть III. Стандартная библиотека С
Зависимые функции isnan() В Семейство функций nearbyint I#include <math.h> float nearbyintf(float arg); double nearbyint(double arg); long double nearbyintl(long double arg); Функции nearbyint (), nearbyintf () и nearbyintl () добавлены в версии C99. Каждая функция семейства nearbyint () возвращает значение аргумента arg, ок- ругленное до ближайшего целого. Однако возвращаемое число представлено в форма- те с плавающей точкой. Зависимые функции rint() и round() В Семейство функций nextafter Ittinclude <math.h> float nextafterf(float from, float towards); double nextafter(double from, double towards); long double nextafterl(long double from, long double towards) ; Функции nextafter (), nextafterf () и nextafterl () добавлены в версии C99. Каждая функция семейства nextafter () возвращает значение, следующее после аргумента from, причем выбор следующего значения осуществляется в направлении, задаваемом аргументом towards. Зависимые функции nexttoward() Р—иг| В Семейство функций nexttoward Itfinclude <math.h> float nexttowardf(float from, long double towards); double nexttoward(double from, long double towards); long double nexttowardl(long double from, long double towards); Функции nexttoward (), nexttowardf () и nexttowardl () добавлены в версии C99. Каждая функция семейства nexttoward () возвращает значение, следующее после аргумента from, причем выбор следующего значения осуществляется в направлении, задаваемом аргументом towards. Действие этих функций аналогично действию функ- ций семейства nextafter () за исключением того, что параметр всех трех функций towards имеет тип long double. Зависимые функции nextafter() Глава 15. Математические функции 365
s Семейство функций pow Ittinclude <math.h> float powf(float base, float exp); double pow(double base, double exp); long double powl(long double base, long double exp); Функции powf () и powl () добавлены в версии C99. Каждая функция семейства pow() возвращает значение аргумента base, возведен- ное в степень ехр, т.е. в результате получается base™?. Если значение аргумента base равно нулю, а ехр меньше или равно нулю, возможна ошибка из-за выхода за пределы области допустимых значений (ошибка из-за нарушения области определения). Она произойдет также в том случае, если base отрицательно, а ехр не является целым чис- лом. При этом также может возникнуть ошибка из-за выхода за пределы диапазона представимых значений. Пример Следующая программа выводит первые десять степеней числа 10, т.е. составляет таблицу степеней числа 10. #include <math.h> #include <stdio.h> int main(void) { double x = 10.0, у = 0.0; do { printf("%fn”, pow(x, y)); У++; } while(y<ll.0); return 0; } Зависимые функции exp(), log() и sqrt0 В Семейство функций remainder |#include <math.h> float remainderf(float a, float b) ; double remainder(double a, double b) ; long double remainderl(long double a, long double b); Функции remainder (), remainderf () и remainderl () определены в версии C99. Каждая функция семейства remainder () возвращает остаток от деления значений аргументов а/Ь. Зависимые функции remquo() 366 Часть III. Стандартная библиотека С
В Семейство функций remquo |#include <math.h> float remquof(float a, float br int ★quo) ; double remquo(double ar double br int ★quo) ; long double remquol(long double ar long double br int ★quo) ; Функции remquo (), remquof () и remquo 1 () определены в версии C99. Каждая функция семейства remquo () возвращает остаток от деления значений аргументов а/b. При этом целое, адресуемое параметром quo, будет содержать частное. Зависимые функции remainder() В Семейство функций rint Ittinclude <math.h> float rintf(float arg); double rint(double arg); long double rint1(long double arg); Функции rint (), rintf () и rintl () добавлены в версии C99. Каждая функция семейства rint () возвращает значение аргумента arg, округлен- ное до ближайшего целого. Однако возвращаемое число представлено в формате с плавающей точкой. Может возникнуть исключение вещественного типа. Зависимые функции nearbyint () и round() В Семейство функций round |#include <math.h> float roundf(float arg); double round(double arg); long double roundl(long double arg); Функции round (), roundf () и roundl () добавлены в версии C99. Каждая функция семейства round () возвращает значение аргумента arg, ок- ругленное до ближайшего целого. Однако возвращаемое число представлено в формате с плавающей точкой. Значения, отстоящие от большего и меньшего це- лого на одинаковую величину (например, число 3.5), округляются в сторону большего целого. Зависимые функции Iround() и llround() Глава 15. Математические функции 367
В Семейство функций scalbln I#include <math.h> float scalblnf(float val, long int exp); double scalbln(double val, long int exp); long double scalblnl(long double val, long int exp); Функции scalbln (), scalblnf () и scalblnl () добавлены в версии C99. Каждая функция семейства scalbln () возвращает произведение параметра val и значения FLT_RADIX, возведенного в степень, которая равна значению параметра ехр, т.е. в результате получается val * FLT RADIX^. Макрос FLT-RADIX определен в заголовке <float.h>, и его значение равно осно- ванию системы счисления, используемой для представления вещественных чисел. Зависимые функции scalbn() В Семейство функций scalbn |#include <math.h> float scalbnf(float val, int exp); double scalbn(double val, int exp); long double scalbnl(long double val, int exp); Функции scalbn (), scalbnf () и scalbnl () добавлены в версии C99. Каждая функция семейства scalbn () возвращает произведение параметра val и значения flt_radix, возведенного в степень ехр, т.е. в результате получается val * FLT.RADIX^. Макрос FLT_RADIX определен в заголовке <float.h>, и его значение равно осно- ванию системы счисления, используемой для представления вещественных чисел. Зависимые функции scalbln() S Семейство функций sin I#include <math.h> float sinf(float arg); double sin(double arg); long double sinl(long double arg); Функции sinf () и sinl () добавлены в версии C99. Каждая функция семейства sin() возвращает значение синуса от аргумента arg. Значение аргумента должно быть задано в радианах. Пример Данная программа выводит синусы последовательности значений, лежащих в пре- делах от -1 до 1 и изменяющихся с шагом одна десятая, т.е. составляет таблицу сину- сов чисел от -1 до 1. 368 Часть III. Стандартная библиотека С
#include <math.h> #include <stdio.h> int main(void) { double val = -1.0; do { printf (’’Синус %f равен %f.n”, val, sin (val) ); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции asin(), acos (), atan2(), atan(), tan (), cos (), sinh(), cosh() HtanhO Семейство функций sinh |#include <math.h> float sinhf(float arg); double sinh(double arg); long double sinhl(long double arg) ; Функции sinhf () и sinhl () добавлены в версии C99. Каждая функция семейства sinh() возвращает значение гиперболического синуса от аргумента arg. Пример Данная программа выводит гиперболические синусы последовательности значе- ний, лежащих в пределах от -1 до 1 и изменяющихся с шагом одна десятая, т.е. со- ставляет таблицу гиперболических синусов чисел от -1 до 1. #include <math.h> #include <stdio.h> int main(void) { double val = -1.0; do { printf (’’Гиперболический синус %f равен %f.n", val, sinh(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции asin(), acos (), atan2(), atan(), tan(), cos(), cosh() Hsin() Глава 15. Математические функции 369
И Семейство функций sqrt |#include <math.h> float sqrtf(float num); double sqrt(double num); long double sqrtl(long double num); Функции sqrtf () и sqrtl () добавлены в версии C99. Каждая функция семейства sqrt() возвращает значение квадратного корня от аргумента пит. Если значение аргумента отрицательно, возникает ошибка из-за вы- хода за пределы области допустимых значений (ошибка из-за нарушения области определения). Пример Данный фрагмент программы выводит на экран число 4. | printfsqrt(16.0)); Зависимые функции exp(), log() и pow() в Семейство функций tan |#include <math.h> float tanf(float arg); double tan(double arg); long double tanl(long double arg); Функции tanf () и tanl () добавлены в версии C99. Каждая функция семейства tan () возвращает значение тангенса от аргумента arg. Значение аргумента должно быть задано в радианах. Пример Данная программа выводит тангенсы последовательности значений, лежащих в пределах от -1 до 1 и изменяющихся с шагом одна десятая, т.е. составляет таблицу тангенсов чисел от -1 до 1. #include <math.h> #include <stdio.h> int main(void) { double val = -1.0; do { printf("Тангенс %f равен %f.n", val, tan(val)); val += 0.1; } while(val<=l.0); ft return 0; } 370 Часть III. Стандартная библиотека С
Зависимые функции acos (), asin(), atan (), atan2(), cos (), sin(), sinh(), cosh () Htanh() В Семейство функций tanh |#include <math.h> float tanhf(float arg); double tanh(double arg) ; long double tanhl(long double arg) ; Функции tanhf () и tanhl () добавлены в версии C99. Каждая функция семейства tanh () возвращает значение гиперболического танген- са от аргумента arg. Пример Данная программа выводит гиперболические тангенсы последовательности значе- ний, лежащих в пределах от -1 до 1 и изменяющихся с шагом одна десятая, т.е. со- ставляет таблицу гиперболических тангенсов чисел от -1 до 1. ttinclude <math.h> ttinclude <stdio.h> int main(void) { double val = -1.0; do { printf("Гиперболический тангенс %f равен %f.n", val, tanh(val)); val += 0.1; } while(val<=l.0); return 0; } Зависимые функции acos (), asin(), atan(), atan2(), cos(), sin(), cosh(), sinh() и tan () В Семейство функций tgamma |#include <math.h> float tgammaf(float arg) ; double tgamma(double arg); long double tgammal(long double arg); Функции tgamma (), tgammaf () и tgammal () добавлены в версии C99. Каждая функция семейства tgamma () возвращает значение гамма-функции от ар- гумента arg. Зависимые функции 1gamma() Глава 15. Математические функции 371
В Семейство функций trunc |#include <math.h> float truncf(float arg}; double trunc(double arg); long double truncl(long double arg); Функции trunc (), truncf () и truncl () добавлены в версии C99. Каждая функция семейства trunc () возвращает усеченное значение аргумента arg, т.е. значение, в котором отброшена дробная часть1. Зависимые функции nearbyint() 1 Иногда говорят, что это округленное значение аргумента arg, причем округление в данном случае выполняется отбрасыванием дробной части. — Прим. ред. 372 Часть III. Стандартная библиотека С
Полный справочник по Глава 16 Функции времени, даты и локализации
В библиотеке стандартных функций несколько функций предназначено для работы с да- той и временем. В ней также определены функции, которые обрабатывают геополи- тическую информацию, связанную с программой. Приведем описание этих функций. Для использования функций времени и даты необходим заголовочный файл <time.h>. Этот файл определяет три типа данных, связанных с исчислением времени: clock_t, и tm. Типы данных clock__t и предназначены для пред- ставления системного времени и даты в виде некоторого целого значения, называе- мого календарным временем. Структурный тип tm содержит дату и время, разбитые на составляющие компоненты. Структура tm состоит из следующих членов: int tm_sec; /* секунды, 0-60 */ int tm_min; /* минуты, 0-59 */ int tm_hour; /* часы, 0-23 */ int tm_mday; /* день месяца, 1-31 */ int tni_mon; /* месяцы, начиная с января, 0-11 */ int tm_year; /* годы, начиная с 1900 */ int tm_wday; /* дни, начиная с воскресенья, 0-6 */ int tm_yday; /* дни, начиная с 1 января, 0-365 */ int tm_isdst /* индикатор летнего времени */ Значение tm_isdst положительно, если действует режим летнего времени (Daylight Saving Time), равно нулю, если не действует, и отрицательно, если информация об этом не- доступна. Такой формат представления времени и даты называется разделенным на компоненты календарным временем (broken-down time). Кроме того, в <time.h> определен макрос CLOCKS_PER_SEC, который содержит число тактов системных часов в секунду. Функции геополитического окружения описаны в заголовочном файле <locale.h>. В нем определена структура Iconv, которая приведена в описании функ- ции localeconv(). * И Функция asctime |#include <time.h> char *asctime(const struct tm *ptr); Функция asctime возвращает указатель на строку, которая содержит информацию, со- храняемую в адресуемой параметром ptr структуре и имеющую следующую форму: День_недели месяц дата часы:минуты:секунды годп Например: | Fri Apr 15 12:05:34 2005 ; Ptr указывает на структуру, заполняемую функциями localtime () или gmtime (). Буфер, используемый функцией asctime () для хранения форматированной стро- ки вывода, является статически распределенным массивом символов. Он перезаписы- вается при каждом вызове функции. Чтобы сохранить содержание строки, скопируйте ее в какую-нибудь другую область памяти. Пример Эта программа отображает местное время, определяемое системой: I #include <time.h> I #include <stdio.h> 374 Часть III. Стандартная библиотека С
int main (void) { struct tm *ptr; time_t It; It = time(NULL); ptr = localtime(<); printf(asctime(ptr)) ; return 0; * } Зависимые функции localtime (), gmtime (), time () и ctime () й Функция clock |#include ctime.h> clock_t clock(void); Функция clock () возвращает значение, которое приблизительно соответствует времени работы вызывающей программы. Для преобразования этого значения в се- кунды нужно разделить его на значение clocks_per_sec. Если системный таймер недоступен, возвращается значение -1. Пример Следующая функция отображает время выполнения вызывающей программы в секундах: Ivoid elapsed—time (void) { printf("Прошло: %u секунд.n", clock()/CLOCKS_PER_SEC) ; } Зависимые функции time (), asctime () и ctime () @ Функция ctime |#include ctime.h> char *ctime(const time_t *time) ; Функция ctime () возвращает указатель на строку, имеющую следующий вид: День месяц год часы:минуты:секунды уеагп. Функции передается указатель на календарное время. Календарное время обычно получают с помощью функции time (). Буфер, используемый ctime () для хранения форматированной строки вывода является статически распределенным массивом символов. Он перезаписывается при каждом вызове функции. Для сохранения строки скопируйте ее в какую-нибудь другую область памяти. Глава 16. Функции времени, даты и локализации 375
Пример Эта программа отображает местное время, определенное в системе: #include ctime.h> #include <stdio.h> int main(void) { time__t It; It = time(NULL); printf(ctime(<)); return 0; } Зависимые функции localtime (), gmtime (), time () и asctime () H Функция difftime |#include ctime.h> double difftime(time_t time2, time_t time!); Функция difftime () возвращает разность в секундах между значениями парамет- ров timel и time2, т.е. возвращается значение выражения time2-timel. Пример Эта программа отображает время в секундах, требуемое для выполнения пустого цикла 5 000 000 раз: #include ctime.h> #include cstdio.h> int main(void) { time_t start, end; .. volatile long unsigned t; start = time(NULL); for(t=0; tc5000000; t++) ; end = time(NULL); printf("Цикл использовал %f секунд.n", difftime(end, start)); return 0; } Зависимые функции localtime (), gmtime (), time () и asctime () 376 Часть III. Стандартная библиотека С
B Функция gmtime Ittinclude <time.h> struct tm *gmtime (const time__t *time) ; Функция gmtime () возвращает указатель на структуру tm, содержащую календар- ное время в разделенной на компоненты форме. Значение time представлено в виде так называемого координированного всемирного времени (Coordinated Universal Time, или UTC)1, которое, по сути, является средним временем по гринвичскому меридиа- ну1 2 (Greenwich mean time, GMT). Функция time О возвращает указатель time. Она возвращает NULL, если система не поддерживает координированное всемирное время. Память для структуры, в которой функция gmtime () сохраняет разделенное на компоненты время, распределяется статически. Эта структура перезаписывается при каждом вызове функции. Чтобы сохранить содержимое структуры, скопируйте ее в какую-нибудь другую область памяти. Пример Эта программа печатает местное время и координированное всемирное время (UTC) системы: #include <time.h> #include <stdio.h> /* Печать местного и координированного всемирного (UTC) времени. */ int main(void) { struct tm *local, *gm; time_t t; t = time(NULL); local = localtime(&t); printf(" Местное время и дата: %sn”, asctime(local)); gm = gmtime(&t); printf("Координированное всемирное время и дата: %s", asctime(gm)) ; return 0; } Зависимые функции localtime(), time() и asctime() Bl Функция localeconv |#include <locale.h> struct Iconv *localeconv(void) 1 Называется также всеобщим скоординированным временем и универсальным глобальным време- нем (по Гринвичу). — Прим. ред. 2 Называется также всемирным (гринвичским средним) временем или средним временем по Грин- вичу. UTC не может отличаться от GMT более чем на 0,9 с. — Прим. ред. Глава 16. Функции времени, даты и локализации 377
Функция localeconvO возвращает указатель на структуру типа Iconv, которая содержит различную информацию о геополитической среде, связанную со способом форматирования чисел. Структура iconv включает следующие члены: char decimal_point; /* Символ десятичной точки для неденежных значений. */ char thousands_sep; /* Разделитель тысяч для неденежных значений. */ char grouping; /* Определяет группирование для неденежных значений. */ char int_curr_symbol; /* Символ международной валюты. */ char currency_symbol; /* Символ местной валюты. */ char mon_decimal_point; /* Символ десятичной точки для денежных значений. */ char mon_thousands_sep; . /* Разделитель тысяч для денежных значений. */ char mon_grouping; /* Определяет группирование для денежных значений. */ char positive_sign; /* Индикатор положительных денежных значений. */ char negative_sign; /* Индикатор отрицательных денежных значений. */ char int_frac_digits; /* Количество цифр справа от десятичной точки для денежных значений, отображаемых в международном формате. */ char frac_digits; /* Количество цифр справа от десятичной точки для денежных значений, отображаемых в местном формате. */ char p_cs__precedes; /* 1 - если символ валюты предшествует положительному значению, 0 - если символ валюты следует за значением. */ char p__sep__by__space; /* 1 - если символ валюты отделяется от значения пробелом, О-в противном случае. В версии С99 содержит разделитель. */ char n_cs__precedes; /* 1 - если символ валюты предшествует отрицательному значению, 0 - если символ валюты следует за значением.*/ char n__sep_by__space; /* 1 - если символ валюты отделяется от отрицательного значения пробелом, 0 - если символ валюты следует за значением. В версии С99 содержит разделитель.*/ char p_sign_posn; /* Указывает позицию символа положительного значения. */ char n_sign_posn; /* Указывает позицию символа отрицательного значения. */ /* Следующие члены добавлены в С99. */ char _p_cs_precedes; /* 1 - если символ валюты предшествует положительному значению, 0 - если символ валюты следует за значением. Применяется для значений в международном формате. */ 378 Часть III. Стандартная библиотека С
char __p__sep__by_space; /* Разделитель между символом валюты, знаком и положительным значением. Применяется для значений в международном формате. */ char _n__cs_precedes; /* 1 - если символ валюты предшествует отрицательному значению, 0 - если ' символ валюты следует за значением. Применяется для значений в международном формате. */ char _n_sep__by_space; /* Разделитель между символом валюты, знаком и отрицательным значением. Применяется для значений в международном формате. */ char _p_sign_posn; /* Указывает позицию символа положительного значения. Применяется для значений в международном формате. */ char _n_sign_posn; /* Указывает позицию символа отрицательного значения. Применяется для значений в международном формате.*/ Функция localeconvO возвращает указатель на структуру Iconv. Следует пом- нить, что содержимое этой структуры изменять нельзя. Обратитесь к документации вашего транслятора для определения специфической информации, касающейся струк- туры Iconv. Пример Следующая программа отображает символ десятичной точки, используемый в те- кущей локализации: #include <stdio.h> #include <locale.h> int main(void) { struct Iconv 1c; 1c = *localeconv(); printf("В качестве разделителя целой и дробной части в десятичных числах используется символ %sn", 1с.decimal_point); return 0; } Родственная функция setlocale() В Функция localtime |#include <time.h> struct tm *localtime(const time_t *time); Функция localtimeO возвращает указатель на структуру типа tm, содержащую время в разделенной на компоненты форме. Время представлено как местное. Указа- тель time обычно получают с помощью функции time (). Глава 16. Функции времени, даты и локализации 379
Память для структуры, в которой localtime () сохраняет разделенное на компо- ненты время, выделяется статически. Поэтому эта структура перезаписывается при каждом вызове функции. Для сохранения содержания структуры, скопируйте ее в ка- кую-нибудь другую область памяти. Пример Эта программа печатает местное время и координированное всемирное время системы: #include <time.h> #include <stdio.h> /* Печатать местное и координированное всемирное (UTC) время. */ int main(void) { struct tm * local; t ime_t t; t = time(NULL); local = localtime(&t); printf (’’Местное время и дата: %sn", asctime(local)); local = gmtime (&t); printf (’’Координированное всемирное время (UTC) и дата: %sn”, asctime(local)); return 0; } Зависимые функции gmtime(), time() и asctime() m Функция mktime |#include <time.h> ? time__t mktime (struct tm *time) ; Функция mktime () возвращает календарный эквивалент времени, хранящийся в разделенном на компоненты виде в структуре, которая адресуется параметром- указателем time. Элементы tm__wday и tm_yday устанавливаются самой функцией, по- этому их не нужно определять при ее вызове. Если mktime () не может представить информацию в виде допустимого календар- ного времени, возвращается -1. Пример Эта программа сообщает день недели 3 января 2005 года: #include <time.h> #include <stdio.h> r int main(void) { struct tm t; time_t t_of_day; 380 Часть III. Стандартная библиотека С
t.tm_year = 2005-1900; t.tm_mon = 0; t.tm_mday = 3; t.tm_hour =0; /* Час, минута, секунда не имеют значения, */ t.tm_min =0; /* если только они не определяют переход */ t.tm_sec =1; /* на новую дату */ t.tm_isdst = 0; t_of_day = mktime(&t); printf (ctime (&t_of_day) ) ; return 0; Зависимые функции time(), gmtime(), asctime() И ctime() В Функция setlocale |#include <locale.h> char *setlocale (int type, const char ★locale); Функция setlocaleO позволяет получить или установить некоторые параметры, которые зависят от геополитической среды выполнения программы. Если указатель locale является нулем, функция setlocale () возвращает указатель на строку текущей локализации. В противном случае функция setlocaleO попытается использовать строку locale для установки локальных параметров в соответствии с параметром type. Для задания стандартных С-параметров региональной привязки используйте строку "С", а для задания собственных параметров среды — пустую строку Чтобы полу- чить подробную информацию о строках локализации, поддерживаемых конкретным компилятором, обратитесь к документации. При вызове функции setlocale () в качестве параметра type должен быть исполь- зован один из следующих макросов (определенных в заголовке clocale.h >). LC_ALL LC_COLLATE LC_CTYFE LC_MONETARY LC_NUMERIC LC_TIME Макрос LC—ALL относится ко всем категориям локализации. Макрос LC_COLLATE ока- зывает влияние на выполнение функции strcoll (). Макрос LC_CTYPE изменяет характер работы символьных функций. Макрос lc_monetary определяет денежный формат. Мак- рос LC_NUMERIC изменяет символ десятичной точки для функций форматированного вво- да-вывода. Наконец, макрос LC_TIME определяет поведение функции strftime (). Функция setlocale () возвращает указатель на строку, связанную с параметром type. Пример Эта программа отображает текущую установку локализации: I#include <locale.h> I#include <stdio.h> Глава 16. Функции времени, даты и локализации 381
int main(void) { printf(setlocale(LC_ALL, "")); return 0; Зависимые функции localeconv (), time(), strcoll () и strftimeO HI Функция strftime |#include <time.h> size_t strftime(char *str, size_t maxsize, const char *fmtf const struct tm ★time); Функция strftimeO помещает информацию о времени и дате (вместе с другой информацией) в строку, адресуемую параметром str, в соответствии с командами фор- матирования, которые содержатся в адресуемой параметром fmt строке. Эта функция использует разделенное на компоненты время, на которое указывает указатель time. В строку str будет помещено не более maxsize символов. В версии С99 к параметрам str, fmt и time применен квалификатор restrict. Работа функции strftime () напоминает работу функции sprintf () в том, что она распознает набор команд форматирования, которые начинаются со знака процен- та (%), и помещает отформатированный результат в строку. Команды форматирова- ния используются для задания точного способа представления различных данных времени и даты в параметре str. Любые другие символы, содержащиеся в строке фор- матирования, помещаются в строку str без изменений. Время и дата отображаются по местному времени. Команды форматирования перечислены в следующей таблице. Обратите внимание на то, что во многих командах прописные и строчные буквы имеют различную интерпретацию. Функция strftimeO возвращает количество символов, помещенных в строку, адресуемую параметром str, или нуль при возникновении ошибки. Код Замещается__________________________________________________________________ %а Сокращенное название дня недели %А Полное название дня недели %Ь Сокращенное название месяца %В Полное название месяца %с Стандартная строка даты и времени %С Две последние цифры года %d День месяца в виде десятичного числа (1-31) %D Дата в виде месяц/день/год (добавлено в версии С99) %е День месяца в виде десятичного числа (1-31) в двух-символьном поле (добавлено в С99) %F Дата в виде “год-месяц-день" (добавлено в С99) %д Последние две цифры года с использованием понедельного года (добавлено в С99) %G Год с использованием понедельного года (добавлено в С99) %h Сокращенное название месяца (добавлено в С99) %Н Час (0-23) %j Час (1-12) %j День года в виде десятичного числа (1 -366) 382 Часть III. Стандартная библиотека С
Код Замещается___________________________________________________________________ %м Месяц в виде десятичного числа (1-12) %М Минуты в виде десятичного числа (0-59) %п Разделитель строк (добавлено в С99) %р Местный эквивалент ДМ (до полудня) или РМ (после полудня) %г 12-часовое время (добавлено в С99) %R Время в виде чч:мм (добавлено в С99) %S Секунды в виде десятичного числа (0-60) %Т Горизонтальная табуляция (добавлено в С99) %Т Время в виде чч:мм:сс (добавлено в С99) %и День недели; понедельник — первый день недели (0-6) (добавлено в С99) %U Неделя года; воскресенье — первый день недели (0-53) %V Неделя года с использованием понедельного года (добавлено в С99) %w День недели в виде десятичного числа (0-6, воскресенье — 0-й день) %W Неделя года; понедельник — первый день недели (0-53) %х Стандартная строка даты %Х Стандартная строка времени %у Год в виде десятичного числа без столетия (0-99) %У Год в виде десятичного числа, включающего столетие %z Сдвиг относительно координированного всемирного (UTC) времени (добавлено в С99) %Z Название часового пояса %% Знак процента Версия С99 позволяет использовать в функции strftimeO определенные команды форматирования с модификаторами Е и О. Модификатор Е может модифицировать такие команды, как с, с, х, х, у, Y, d, е и н. Модификатор о может модифицировать команды: I, m, М, S, u, и, V, w, W и у. Использование этих модификаторов приводит к альтернатив- ному представлению отображаемого времени и/или даты. За подробностями обращайтесь к документации, поставляемой вместе с используемым вами компилятором. : Понедельный год используется командами форматирования %g, %G и %v. При та- ком представлении первым днем недели является понедельник, а первая неделя года должна включать день с датой “4 января". Пример Предположим, что itime указывает на структуру, которая содержит 10:00:00 AM. Следующая программа печатает: “Сейчас 10 AM.”: ttinclude <time.h> ' ttinclude <stdio.h> int main(void) { struct tm *ptr; time_t It; char str[80]; It = time(NULL); ptr = localtime(<) ; strftime (str, 100, "Сейчас: %H %p.", ptr); printf(str); return 0; } Глава 16. Функции времени, даты и локализации 383
Зависимые функции time (), localtime () и gmtime () В Функция time |#include <time.h> time_t time(time_t *time); Функция time () возвращает текущее календарное время системы. Если в системе отсчет времени не производится, возвращается значение -1. Функцию time () можно вызывать либо с нулевым указателем, либо с указателем на переменную типа time_t. В последнем случае этой переменной будет присвоено календарное время. Пример Эта программа отображает местное время, определенное системой: #include <time.h> #include <stdio.h> int main(void) { struct tm *ptr; time_t It; It = time (NULL); ptr = localtime(<); printf(asctime(ptr)); return 0; } Зависимые функции localtime() , gmtime(), strftime() и ctime() 384 Часть III. Стандартная библиотека С
Полный справочник по Глава 17 Функции динамического распределения памяти
В этой главе описаны функции динамического распределения памяти в языке С. Основные среди них — malloc () и free(). При каждом вызове malloc () рас- пределяется часть остающейся свободной памяти. Каждый вызов free () возвращает память системе. Область свободной памяти, в которой распределяется память, назы- вается динамически распределяемой областью памяти или кучей (heap). Прототипы функций динамического распределения памяти находятся в <stdlib.h>. Обзор динамического распределения памяти вы найдете в главе 5. На заметку В стандарте языка С определено четыре функции динамического распределения памяти, которые поддерживаются всеми трансляторами: calloc (), malloc (), free () и realloc (). Однако конкретный транслятор почти наверняка содержит несколько версий этих функций, в которых учтены различные возможности и особенности сре- ды. Например, с трансляторами, генерирующими код для сегментированной модели памяти процессора 8086, поставляются специфические функции распределения. Для получения подробных сведений и описания дополнительных функций распределения памяти обратитесь к документации по компилятору. Н Функция calloc |#include <stdlib.h> void *calloc(size_t num, size_t size); Функция calloc () выделяет память, размер которой равен значению выражения num * size, т.е. память, достаточную для размещения массива, содержащего пит объек- тов размером size. Все биты распределенной памяти инициализируются нулями. Функция calloc () возвращает указатель на первый байт выделенной области па- мяти. Если для удовлетворения запроса нет достаточного объема памяти, возвращает- ся нулевой указатель. Перед попыткой использовать распределенную память важно проверить, что возвращаемое значение не равно нулю. Пример Эта функция возвращает указатель на динамически распределенный блок памяти для массива из 100 чисел типа float: #include <stdlib.h> #include <stdio.h> float *get_mem(void) { float *p; p = calloc(100, sizeof(float)); if(!p) { printf("Ошибка при распределении памятип"); exit (1); } return p; Зависимые функции free(), malloc() и realloc () 386 Часть III. Стандартная библиотека С
В Функция free Ittinclude <stdlib.h> void free(void *ptr); Функция free () возвращает в динамически распределяемую область памяти блок памяти, адресуемый указателем ptr, после чего эта память становится доступной для выделения в будущем. Обязательно следите, чтобы free () вызывалась только с указателем, который был ранее получен в результате вызова одной из системных функций динамического распре- деления. Использование недопустимого указателя при вызове, скорее всего, приведет к разрушению механизма управления памятью и, возможно, вызовет крах системы. При передаче нулевого указателя функция f гее () не выполняет никакого действия. Пример Эта программа распределяет блок памяти для вводимых пользователем строк, а за- тем освобождает блок памяти: tfinclude <stdlib.h> #include <stdio.h> int main(void) { char *str[100]; int i; for(i=0; i<100; i++) { if((str[i] = malloc(128))==NULL) { printf("Ошибка при распределении памятип"); exit(1); } gets(str[i]); } /* Освобождение блока памяти */ for(i=0; i<100; i++) free(str[i]); return 0; } Зависимые функции calloc(), malloc() И realloc() И функция malloc |#include <stdlib.h> void *malloc(size_t size}; Функция mallocO возвращает указатель на первый байт области памяти разме- ром size, которая была выделена из динамически распределяемой области памяти. Ес- ли для удовлетворения запроса в динамически распределяемой области памяти нет достаточного объема памяти, возвращается нулевой указатель. Перед попыткой ис- Глава 17. Функции динамического распределения памяти 387
пользовать выделенную память всегда проверяйте, что возвращаемое значение не яв- ляется нулевым указателем. Попытка использовать нулевой указатель обычно приво- дит к полному отказу системы. Пример Эта функция выделяет память для структуры типа addr: struct addr { char name[40]; char street[40]; char city[40]; char state[3]; char zip [10]; }; struct addr *get_struct(void) ( struct addr *p; if((p = malloc(sizeof(struct addr)))==NULL) { printf("Ошибка при распределении памятип"); exit(1); } return p; } Зависимые функции free(), realloc() и calloc() H Функция realloc Ittinclude <stdlib.h> void *realloc(void ★ptr, size_t size); Действие функции realloc () в версии C99 немного отличается от ее работы в С89, хотя конечный результат одинаков. В С89 функция realloc () изменяет размер блока ранее выделенной памяти, адресуемой указателем ptr в соответствии с заданным размером size. Значение параметра size может быть больше или меньше, чем перерас- пределяемая область. Функция realloc () возвращает указатель на блок памяти, по- скольку не исключена необходимость перемещения этого блока (например при увели- чении размера блока памяти). В этом случае содержимое старого блока (до size бай- тов) копируется в новый блок. В версии С99 блок памяти, адресуемый параметром ptr, освобождается, а вместо него выделяется новый блок. Содержимое нового блока совпадает с содержимым ис- ходного (по крайней мере совпадают первые size байтов). Функция возвращает указа- тель на новый блок. Причем допускается, чтобы новый и старый блоки начинались с одинакового адреса (т.е. указатель, возвращаемый функцией realloc О, может сов- падать с указателем, переданным в параметре ptr). Если указатель ptr нулевой, функция realloc () просто выделяет size байтов памя- ти и возвращает указатель на эту память. Если значение параметра size равно нулю, память, адресуемая параметром ptr, освобождается. 388 Часть III. Стандартная библиотека С
Если в динамически распределяемой области памяти нет достаточного объема сво- бодной памяти для выделения size байтов, возвращается нулевой указатель, а исход- ный блок памяти остается неизменным. Пример Эта программа сначала выделяет блок памяти для 17 символов, копирует в них строку ’’Это - 17 символов’’, а затем использует realloc () для увеличения размера блока до 18 символов, чтобы разместить в конце точку. #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { char *p; p = malloc(17) ; if(!p) { printf("Ошибка при распределении памятиХп"); exit (1); } strcpy(p, "Это - 17 символов"); р = realloc(р, 18); if dp) { printf("Ошибка при распределении памятиХп"); exit (1); } strcat(р, "."); printf(р); free(р); return 0; } Зависимые функции free (), malloc () И calloc () Глава 17. Функции динамического распределения памяти 389
олный справочник по Глава 18 Служебные функции
В библиотеке стандартных функций определен целый ряд так называемых слу- жебных функций. Они осуществляют различные преобразования, обрабаты- вают списки аргументов переменной длины, выполняют сортировку и поиск, а также генерируют случайные числа. Многие из этих функций описаны в заголо- вочном файле <stdlib.h>. В этом заголовке объявлены типы div_t и ldiv_t. Значения этих типов возвращаются функциями div() и IdivQ соответственно. В С99 добавлены тип lldiv_t и функция lldiv(). Здесь также объявлены типы size_t и wchar_t и определены следующие макрокоманды: Макрос Значение MB_CUR_MAX NULL RAND_MAX EXIT_FAILURE Максимальная длина (в байтах) многобайтового символа Нулевой указатель Максимальное значение, которое может возвратить функция rand () Значение, возвращаемое вызывающему процессу при неудачном завершении программы EXIT_SUCCESS Значение, возвращаемое вызывающему процессу при успешном завершении программы Если для вызова некоторой функции необходимо использовать заголовок, отлич- ный от <stdlib.h>, об этом будет специально указано в описании функции. И Функция abort |#include <stdlib.h> void abort(void); Функция abort () вызывает немедленное аварийное завершение программы. Как пра- вило, буфера файлов не дозаписываются. В средах, которые поддерживают эту функцию, она возвращает вызывающему процессу (обычно им является операционная система) зна- чение (определяемое конкретной реализацией), которое сигнализирует об отказе. Пример Эта программа заканчивается, если пользователь вводит А: #include <stdlib.h> #include <stdio.h> int main(void) { for (;;) if(getchar()== ’A*) abort(); return 0; } Зависимые функции exit() и atexit() 392 Часть III. Стандартная библиотека С
И Функция abs Ittinclude <stdlib.h> int abs(int num); Функция abs () возвращает абсолютное значение целочисленного аргумента пит. Пример Эта функция преобразует введенное пользователем число в его абсолютное значение: int get_abs(void) { char num[80]; gets(num); return abs(atoi(num)); } Зависимая функция fabs() Ц Функция-макрос assert |#include <assert.h> void assert(int exp); Макрос assert (), определенный в заголовке <assert.h>, записывает информа- цию об ошибке в поток stderr, а затем прекращает выполнение программы, если выражение ехр равно нулю. В противном случае макрос assert () никаких действий не выполняет. Хотя формат выводимого сообщения зависит от конкретной реализа- ции системы программирования, большинство трансляторов используют сообщение, подобное следующему: | Assertion failed: <выражение>, file <имя_файла>, line <номер_строки> В версии С99 отображаемое сообщение также включает имя функции, содержащей макрос assert (). Макрос assert () обычно используется, чтобы убедиться в правильном выполне- нии программы, причем выражение составляется таким образом, что оно истинно только при отсутствии ошибок. Нет необходимости удалять из исходного текста программы операторы assert О после отладки программы, потому что если определить макрос NDEBUG, то макрос assert () будет игнорироваться. Пример Этот фрагмент кода проверяет, является ли данное, прочитанное из последова- тельного порта, ASCII-символом (то есть, не используется ли седьмой бит): I/* ... */ ch = read_port() ; assert (! (ch & 128)); /*проверяет 7-й бит */ Глава 18. Служебные функции 393
Зависимая функция abort (). S Функция atexit i#include <stdlib.h> int atexit(void (★func) (void)); Функция atexit () регистрирует функцию, на которую указывает June, как функцию, вызываемую при нормальном завершении программы. (Иными словами, при нормальном завершении программы будет вызвана функция, адресуемая параметром June.) Функция atexit () возвращает нуль, если задаваемая функция успешно зарегистрирована в качест- ве функции завершения, а в противном случае она возвращает ненулевое значение. Вообще может быть зарегистрировано до 32 функций завершения, которые будут вызываться в порядке, обратном порядку регистрации (т.е. функция, зарегистриро- ванная последней, выполнится первой). Пример Эта программа печатает "Привет здесь" на экране при ее завершении: #include <stdlib.h> #include <stdio.h> void done(void); int main(void) { if(atexit(done)) printf ("Ошибка в atexit ()."); return 0; } void done(void) { printf("Привет здесь"); } Зависимые функции exit() и abort() м Функция atof |#include <stdlib.h> double atof(const char ★str); Функция atof () преобразует строку, адресуемую параметром str, в значение типа double. Эта строка должна содержать допустимое число с плавающей точкой. В про- тивном случае возвращаемое значение не определено. После числа может следовать любой символ, который не может быть частью до- пустимого числа с плавающей точкой. Имеются в виду пробелы, символы табуляции и пустой строки, знаки препинания (но не точки) и символы, отличные от буквы “Е” или “е”. Это значит, что, если функция atof() вызывается с аргументом "100. OOHELLO", будет возвращено значение 100.00. 394 Часть III. Стандартная библиотека С
Пример Эта программа читает два числа с плавающей точкой и выводит их сумму: #include <stdlib.h> #include <stdio.h> int main(void) { char numl[80], num2[80]; printf("Введите первое число: "); gets(numl); printf("Введите второе число: ”); gets (num2); printf("Сумма: %lf.", atof(numl) + atof(num2)); return 0; Зависимые функции atoi() и atol(). В Функция atoi |#include <stdlib.h> int atoi(const char *str) ; Функция atoi () преобразует строку, адресуемую параметром str, в значение типа int. Эта строка должна содержать допустимое целое число. В противном случае воз- вращаемое значение не определено. После числа может следовать любой символ, который не может быть частью целого числа. Имеются в виду пробелы, символы табуляции и пустой строки, знаки препинания и буквы. Это значит, что, если функция atoi () вызывается с аргументом ”123.23”, будет возвращено целое значение 123, а подстрока ”.23" будет проигнорирована. Пример Следующая программа считывает два целых числа и выводит их сумму: #include <stdlib.h> #include <stdio.h> int main(void) { char numl[80], num2[80]; printf("Введите первое число: "); gets (numl); printf("Введите второе число: "); gets(num2); printf("Сумма: %d.", atoi(numl)+atoi (num2)) ; return 0; } Глава 18. Служебные функции 395
Зависимые функции atof() и atol(). И Функция atol |#include <stdlib.h> long int atol(const char ★str); Функция atol () преобразует строку, адресуемую параметром str, в значение типа long int. Эта строка должна содержать допустимое целое число. В противном случае возвращаемое значение не определено. После числа может следовать любой символ, который не может быть частью целого числа. Имеются в виду пробелы, символы табуляции и пустой строки, знаки препинания и буквы. Это значит, что, если функция atoi() вызывается с аргументом ”123.23”, будет возвращено длинное целое значение 123L, а подстрока ". 23” будет проигнорирована. Пример Следующая программа считывает два целых числа в виде строк, преобразует их в два длинных целых числа и выводит их сумму: ttinclude <stdlib.h> ttinclude <stdio.h> int main(void) { char numl[80], num2[80]; printf("Введите первое число: ”); gets(numl); printf("Введите второе число: ”); gets(num2); printf("Сумма: %ld.", atol(numl)+atol(num2)); return 0; } Зависимые функции atof(), atoi() и atoll() Bil Функция atoll |#include <stdlib.h> long long int atoll(const char ★str); Функция atoll () добавлена в версии C99. Функция atoll () преобразует строку, адресуемую параметром str, в значение ти- па long long int. В остальном она аналогична функции atol (). Зависимые функции atof(), atoi () и atol() 396 Часть III. Стандартная библиотека С
IS Функция bsearch 1#include <stdlib.h> void *bsearch(const void ★key, const void ★buf, size_t num, size_t size, int (*compare) (const void ★, const void *)); Функция bsearch () выполняет двоичный поиск в отсортированном массиве, адресуе- мом параметром buf, и возвращает указатель на первый член, который совпадает с иско- мым ключом-значением, адресуемым параметром key. Количество элементов в массиве за- дается параметром пит, а размер (в байтах) каждого элемента — параметром size. Для сравнения каждого элемента массива с ключом-значением используется функция, адресуемая параметром compare. Функция compare должна иметь следующее определение. | int func_name(const void ★argl, const void ★arg2); Она должна возвращать значения, описанные в следующей таблице. Сравнение__________________________Возвращаемое значение__________________ агд1 меньше чем агд2 Меньше нуля агд1 равен агд2 Нуль агд1 больше чем агд2 Больше нуля Массив должен быть отсортирован в порядке возрастания, чтобы по самому младшему адресу содержался наименьший элемент. Если массив не содержит искомого ключа- значения, возвращается нулевой указатель. Пример Следующая программа считывает вводимые с клавиатуры символы и определяет, входят ли они в алфавит: #include <stdlib.h> #include <ctype.h> #include <stdio.h> char *alpha = "abcdefghijklmnopqrstuvwxyz"; int comp(const void *ch, const void *s); int main(void) { char ch; char *p; printf("Введите символ: "); ch = getchar(); ch = tolower(ch); p = (char *) bsearch(&ch, alpha, 26, 1, comp); if(p) printf(" %c находится в алфавитеп", *p); else printf("не входит в алфавитп"); return 0; } /* Сравнивает два символа. */ int comp(const void *ch, const void *s) { return *(char *)ch - *(char *)s; } Глава 18. Служебные функции 397
Зависимая функция qsort() И Функция div |#include <stdlib.h> div_t div(int numerator, int denominator); Функция div () возвращает в структуре типа div_t частное и остаток, полученные в ре- зультате выполнения операции деления числителя numerator на знаменатель denominator. Структура типа div_t имеет следующие два поля. Iint quot; /* частное */ int rem; /* остаток */ Пример Эта программа выводит частное и остаток от деления 10 на 3: ttinclude <stdlib.h> #include <stdio.h> int main(void) { div_t n; n = div(10, 3); printf("Частное и остаток: %d %d.n", n.quot, n.rem); return 0; } Зависимые функции ldiv() и lldiv() S Функция exit Ittinclude <stdlib.h> void exit(int exit_code); Функция exit () вызывает немедленное нормальное завершение программы. Это зна- чит, что вызываются функции завершения, зарегистрированные функцией atexit (), и любые открытые файлы после дозаписи буферов в них закрываются. В вызывающий процесс (обычно это операционная система) передается значе- ние параметра exitcode, если в данной среде предусмотрена поддержка возмож- ных значений. По соглашению, если параметр exit code равен нулю или значению EXIT SUCCESS, предполагается нормальное завершение программы. Ненулевое значение, или значение EXIT FAILURE, используется для индикации ошибки, оп- ределенной конкретной реализацией. 398 Часть III. Стандартная библиотека С
Пример Эта программа обработки списка рассылки позволяет пользователю сделать выбор из меню. Программа завершается, если введена буква Q. int menu(void) { char choice; do { printf("Ввод имени (E)n"); printf("Удаление имени (D)n"); printf("Печать (P)n"); printf("Выход (Q)n"); choice = getchar(); } while(!strchr("EDPQ", toupper(choice))); if(choice==’Q') exit(O); return choice; } Зависимые функции atexit (), abort () и __Exit () В Функция .Exit |#include <stdlib.h> void _Exit (int exit__code) ; Функция -Exit () добавлена в версии C99. Действие функции _Exit () аналогично действию функции exit () за исключени- ем следующих моментов: Не вызываются функции завершения, зарегистрированные функцией atexit (). Не вызываются обработчики сигналов, зарегистрированные функцией signal (). Не всегда закрываются открытые файлы и, возможно, они не дозаписываются. Зависимые функции atexit(), abort() И exit() В Функция getenv |#include <stdlib.h> char *getenv(const char ★name); Функция getenv () возвращает указатель на данные о среде, которые хранятся в строке, адресуемой параметром пате в таблице характеристик среды, определенной конкретной реализацией. Ваша программа не должна изменять значения, хранящиеся в этой таблице. Глава 18. Служебные функции 399
Среда программы может включать такие данные, как пути и подключенные устройства. Формат данных определяется конкретной реализацией, поэтому для уточнения деталей необходимо обратиться к руководству пользователя, прилагаемому к компилятору. Если при вызове функции getenv () значение аргумента не совпадает ни с одним из данных в описании среды, возвращается нулевой указатель. Пример Предположим, что определенный компилятор поддерживает информацию среды относительно устройств, подключенных к системе, тогда следующий фрагмент воз- вращает указатель на список устройств: I char *р; I /* ... */ |р = getevn (’’DEVICES”) ; Зависимая функция system() Н| функция labs Itfinclude <stdlib.h> long labs(long num); Функция labs () возвращает абсолютное значение аргумента num. Пример Приведенная ниже функция преобразует введенное с клавиатуры число в его абсо- лютное значение: long int get_labs() { char num[80]; gets(num); return labs(atoi(num)); } Зависимые функции abs() И llabs() I Функция llabs |#include <stdlib.h> long long int llabs(long long int num); Функция llabs () добавлена в версии C99. Функция llabs () возвращает абсолютное значение аргумента пит. Она аналогич- на функции labs (), но работает со значениями типа long long int. 400 Часть III. Стандартная библиотека С
Зависимые функции abs() и labs() В Функция Idiv |#include <stdlib.h> ldiv__t Idiv (long int numerator, long int denominator); Функция Idiv () возвращает частное и остаток, полученные в результате деления чис- лителя numerator на знаменатель denominator, в структуре типа ldiv_t. Структура типа ldiv_t имеет следующие два поля. Ilong int quot; /* частное */ long int rem; /* остаток */ Пример Следующая программа выводит частное и остаток от деления 10 на 3: #include <stdlib.h> #include <stdio.h> int main(void) { ldiv_t n; n = Idiv(10L, 3L) ; printf("Частное и остаток: %ld %ld.n", n.quot, n.rem); return 0; } Зависимые функции div () и lldiv () H Функция lldiv |#include <stdlib.h> lldiv_t lldiv(long long int numerator, long long int denominator); Функция lldiv () добавлена в версии C99. Функция lldiv () возвращает в структуре типа lldiv_t частное и остаток, полученные в результате деления числителя numerator на знаменатель denominator. Функция lldiv () аналогична функции Idiv (), но работает со значениями типа long long int. Структура типа lldiv_t имеет следующие два поля: Ilong long int quot; /* частное */ long long int rem; /* остаток */ Зависимые функции div () и Idiv () Глава 18. Служебные функции 401
HI Функция longjmp |#include <setjmp.h> void longjmp(jmp_buf envbuf, int status); Функция longjmp () возобновляет выполнение программы с места последнего обра- щения к функции set jmp (). Таким образом, функции longjmp () и set jmp () предос- тавляют средство передачи управления между функциями. Обратите внимание на необхо- димость включения заголовка <set jmp. h>. Функция longjmp () восстанавливает состояние стека, сохраненное в буфере envbuf с помощью функции set jmp (). В результате выполнений программы возобновляется с оператора, следующего за вызовом функции setjmpO. Иначе говоря, компьютер “вводится в заблуждение”: “он считает”, будто управление программой не выходило за пределы функции, которая вызвала функцию setjmpO. (Выражаясь образно, функция longjmp () подобна многомерной машине пространства-времени. Она позволяет пу- тешествовать во времени, не соблюдая какой бы то ни было последовательности со- бытий: с ее помощью можно вернуться в “покинутый мир”, не обращая внимания на то, что предварительно должен был быть произведен выход из вызванных функций. С ее помощью можно “вернуться домой”, минуя промежуточные пункты. Она “искривляет” время и пространство (памяти) так, что с ее помощью можно попасть в покинутую точку программы, не выполняя нормальный процесс возврата из функции.) Буфер evnbuf имеет тип jmp_buf, который определен в заголовке <setjmp.h>. Этот буфер должен быть заполнен в результате обращения к функции setjmpO еще до вызова функции longjmp (). Значение параметра status становится возвращаемым значением функции setjmpO, и оно используется для того, чтобы определить “происхождение длинного перехода”. Единственным недопустимым значением является нуль. Функция setjmpO возвращает нуль в том случае, когда она вызывается непосредственно про- граммой, а не косвенно, т.е. путем выполнения функции longjmp (). Функция longjmp () используется в основном для возврата из глубоко вложенного на- бора функций при возникновении ошибок. Пример Эта программа печатает 12 3: #include <setjmp.h> #include <stdio.h> jmp_buf ebuf; void f2(void); int main(void) { int i; printf("1 "); i = setjmp(ebuf); if(i == 0) { f 2 () ; printf("Это не будет напечатано."); } printf("%d", i); return 0; 402 Часть III. Стандартная библиотека С
} void f2(void) { printf(”2 ”); longjmp(ebuf, 3); } Зависимая функция setjmp() SI Функция mblen Ittinclude <stdlib.h> int mblen(const char ★str, size_t size); Функция mblen () возвращает длину (в байтах) многобайтового символа, адресуе- мого параметром str. Учету подлежат только первые size символов. При ошибке функ- ция возвращает значение -1. Если указатель str нулевой, функция mblen () возвращает ненулевое значение в слу- чае, когда многобайтовые символы имеют кодировку, зависящую от территориально- языковых особенностей. В противном случае возвращается нуль. Пример Этот оператор отображает размер многобайтового символа, адресуемого указателем mb: | printf(”%d", mblen(mb, 2)); Зависимые функции mbtowc () HwctombO S Функция mbstowcs |#include <stdlib.h> size_t mbstowcs(wchar_t ★out, const char *in, size_t size); Функция mbstowcs () преобразует многобайтовую строку, адресуемую параметром in, в строку, состоящую из двухбайтовых символов, и помещает результат в массив, ад- ресуемый параметром out. В массиве out будет сохранено в памяти только size байтов. В версии С99 к параметрам out и in применен квалификатор restrict. Функция mbstowcs () возвращает количество преобразованных многобайтовых символов. При возникновении ошибки функция возвращает значение -1. Пример Этот оператор преобразует первые четыре символа в многобайтовой строке, адре- суемой указателем mb, и помещает результат в str. | mbstowcs(str, mb, 4); Зависимые функции wcstombs() И mbtowc() Глава 18. Служебные функции 403
Bl Функция mbtowc Ittinclude <stdlib.h> int mbtowc(wchar_t ★out, const char ★in, size_t size); Функция mbtowc () преобразует многобайтовый символ, который содержится в массиве, адресуемом параметром /л, в его двухбайтовый эквивалент и помещает ре- зультат в объект, адресуемый параметром out. Преобразованию подлежат только пер- вые size символов. В версии С99 к параметрам out и in применен квалификатор restrict. Функция возвращает количество байтов, помещенных в объект out. При возник- новении ошибки возвращается значение -1. Если указатель in нулевой, функция mbtowc () возвращает ненулевое значение в случае, когда многобайтовые символы имеют кодировку, зависящую от территориально-языковых особенностей. В про- тивном случае возвращается нуль. Пример Этот оператор преобразует многобайтовый символ в mbstr в его двухбайтовый эк- вивалент символа и помещает результат в массив, адресуемый указателем widenonn. (Преобразуются только первые 2 байта mbstr.) | mbtowc(widenonn, mbstr, 2); Зависимые функции mblen() и wctomb() И Функция qsort Ittinclude <stdlib.h> void qsort(void ★buf, size_t num, size_t size, int (*compare) (const void ★, const void *)); Функция qsort () сортирует массив, адресуемый параметром-указателем buf. (Для сортировки используется алгоритм быстрой сортировки (алгоритм quicksort), разрабо- танный Ч.Э.Р. Хоаром (C.A.R. Ноаге). Быстрая сортировка считается наилучшим ал- горитмом сортировки общего назначения.) Количество элементов в массиве задается параметром пит, а размер (в байтах) каждого элемента — параметром size. Для сравнения двух элементов массива используется функция, передаваемая через параметр compare. Функция compare должна иметь следующее описание. | int func_name(const void ★argl, const void ★arg2); Она должна возвращать значения, описанные ниже. Сравнение___________________________Возвращаемое значение__________________ агд1 меньше агд2 Меньше нуля агд1 равен агд2 Нуль агд1 больше агд2 Больше нуля Массив сортируется в порядке возрастания, т.е. по самому младшему адресу будет запи- сан наименьший элемент. 404 Часть III. Стандартная библиотека С
Пример Следующая программа сортирует список целых чисел и выводит результат: #include <stdlib.h> ^include <stdio.h> int num[10] = { 1, 3, 6, 5, 8, 7, 9, 6, 2, 0 }; int comp(const void ★, const void *); int main(void) { int i; printf (’’Исходный массив: ”) ; for(i=0; i<10; i++) printf("%d ", num[i]); gsort (num, 10, sizeof(int), comp); printf("Отсортированный массив: "); for(i=0; i<10; i++) printf("%d ", num[i]); return 0; } /* Сравнение целых */ int comp(const void *i, const void *j) { return *(int *)i - *(int *)j; } Зависимая функция bsearch() Сортировка в убывающем порядке Функция-параметр compare фактически определяет порядок, используемый при сорти- ровке. Задавая с ее помощью различные порядки на сортируемом множестве, можно полу- чить различные упорядочения исходного массива. Например, чтобы отсортировать массив в порядке убывания (т.е. от большего к меньшему), необходимо в этой функции опреде- лить обратный (т.е. дуальный или двойственный) порядок. Для этого достаточно опреде- лить функцию, лишь знаком отличающуюся от исходной. Это можно сделать, например, так: comparel(x,y) = сотраге(урс) или так: comparel(x,y) = - сотраге(ху)1. S Функция raise |#include <signal.h> int raise(int signal); Функция raise () посылает выполняемой программе сигнал, заданный параметром signal. При успешном выполнении возвращается нуль, в противном случае — ненулевое значение. Заметьте: функция использует заголовок <signal.h>. 1 Раздел добавлен редактором перевода. — Прим. ред. Глава 18. Служебные функции 405
Стандартом языка С определены следующие сигналы (не исключено, что конкрет- ный компилятор поддерживает и некоторые дополнительные сигналы). Макрос Значение SIGABRT Аномальное завершение работы программы SIGFPE Ошибка при выполнении действий над вещественными числами SIGILL Недопустимая инструкция SIGINT Пользователь нажал комбинацию клавиш <Ctrl+C> SIGSEGV Неразрешенный доступ к памяти SIGTERM Прекратить выполнение программы Зависимая функция signal () В функция rand |#include <stdlib.h> int rand(void); Функция rand () генерирует последовательность псевдослучайных чисел. При ка- ждом обращении к функции возвращается целое в интервале между нулем и значени- ем rand_max, которое в любой реализации должно быть не меньше числа 32 767. Пример Следующая программа отображает 10 псевдослучайных чисел: #include <stdlib.h> #include <stdio.h> int main(void) { int i ; for(i=0; i<10; i++) printf(”%d ", rand()); return 0; } Зависимая функция srand() И Функция setjmp |#include <setjmp.h> int setjmp(jmp_buf envbuf); Макрос set jmp () сохраняет содержимое системного стека в буфере envbuf для исполь- зования в будущем с помощью функции long jmp (). Макрос использует заголовок <setjmp.h>. 406 Часть III. Стандартная библиотека С
Макрос-функция setjmpO при инициализации возвращает нуль. Однако longjmpO передает аргумент функции set jmp (), и именно его значение (всегда отличное от ну- ля), станет значением set jmp () после вызова long jmp (). Таким образом, если макрос set jmp () выполняется после вызова функции long jmp (), он возвращает значение аргумен- та, переданного ему функцией longjmpO. Дополнительная информация приведена в описании longjmp. Зависимая функция longjmp() В Функция signal Ittinclude <signal.h> void (*signal (int signal, void (★func) (int))) (int) ; Функция signal () регистрирует функцию, переданную через параметр-указатель June, в качестве обработчика сигнала, указанного параметром signal. Это означает, что функция, переданная через указатель-параметр June, будет вызвана тогда, когда программа получит сигнал signal. Для использования signal () требуется включить заголовок <signal.h>. Значением параметра June может быть адрес функции обработчика сигнала или один из следующих макросов, определенных в заголовке <signal.h>. Макрос________Значение_____________________________________________________ sig_dfl Использовать стандартную обработку сигнала sig ign Игнорировать сигнал данного типа Если используется адрес функции, то при получении сигнала будет выполнен за- данный обработчик. Для получения дополнительных сведений обратитесь к докумен- тации, поставляемой с компилятором. При успешном выполнении функция signal () возвращает адрес ранее определенной функции-обработчика данного сигнала. При ошибке возвращается значение sig_err (определенное в заголовке <signal. h>). Зависимая функция raise () И Функция srand Itfinclude <stdlib.h> void srand(unsigned seed); Функция srand () устанавливает исходное число для последовательности, генерируемой функцией rand (). (Функция rand () возвращает псевдослучайные числа.) Часто функция srand () используется, чтобы при различных запусках программа могла использовать различные последовательности псевдослучайных чисел, — для этого она должна задавать различные исходные числа. Кроме того, с помощью функ- ции srand () можно многократно генерировать одну и ту же последовательность псевдослучайных чисел, — для этого нужно задавать в качестве исходного числа одно и то же значение. Иными словами, чтобы многократно генерировать одну и ту же по- следовательность псевдослучайных чисел, нужно вызывать данную функцию с одним и тем же значением параметра seed до начала генерации этой последовательности. Глава 18. Служебные функции 407
Пример Следующая программа использует системное время в качестве параметра srand(). Это позволяет инициализировать функцию rand () случайным числом. #include <stdio.h> #include <stdlib.h> #include ctime.h> /* Засевает rand() при помощи системного времени и отображает первые 10 чисел. */ int main(void) { int i, stime; long Itime; /* получает текущее календарное время */ Itime = time(NULL); stime = (unsigned) ltime/2; srand(stime); for(i=0; i<10; i++) printf ("%d ", rand()); return 0; } Зависимая функция rand() Я Функция strtod i#include <stdlib.h> double strtod(const char ★start, char ★★end); Функция strtod () преобразует строковое представление числа, которое содер- жится в строке, адресуемой параметром start, в значение типа double и возвращает полученный результат. В версии С99 к параметрам start и end применен квалификатор restrict. Функция strtod () работает следующим образом: Сначала в строке, адресуемой параметром start, пропускаются пробелы, символы табуляции и пустой строки. Затем считываются символы, составляющие число. Когда считывается символ, который не может встречаться в записи числа с плавающей точ- кой, считывание прекращается. К таким символам относятся пробелы, символы табу- ляции и пустой строки, знаки препинания (но не точки) и символы, отличные от букв “Е” и “е”. Наконец, параметр-указатель end устанавливается так, чтобы указывать на “неиспользованный” остаток исходной строки, если таковой существует. Это означа- ет, что, если функция strtod() вызывается с аргументом ”100.00 плоскогубцев", то она возвратит значение 100.00, а параметр-указатель end будет указывать на про- бел, предшествующий слову "плоскогубцев". При возникновении переполнения функция strtod () возвращает либо значение HUGE_VAL, либо значение -HUGE_VAL (означающее положительное или отрицательное пе- реполнение соответственно), а глобальная переменная errno устанавливается равной зна- 408 Часть III. Стандартная библиотека С
чению ERANGE, свидетельствующему об ошибке из-за выхода результата за пределы пред- ставимых чисел. При потере значимости возвращается нуль, а глобальная переменная errno устанавливается равной значению ERANGE. Если параметр start не указывает на число, никакого преобразования не выполняется и функция возвращает нуль. Пример Следующая программа читает числа с плавающей точкой из массива символов. #include <stdlib.h> #include <ctype.h> #include <stdio.h> int main(void) { char *end, *start = ”100.00 плоскогубцев 200.00 молотков"; end = start; while(*start) { printf("%f, ", strtod(start, &end)); printf("Остаток: %sn" ,end); start = end; /* пропускает символы, не входящие в числа */ while(!isdigit(*start) && *start) start++; } return 0; } Вот что выводит эта программа: 1100.000000, Остаток: плоскогубцев 200.00 молотков 200.000000, Остаток: молотков Зависимые функции atof(), strtold() и strtof() См. также функции atof (), strtold () и strtof (). В функция strtof Ittinclude <stdlib.h> float strtof(const char * restrict start, char restrict ** restrict end) ; Функция strtof () добавлена в версии C99. Функция strtof () аналогична функции strtod () за исключением того, что она возвращает значение типа float. При возникновении переполнения возвращается либо значение huge_val, либо значение -huge_val, а глобальная переменная errno устанавливается равной значению erange, свидетельствующему об ошибке из-за выхо- да результата за пределы представимых чисел. Если параметр start не указывает на чис- ло, никакого преобразования не выполняется и функция возвращает нуль. Зависимые функции atof(), strtold() и strtol() Глава 18. Служебные функции 409
IM Функция strtol | #include <stdlib.h> I long int strtol(const char ★start, char ★★end, I int radix) ; Функция strtol () преобразует строковое представление числа, которое содер- жится в строке, адресуемой параметром-указателем start, в значение типа long int и возвращает полученный результат. Основание системы счисления, в которой пред- ставлено преобразуемое число, определяется параметром radix. Если значение radix равно нулю, то основание определяется так же, как и основание системы счисления при записи констант. Если значение radix не равно нулю, то оно должно быть целым числом от 2 до 36. В версии С99 к параметрам start и end применен квалификатор restrict. Функция strtol () работает следующим образом: Сначала в строке, адресуемой параметром start, пропускаются пробелы, симво- лы табуляции и пустой строки. Затем считывается число. Считывание заканчива- ется как только будет обнаружен символ, который не может быть частью длин- ного целого числа. К таким символам относятся пробелы, символы табуляции и пустой строки, знаки препинания и другие символы. Наконец, параметр end уста- навливается так, чтобы указывать на “неиспользованный” остаток исходной стро- ки, если таковой существует. Это означает, что, если функция strtol () вызыва- ется с аргументом ”100 клещей", она возвратит значение 10 0L, а параметр end будет указывать на пробел, предшествующий слову "клещей”. Если результат не может быть представлен как значение типа long int, функция strtol () возвращает либо значение LONG_MAX, либо значение LONG_MIN, а глобальная переменная errno устанавливается равной значению ERANGE, свидетельствующему об ошибке из-за выхода за границы представимых чисел. Если параметр start не указывает на число, никакого преобразования не выполняется и функция возвращает нуль. Пример Следующая функция может использоваться для чтения из стандартного входного потока числа, представленного в десятичной системе счисления. Данная функция воз- вращает результат (целое число) типа long. long int read_long(void) { char start[80], *end; printf("Введите число: "); gets(start); return strtol(start, &end, 10); } Зависимые функции atol () и strtoll() 410 Часть III. Стандартная библиотека С
й функция strtold |#include <stdlib.h> long double strtold(const char * restrict start, char ** restrict end); Функция strtold () добавлена в версии C99. Функция strtold () аналогична функции strtod () за исключением того, что она возвращает значение типа long double. При возникновении переполнения возвра- щается либо значение huge vall, либо значение -huge vall, а глобальная перемен- ная errno устанавливается равной значению ERANGE, свидетельствующему об ошибке из-за выхода результата за пределы представимых чисел. Если параметр start не указыва- ет на число, никакого преобразования не выполняется и функция возвращает нуль. Зависимые функции atof(), strtold() и strtof() В Функция strtoll I# include <stdlib.h> long long int strtoll(const char * restrict start, char ** restrict end, int radix); Функция strtoll () добавлена в версии C99. Функция strtoll () аналогична функции strtol () за исключением того, что она возвращает значение типа long long int. Если результат не может быть представлен как значение типа long long int, возвращается либо значение LLONG_MAX, либо значение LLONG MIN, а глобальная переменная errno устанавливается равной значе- нию ERANGE, свидетельствующему об ошибке из-за выхода результата за пределы пред- ставимых чисел. Если параметр start не указывает на число, никакого преобразования не выполняется и функция возвращает нуль. Зависимые функции atoi () и strtol () В Функция strtoul |#include <stdlib.h> unsigned long int strtoul(const char ★start, char ★★end, int radix); Функция strtoul () преобразует строковое представление числа, которое содер- жится в строке, адресуемой параметром start, в значение типа unsigned long и воз- вращает полученный результат. Основание системы счисления, в которой представле- но число, определяется параметром radix. Если значение radix равно нулю, то основа- ние определяется так же, как и основание системы счисления при записи констант. Если значение radix не равно нулю, то оно должно быть целым числом от 2 до 36. В версии С99 к параметрам start и end применен квалификатор restrict. Функция strtoul () работает следующим образом: Глава 18. Служебные функции 411
Сначала в строке, адресуемой параметром start, пропускаются пробелы, символы табу- ляции и пустой строки. Затем считывается число. Считывание заканчивается как толь- ко будет обнаружен символ, который не может быть частью длинного целого числа без знака. К таким символам относятся пробелы, символы табуляции и пустой строки, знаки препинания и другие символы. Наконец, параметр end устанавливается так, чтобы указывать на “неиспользованный” остаток исходной строки, если такой суще- ствует. Например, если функция strtoul () вызывается с аргументом ”100 клещей", то она возвратит значение 100L, а параметр end будет указывать на пробел, предшествующий слову "клещей". Если результат не может быть представлен как длинное целое без знака, функция strtoul () возвращает значение ULONG_MAX, а глобальная переменная errno устанав- ливается равной значению erange, что свидетельствует об ошибке из-за выхода резуль- тата за пределы представимых чисел. Если параметр start не указывает на число, ника- кого преобразования не выполняется и функция возвращает нуль. Пример Следующая функция может использоваться для чтения из стандартного входного потока числа, представленного в шестнадцатеричной системе счисления. Данная функция возвращает результат (целое число) типа unsigned long. unsigned long int read_unsigned_long(void) { char start[80], *end; printf("Введите шестнадцатеричное число: ”); gets(start); return strtoul(start, &end, 16); } Зависимые функции strtolO И strtoull() И Функция strtoull |#include <stdlib.h> unsigned long long int strtoull(const char *restrict start, char **restrict end, int radix); Функция strtoull () добавлена в версии C99. Функция strtoull () аналогична функции strtoul () за исключением того, что она возвращает значение типа unsigned long long int. Если результат не может быть представлен как значение типа unsigned long long int, возвращается значе- ние ULLONG_MAX, а глобальная переменная errno устанавливается равной значению ERANGE, свидетельствующему об ошибке из-за выхода результата за пределы представи- мых чисел. Если параметр start не указывает на число, никакого преобразования не выполняется и функция возвращает нуль. Зависимые функции strtolO и strtoul () 412 Часть III. Стандартная библиотека С
§ Функция system |#include <stdlib.h> int system(const char ★str); Функция system () передает строку, адресуемую параметром str, в качестве коман- ды для командного процессора операционной системы. Если функция system () вызывается с нулевым указателем, она возвращает нену- левое значение при условии доступности командного процессора и нуль в противном случае. (Программы, выполняемые в специальных средах, могут не иметь доступа к командному процессору.) Значение, возвращаемое функцией systemO, определяется конкретной реализацией. Но обычно возвращается нуль при успешном выполнении команды, а ненулевое значение кода возврата означает наличие ошибки. Пример В операционной системе Windows эта программа отображает содержимое текущего каталога: #include <stdlib.h> int main(void) { return system("dir") ; } Зависимая функция exit() И Функции-макросы va_arg, va_start5 va_end и va_copy #include <stdarg.h> type va_arg(va_list argptr, type); void va_copy(va_list target, va_list source); void va_end(va_list argptr); void va_start(va_list argptr, last_parm); Макрос va copy () добавлен в версии C99. Для передачи функции переменного числа аргументов совместно используются макросы va arg, va start и va end. Самым распространенным примером функции, которая принимает переменное число аргументов, является функция printf (). Тип va list определен в заголовке <stdarg.h>. Общая процедура создания функции, которая может принимать переменное количест- во аргументов, такова: Функция должна иметь по крайней мере один известный параметр (может и больше), указываемый до переменного списка параметров. (Такие параметры называются также обязательными, а параметры, следующие за ними — необязательными.) Крайний пра- вый известный параметр называется last_рагт. (Он предшествует первому необязательно- му параметру.) Его имя используется в качестве второго параметра в обращении к макросу va start (). Чтобы получить доступ к любому дополнительному параметру, сначала не- Глава 18. Служебные функции 413
обходимо инициализировать указатель-аргумент argptr1, обратившись к макросу va_start(). (Иными словами, необходимо выполнить вызов va_start {argptr, <имя last_parm>}. — Прим, ред.) После этого значения параметров возвращаются в результате вызова макроса va_arg (). В качестве второго аргумента этого макроса (соответствующего параметру type), нужно указать тип следующего параметра1 2. Наконец, после прочтения всех параметров до возвращения из функции необходимо вызвать макрос va_end (), что- бы гарантировать корректное восстановление стека. Если макрос va_end () вызван не бу- дет, высока вероятность аварийного отказа программы. Макрос va_copy () копирует список аргументов, обозначенный параметром target, в объект, обозначенный параметром source. Пример Эта программа использует функцию sum_series (), возвращающую сумму после- довательности чисел. Первый аргумент содержит число дополнительно передаваемых аргументов. В этом примере программа суммирует первые пять слагаемых суммы 1111 1 2 + 4 + 8 + 16 + + 2N Будет выведено 0.968750. #include <stdio.h> #include <stdarg.h> double sum_series(int num. ...); /* Пример переменного числа аргументов - сумма последовательности. */ int main(void) { double d; d= sum_series(5, 0.5, 0.25, 0.125, 0.0625, 0.03125); printf("Сумма последовательности %f.n", d); return 0; } double sum_series(int num, ...) { double sum=0.0, t; va_list argptr; /* Инициализация argptr */ va_start(argptr, num); /* сумма последовательности */ for ( ; num; num—) { t = va_arg(argptr, double); /* получить следующий аргумент */ sum += t; } 1 Он должен быть объявлен, например, так: va_list argptr; — Прим. ред. 2 Ну где вы видели функцию, которой в качестве аргумента передается служебное слово? Конечно, это возможно именно потому, что va_arg() — макрос, а не функция языка С. Кроме того, посмотрите на описание type va_arg(va_list argptr, type}; Какой же компилятор по- зволит вам так описывать функцию?! — Прим. ред. 414 Часть III. Стандартная библиотека С
I /* выполнение корректного выхода */ I va_end(argptг); I return sum; I } Зависимая функция vprintf() В Функция wcstombs |#include <stdlib.h> size_t wcstombs(char ★out, const wchar_t *in, size_t size); Функция wcstombs () преобразует массив двухбайтовых символов, адресуемый па- раметром-указателем /л, в его многобайтовый эквивалент и помещает результат в мас- сив, адресуемый параметром out. Преобразованию подлежат только первые size симво- лов. Процесс преобразования прекращается раньше, если будет обнаружен символ кон- ца строки (‘О'). В версии С99 к параметрам out к in применен квалификатор restrict. При успешном выполнении функция wcstombs () возвращает количество байтов, помещенных в массив out. При возникновении ошибки возвращается значение -1. Зависимые функции wctombO И mbstowcs () В Функция wctomb |#include <stdlib.h> int wctomb(char ★out, wchar_t in); Функция wctomb () преобразует двухбайтовый символ, содержащийся в параметре ш, в его многобайтовый эквивалент и помещает результат в массив, адресуемый пара- метром out. Массив, адресуемый параметром out, должен иметь длину не меньше MB_CUR_MAX СИМВОЛОВ. При успешном выполнении функция wctomb () возвращает количество байтов, со- держащихся в многобайтовом символе. При возникновении ошибки возвращается значение -1. Если параметр out равен нулю, функция wctomb () возвращает ненулевое значение в случае, когда многобайтовый символ имеет кодировку, зависящую от территориаль- но-языковых особенностей. В противном случае возвращается нуль. Зависимые функции wcstomb(s) И mbstowcs () Глава 18. Служебные функции 415
Полный справочник по Глава 19 Функции обработки двухбайтовых символов
В 1995 году к стандарту С89 был добавлен ряд функций, предназначенных для об- работки двухбайтовых символов (wide-character functions), которые позже вошли в стандарт С99. Эти функции работают с символами типа wchar_t длиной 16 бит. Для большинства этих функций существуют эквивалентные им функции, работающие с символами типа char. Например, функция iswspaceO для обработки двухбайтовых символов является версией функции isspace (). В целом, имена функций для обра- ботки двухбайтовых символов образованы из имен аналогичных функций для работы с символами типа char путем добавления буквы “w” (wide-character, т.е. “широкоформатный” символ). В языке С функции, предназначенные для работы с двухбайтовыми символами, исполь- зуют два заголовка: <wchar. h> и <wctype. h>. В заголовке <wctype. h> определены типы wint_t, wctrans_t и wctype_t. Многие функции, предназначенные для работы с двух- байтовыми символами, принимают в качестве параметра двухбайтовый символ. Такой пара- метр имеет тип wint t, в которой можно записать двухбайтовый символ. Использование типа wint_t в функциях, предназначенных для работы с двухбайтовыми символами, ана- логично использованию типа int в функциях, обрабатывающих тип char. Типы wctrans_t и wctype t — это типы объектов, используемые для преобразования символов и определения категории символа соответственно. Кроме того, в заголовке <wctype. h> оп- ределен двухбайтовый признак конца файла (EOF) под именем WEOF. Помимо win_t, в заголовке <wchar.h> определены такие типы, как wchar_t, size_t и mbstate t. Тип wchar t создает двухбайтовый символ- объект, a size_t — это тип зна- чения, возвращаемого оператором sizeof. Тип mbstate_t описывает объект, который хра- нит состояние преобразования многобайтового объекта в двухбайтовые символы. Заголовок <wchar.h> также определяет макросы NULL, WEOF, WCHAR_MAX и WCHAR_MIN. Последние два макроса определяют максимальное и минимальное значения, которые могут храниться в объекте типа wchar_t. Поскольку большинство функций, предназначенных для работы с двухбайтовыми сим- волами, аналогичны соответствующим им функциям для работы с символами типа char, т.е. тем самым функциям, которые часто используется большинством программистов, зна- комых с языком С, то в этой главе приводится лишь краткое описание таких функций. Щ функции классификации двухбайтовых символов Заголовок <wctype.h> содержит прототипы тех функций, которые позволяют классифицировать двухбайтовые символы. Эти функции распределяют по категориям двухбайтовые символы или преобразуют регистр буквенного символа, устанавливая строчное или прописное написание. В табл. 19.1 приведены списки этих функций, а также соответствующие им функции для работы с символами типа char, которые бы- ли описаны в главе 14. Таблица 19.1. Функции, предназначенные для работы с двухбайтовыми символами, и соответствующие им функции для типа char Функция Соответствующая функция для типа char int iswalnum(wint_t ch) isalnum() int iswalpha(wint_t ch) isalpha() int iswblank(wint_t ch) isblank () (Добавлена в C99.) int iswcntrl(wint_t ch) iscntri() int iswdigit(wint_t ch) isdigit() 418 Часть III. Стандартная библиотека С
Окончание табл. 19.1 Функция Соответствующая функция для типа char int iswgraph(wint_t ch) isgraph() int iswlower(wint_t ch) islower() int iswprint(wint_t Ch) isprint() int iswpunct(wint_t Ch) ispunct() int iswspace(wint_t ch) isspace() int iswupper(wint_t Ch) isupper() int iswxdigit(wint_t ch) isxdigit() wint_t towlower(wint_t ch) tolower() wint_t towupper(wint_t ch) toupper() Помимо функций, приведенных в табл. 19.1, в заголовке <wctype.h> определены сле- дующие функции, которые предоставляют открытые средства классификации символов. |wctype_t wctype (const char ★attr) ; int iswctype(wint_t ch, wctype_t attr_ob); Функция wctype () возвращает значение, которое можно передать функции iswctype () в качестве параметра attr_ob. Строка, адресуемая параметром attr, задает свой- ство, которое должен иметь символ. Это значение можно затем использовать для определе- ния, является ли ch символом, который обладает этим свойством. Если является, то функция iswctype () возвращает ненулевое значение. В противном случае возвращается нуль. В лю- бых условиях выполнения программы определены следующие строки свойств: alnum digit print upper alpha graph punct xdigit cntrl lower space В версии C99 также определена строка blank. Следующий фрагмент демонстрирует использование функций wctype () и iswctype (): wctype_t х; х = wctype("space"); if(iswctype(L’ ', x)) printf("Это пробел.n"); Будет выведено “Это пробел”. Кроме того, в заголовке <wctype.h> определены функции wctransO и towctrans (). Их описания приведены ниже. |wctrans_t wctrans (const char ★mapping) ; wint_t towctrans(wint_t ch, wctrans_t mapping_ob); Функция wctransO возвращает значение, которое можно передать функции towctrans () в качестве параметра mapping_ob. Строка, адресуемая параметром mapping, определяет отображение одного символа на другой. Данная строка затем может быть ис- пользована функцией towctrans () для преобразования символа ch. Функция возвращает преобразованное значение. При всех условиях выполнения программы поддерживаются сле- дующие строки преобразования. tolower toupper Глава 19. Функции обработки двухбайтовых символов 419
Следующая последовательность демонстрирует применение функций wctrans () и towctrans(): wctranS—t х; х = wctrans("tolower"); wchar_t ch = towctrans(L’W', x); printf("%c", (char) ch); Выводит w на нижнем регистре. i Функции ввода-вывода двухбайтовых символов Некоторые функции ввода-вывода, описанные в главе 13, имеют реализации, ори- ентированные на работу с двухбайтовыми символами. Эти функции (они перечислены в табл. 19.2) используют заголовок <wchar.h>. Обратите внимание на то, что функ- ции swprintf () и vswprintf () требуют передачи дополнительного параметра, в ко- тором не нуждаются соответствующие им функции для типа char. Таблица 19.2. Функции ввода/вывода для двухбайтовых символов и соответствующие им функции для типа char Функция Соответствующая функция для типа char wint_t fgetwc(FILE *stream) wchar_t *fgetws(wchar_t ★str, int num, file ★stream) wint_t fputwc(wchar_t ch, file ★stream) int fputws (const wchar_t ★str, file ★stream) fgetc() fgets() В версии C99 к параметрам str и stream применен квалификатор restrict fputc() fputs ( ) В версии С99 к параметрам str и stream применен квалификатор restrict int fwprintf (file ★stream, const wchar_t ★fmt, . . .) fprintf() В версии С99 к параметрам stream и fmt применен квалификатор restrict int fwscanf(FiLE ★stream, const wchar_t ★fmt, . . .) fscanf() В версии С99 к параметрам stream и fmt применен квалификатор restrict wint_t getwc(FILE *stream) wint_t getwchar(void) wint_t putwc(wchar_t ch, FILE *stream) wint_t putwchar(wchar_t ch) int swprintf (wchar_t ★str, size_t num, const wchar_t ★fmt, . . .) getc() getchar() putc() putchar() sprintf() Обратите внимание на то, что добавлен параметр num, который ограничивает количество символов, записы- ваемых в массив str. В версии С99 к параметрам str и fmt применен квалификатор restrict int swscanf (const wchar_t ★str, const wchar_t ★fmt, . . .) sscanf() В версии С99 к параметрам str и fmt применен ква- лификатор restrict wint_t ungetwc(wint_t ch, FILE *stream) ungetc() 420 Часть III. Стандартная библиотека С
Окончание табл. 19.2 Функция int vfwprintf (file ★stream, const wchar_t ★fmt, va_list arg) int vfwscanf ( file * restrict stream, const wchar_t * restrict fmt, va_list arg); int vswprintf (wchar_t ★str, size_t num, const wchar_t ★fmt, va_list arg) int vswscanf(const wchar_t * restrict Str, const wchar_t * restrict fmt, va_list arg) ; int vwprintf (const wchar_t ★fmt, va_list arg) , int vwscanf( const wchar_t * restrict fmt, va_list arg); int wprintf( const wchar_t ★fmt, . . .) int wscanf( const wchar_t ★fmt, . . .) Соответствующая функция для типа char vfprintf() В версии C99 к параметру fmt применен квалифика- тор restrict vf scanf () (Добавлена в версии С99.) vsprintf() Обратите внимание на то, что добавлен параметр num, который ограничивает количество символов, записываемых в массив str. В версии С99 к парамет- рам str и fmt применен квалификатор restrict vsscanf() (Добавлена в версии С99.) vprintf() В версии С99 к параметру fmt применен квалифика- тор restrict vscanf () (Добавлена в версии С99.) printf() В версии С99 к параметру fmt применен квалифика- тор restrict scanf() В версии С99 к параметру fmt применен квалифика- тор restrict Дополнительно к функциям, показанным в таблице, добавлена следующая функ- ция, ориентированная на работу с двухбайтовыми символами: | int fwide(FILE ★stream, int how) ; Если значение параметра how положительно, функция fwide () делает поток stream по- током двухбайтовых символов. Если же значение параметра how отрицательно, то функция fwide () превращает поток stream в поток объектов типа char. А если значение how равно нулю, на поток stream никакого воздействия не оказывается. Если этот поток уже был ориен- тирован либо на двухбайтовые, либо на обычные символы, он изменяться не будет. Функция возвращает положительное значение, если поток рассматривается как содержащий двухбай- товые символы. Отрицательное значение возвращается, если он рассматривается как содер- жащий символы типа char. В случае, когда поток еще не ориентирован, функция возвраща- ет нуль. Ориентация потока также определяется его первым использованием. Ш Функции для операций над строками двухбайтовых символов Для операций над строками двухбайтовых символов существуют версии функций, описанных в главе 14. Эти функции (перечисленные в табл. 19.3) используют заголовок <wchar.h>. Заметьте, что функция wcstokO, в отличие от версии функции для типа char, требует передачи дополнительного параметра. Глава 19. Функции обработки двухбайтовых символов 421
Таблица 19.3. Функции для операций над строками двухбайтовых символов и соответствующие им функции для типа char. Функция Соответствующая функция для типа char wchar_t *wcscat (wchar_t ★strl, const wchar_t *sfr2) wchar_t *wcschr (const wchar_t ★str, wchar_t ch) int wcscmp (const wchar_t ★strl, const wchar_t *sfr2) int wcscoll (const wchar_t ★strl, const wchar_t ★str?) size_t wcscspn (const wchar_t ★strl, const wchar_t ★str2) wchar_t *wcscpy (wchar_t ★strl, const wchar_t ★str?) size_t wcslen (const wchar_t ★str) wchar_t *wcsncpy (wchar_t *Str1, const wchar_t ★str?, size_t num) wchar_t *wcsncat (wchar_t ★strl, const wchar_t ★str?, size_t num) int wcsncmp (const wchar_t ★strl, const wchar_t ★str?, size_t num) wchar_t *wcspbrk (const wchar_t ★strl, const wchar_t ★str?) wchar_t *wcsrchr( const wchar_t ★strl, wchar_t ch) size_t wcsspn (const wchar_t ★strl, const wchar_t ★str?) wchar_t *wcstok (wchar_t ★strl, const wchar_t ★str?, wchar_t ★★ endptr) wchar_t *wcsstr (const wchar_t ★strl, const wchar_t ★str?) size_t wcsxfrm(wchar_t ★strl, const wchar_t ★str?, size_t num) strcat() В версии C99 к параметрам strl и str? применен квалификатор restrict strchr() strcmp() strcoll() strcspn() strcpy() В версии C99 к параметрам strl и str? применен квалификатор restrict strlen() strncpy() В версии С99 к параметрам strl и str? применен квалификатор restrict strncat() В версии С99 к параметрам strl и str? применен квалификатор restrict strncmp() strpbrk() strrchr() strspn() strtok() Здесь параметр endptr является указателем, кото- рый содержит информацию, необходимую для про- должения процесса разделения строки на лексемы. В версии С99 к параметрам strl и str? применен квалификатор restrict strstr() strxfrm() В версии С99 к параметрам strl и str? применен квалификатор restrict |В| Преобразование строк двухбайтовых символов Функции, перечисленные в табл. 19.4, предназначены для преобразования строк двухбайтовых символов в числа; в таблице приведены также функции времени. Для всех функций в таблице указаны соответствующие им стандартные функции. Все функции, указанные в таблице, используют заголовок <wchar.h>. 422 Часть III. Стандартная библиотека С
Таблица 19.4. Функции преобразования строк двухбайтовых символов и соответствующие им функции для типа char. Функция Соответствующая функция для типа char size_t wcsftime (wchar_t ★Str, size_t max, const wchar_t ★fmt, const struct tm *ptr) double wcstod(const wchar_t ★start, wchar_t ★★end); float wcstof (const wchar_t * restrict start, wchar_t ** restrict end)] long double wcstold( const wchar_t * restrict start, wchar_t ** restrict end)] long int wcstol (const wchar_t ★start, wchar_t ★★end, int radix) long long int wcstoll(const wchar_t * restrict start, wchar_t ** restrict end, int radix) unsigned long int wcstoul(const wchar_t * restrict start, wchar_t ** restrict end, int radix) unsigned long long int wcstoull( const wchar_t ★start, wchar_t ★★end, int radix) strftime() В версии С99 к параметрам str1t fmt и ptr применен квалификатор restrict strtod() В версии С99 к параметрам start и end применен квалификатор restrict strtof () (Добавлена в версии С99.) strtold () (Добавлена в версии С99.) strtol() В версии С99 к параметрам start и end применен квалификатор restrict strtoll () (Добавлена в версии С99.) strtoul() В версии С99 к параметрам start и end применен квалификатор restrict strtoull () (Добавлена в версии С99.) Щ Функции для обработки массивов двухбайтовых символов Для стандартных функций, предназначенных для обработки массивов символов (например, для memcpy ()), имеются соответствующие функции, выполняющие анало- гичные операции над массивами двухбайтовых символов. Эти функции (перечисленные в следующей табл. 19.5) используют заголовок <wchar.h>. Таблица 19.5. Функции для обработки массивов двухбайтовых символов и соответствующие им функции для типа char. Функция Соответствующая функция для типа char wchar_t *wmemchr (const wchar_t ★str, memchr() wchar_t ch, size_t num) int wmemcmp(const wchar_t *strl, const wchar_t *str2, size_t num) memcmp() wchar_t *wmemcpy (wchar_t ★strl, const wchar_t *str2, size_t num) memcpy() В версии C99 к параметрам strl и str2 применен квалификатор restrict wchar_t *wmemmove(wchar_t *strl, const wchar_t *str2, size_t num) memmove() wchar_t *wmemset(wchar_t *str, wchar_t ch, size_t num) memset() Глава 19. Функции обработки двухбайтовых символов 423
IS функции для преобразования многобайтовых и двухбайтовых символов Стандартная библиотека поддерживает различные функции, предназначенные для преобразования многобайтовых и двухбайтовых символов. Эти функции (перечисленные в табл. 19.6) используют заголовок <wchar.h>. Многие из них пред- ставляют собой версии обычных многобайтовых функций, которые могут быть пре- рваны (и повторно запущены — restartable). Повторно запускаемая версия использует информацию о состоянии, передаваемую ей в параметре типа mbstate_t. Если этот параметр нулевой, функция предоставит собственный объект типа mbstate t. Таблица 19.6. Функции для преобразования многобайтовых и двухбайтовых символов Функция win_t btowc(int Ch) size_t mbrlen(const char ★Str, size_t num, mbstate_t * State) size_t mbrtowc(wchar_t ★out, const char ★in, size_t num, mbstate_t ★ State) int mbsinit(const mbstate_t ★ state) size_t mbsrtowcs(wchar_t ★out, const char ★★in, size_t num, mbstate_t ★state) size_t wcrtomb(char ★out, wchar_t Ch, mbstate_t ★ State) size_t wesrtombs (char *out, const wchar_t ★★in, size_t num, mbstate_t ★state) int wctob(wint_t Ch) Описание Преобразует параметр ch в его двухбайтовый эквивалент и воз- вращает результат. Значение weof возвращается при ошибке или если ch не однобайтовый, а многобайтовый символ Повторно запускаемая версия функции mblen(), в которой информация о состоянии передается через параметр state. Возвращает положительное число, равное длине следующего многобайтового символа. Нуль возвращается в случае, если следующий символ — нулевой. При ошибке возвращается от- рицательное значение. В версии С99 к параметрам str и state применен квалификатор restrict Повторно запускаемая версия функции mbtowe (), в которой ин- формация о состоянии передается через параметр state. Возвра- щает положительное число, равное длине следующего многобай- тового символа. Нуль возвращается в случае, если следующий символ — нулевой. При ошибке возвращается значение -1 и пе- ременной errno присваивается макрос eilseq. Если преобразо- вание не завершено, возвращается число -2. В версии С99 к па- раметрам out, in и state применен квалификатор restrict Возвращает значение true, если параметр state представляет начальное состояние процесса преобразования Повторно запускаемая версия функции mbstowcsO, в кото- рой информация о состоянии передается через параметр state. Кроме того, функция mbsrtowcs () отличается от функ- ции mbstowcsO тем, что параметр in является косвенным указателем на исходный массив. При ошибке переменной errno присваивается макрос eilseq. В версии С99 к пара- метрам out, in и state применен квалификатор restrict Повторно запускаемая версия функции wetomb (), в которой информация о состоянии передается через параметр state. При ошибке переменной errno присваивается макрос eilseq. В версии С99 к параметрам out и state применен ква- лификатор restrict Повторно запускаемая версия функции wcstombsO, в кото- рой информация о состоянии передается через параметр state. Кроме того, функция wesrtombs О отличается от функ- ции wcstombsO тем, что параметр in является косвенным указателем на исходный массив. При ошибке переменной errno присваивается макрос eilseq. В версии С99 к пара- метрам out, in и state применен квалификатор restrict Преобразует параметр ch в его однобайтовый эквивалент. При сбое функция возвращает значение eof 424 Часть III. Стандартная библиотека С
Полный справочник по Глава 20 Библиотечные средства, добавленные в версии С99
Благодаря Стандарту С99 произошло значительное увеличение возможностей библиоте- ки языка С. Во-первых, добавлены новые функции в заголовки, ранее определенные в версии С89. Например, намного расширена математическая библиотека, поддерживаемая заголовком <math.h>. Эти дополнительные функции описывались в предыдущих главах. Во-вторых, созданы новые категории функций, начиная от поддержки арифметических операций с комплексными числами и заканчивая макросами обобщенного типа, а также новые заголовки, предназначенные для поддержки таких функций. Эти новые библиотеч- ные элементы и описываются в данной главе. В Библиотека поддержки арифметических операций с комплексными числами В версии С99 стало возможным выполнять арифметические операции с комплексными числами. Библиотека поддержки арифметических операций с комплексными числами имеет заголовочный файл <complex. h>. В нем определены следующие макросы: Макрос Расширение complex imaginary _Complex_I _Imaginary_I I —Complex —Imaginary (const float—Complex) / (const float imaginary) / —Imaginary_I (или _Complex_i, если не поддерживаются мнимые типы) Здесь i представляет мнимое значение, которое равно квадратному корню из —1. Под- держка мнимых типов не обязательна. В версии С99 вместо ключевых слов complex и imaginary определены слова -Complex и -Imaginary, поскольку во многих существующих программах, написанных на С89, уже определены пользовательские типы данных complex и imaginary для комплекс- ных чисел. Использование в версии С99 слов Complex и Imaginary позволяет избежать изменения написанного ранее кода. Однако в новые программы лучше всего включить заголовок <complex.h>, а затем использовать макросы complex и imaginary. В C++ определен класс complex, в котором предлагается иной способ реали- зации действий с комплексными числами. На заметку Ниже в табл. 20.1 приведены математические функции, часто используемые в эле- ментарной теории функций комплексного переменного. Обратите внимание, что для каждой функции определены версии float complex, double complex и long double complex. Имя версии float complex имеет суффикс f, а имя версии long double complex —суффикс 1. Углы измеряются, разумеется, в радианах. Таблица 20.1. Математические функции, используемые в элементарной теории функций комплексного переменного Функция____________________________Назначение______________________________________ float cabsf (float complex arg); Возвращает абсолютную величину (модуль) комплексно- го числа arg double cabs(double complex arg) ; long double cabsl( long double complex arg) ; 426 Часть III. Стандартная библиотека С
Продолжение табл. 20.1 Функция float complex cacosf( float complex arg) ; Назначение Возвращает комплексное значение арккосинуса от пара- метра arg double complex cacos( double complex arg) ; long double complex cacosl( long double complex arg) ; float complex cacoshf( float complex arg); Возвращает комплексное значение гиперболического арккосинуса от параметра arg double complex cacosh( double complex arg) ; long double complex cacoshl( long double complex arg) ; float cargf (float complex arg); Возвращает значение аргумента комплексного числа arg double carg(double complex arg); long double cargl( long double complex arg) ; float complex casinf ( Возвращает комплексное значение арксинуса от пара- float complex arg); метра arg double complex easin( double complex arg) ; long double complex casinl( long double complex arg) ; float complex casinhf ( Возвращает комплексное значение гиперболического float complex arg); арксинуса от параметра arg double complex casinh( double complex arg); long double complex casinhl( long double complex arg) ; float complex catanf ( Возвращает комплексное значение арктангенса от пара- float complex arg); метра arg double complex catan( double complex arg) ; long double complex catanl( long double complex arg); float complex catanhf ( Возвращает комплексное значение гиперболического float complex arg); арктангенса от параметра arg double complex catanh( double complex arg) ; long double complex catanhl( long double complex arg) ; Глава 20. Библиотечные средства, добавленные в версии С99 427
Продолжение табл. 20.1 Функция Назначение float complex ccosf( Возвращает комплексное значение косинуса от парамет- float complex arg); double complex ccos( double complex arg); long double complex ccosl( long double complex arg); pa arg float complex ccoshf( Возвращает комплексное значение гиперболического ко- float complex arg); double complex ccosh( double complex arg) ; long double complex ccoshl( long double complex arg); синуса от параметра arg float complex cexpf( Возвращает комплексное значение earfl, где е — основа- float complex arg); double complex cexp( double complex arg) ; long double complex cexpl( long double complex arg) ; ние натурального логарифма float cimagf(float complex arg); double cimag(double complex arg); long double cimagl( long double complex arg) ; Возвращает мнимую часть параметра arg float complex clogf( Возвращает комплексное значение натурального лога- float complex arg) ; double complex clog( double complex arg) ; long double complex clogl( long double complex arg) ; рифма от параметра arg float complex conjf( Возвращает комплексно-сопряженное значение пара- float complex arg) ; double complex conj( double complex arg) ; long double complex conjl( long double complex arg) ; метра arg float complex cpowf( float complex a, long double complex b) ; double complex cpow( double complex a, double complex b) ; long double complex cpowl( long double complex a, long double complex b) ; Возвращает комплексное значение аь 428 Часть III. Стандартная библиотека С
Окончание табл. 20.1 Функция Назначение float complex cprojf( float complex arg) ; double complex cproj( double complex arg) ; long double complex cproj1( long double complex arg) ; Возвращает проекцию параметра arg на сферу Римана float crealf(float complex arg); double creal(double complex arg); long double creall( long double complex arg) ; Возвращает вещественную часть параметра агд float complex csinf( float complex arg) ; double complex csin( double complex arg) ; long double complex csinl( long double complex arg); Возвращает комплексное значение синуса от параметра агд float complex csinhf( Возвращает комплексное значение гиперболического си- float complex arg) ; double complex csinh( double complex arg) ; long double complex csinhl( long double complex arg) ; нуса от параметра агд float complex csqrtf( Возвращает комплексное значение квадратного корня из float complex arg) ; double complex csqrt( double complex arg) ; long double complex csqrtl( long double complex arg); параметра агд float complex ctanf( Возвращает комплексное значение тангенса от парамет- float complex arg) ; double complex ctan( double complex arg) ; long double complex ctanl( long double complex arg); ра агд float complex ctanhf( Возвращает комплексное значение гиперболического float complex arg) ; double complex ctanh( double complex arg) ; long double complex ctanhl( long double complex arg) ; тангенса от параметра агд Глава 20. Библиотечные средства, добавленные в версии С99 429
Н Библиотека поддержки среды вычислений с плавающей точкой В версии С99 заголовком <fenv.h> объявляются функции, которые имеют доступ к среде вычислений с плавающей точкой. Эти функции описаны в табл. 20.2. Заголо- вок <fenv.h> также определяет типы fenv_t и fexcept_t, которые представляют конфигурацию вычислителя, реализующего среду вычислений с плавающей точкой и флаги состояния этого вычислителя соответственно. Макрос FE_DFL_ENV задает ука- затель на действующую по умолчанию конфигурацию вычислителя, реализующего среду вычислений с плавающей точкой, определенную при запуске программы. Определены также следующие макросы исключений, возникающих при работе с числами с плавающей точкой: FE_DIVBYZERO FE_INEXACT FE_INVALID FE_OVERFLOW FE_UNDERFLOW FE_ALL_EXCEPT Все комбинации этих макросов, полученные с помощью операции ИЛИ, можно сохранять в объекте типа int. Определены также следующие макросы, используемые для указания направления ок- ругления значений: FE-DOWNWARD FE_TONEAREST FE_TOWARDZERO FE_UPWARD Для проверки флагов вычислителя, реализующего среду вычислений с плавающей точкой, необходимо установить специальную директиву (прагму) для компилятора FENV_ACCESS в положение “включено”. Разрешен ли доступ к этим флагам по умол- чанию, зависит от конкретной реализации. Таблица 20.2. Функции вычислителя, реализующего среду вычислений с плавающей точкой Функция Назначение void feclearexcept(int ex); Сбрасывает исключения, заданные параметром ex void fegetexceptflag(fexcept_t В переменной, адресуемой указателем fptr, сохраняет *fptr, int ex); состояние флагов исключений вычислителя, реализую- щего среду вычислений с плавающей точкой, заданных параметром ех void feraiseexcept(int ex); Возбуждает исключения, заданные параметром ех void fesetexceptflag(fexcept_t Устанавливает флаги состояния вычислителя, реали- ★fptr, int ex) ; зующего среду вычислений с плавающей точкой, задан- ные параметром ех, в состояние флагов, содержащихся в объекте, адресуемом параметром fptr int fetestexcept(int ex); Выполняет операцию поразрядного ИЛИ над флагами, заданными параметром ех, и текущими флагами вычис- лителя, реализующего среду вычислений с плавающей точкой. Возвращает результат этой операции int fegetround(void) ; Возвращает значение действующего направления ок- ругления int fesetround(int direction); Устанавливает значение текущего направления округ- ления с помощью параметра direction. При успешном выполнении возвращается нуль void fegetenv (fenv_t ★envptr); В объект, адресуемый параметром envptr, записывается конфигурация вычислителя, реализующего среду вы- числений с плавающей точкой 430 Часть III. Стандартная библиотека С
Окончание табл. 20.2 Функция Назначение int feholdexcept (fenv_t ★envptr); Устанавливает безостановочную обработку исключения, возникшего при выполнении вычислений с плавающей точкой. Сохраняет конфигурацию вычислителя, реали- зующего среду вычислений с плавающей точкой, в пе- ременной, адресуемой параметром envptr, и сбрасыва- ет флаги состояния. При успешном выполнении воз- вращает нуль void fesetenv(fenv_t ★envptr); Устанавливает конфигурацию вычислителя, реализую- щего среду вычислений с плавающей точкой, равной значению переменной, адресуемой параметром envptr, но исключения с плавающей точкой при этом не возбу- ждаются. Объект, адресуемый параметром envptr, дол- жен быть получен в результате вызова функции fegetenv () или функции feholdexcept () void feupdateenv(fenv_t ★envptr); Устанавливает конфигурацию вычислителя, реализую- щего среду вычислений с плавающей точкой, равной значению переменной, адресуемой параметром envptr. Сначала сохраняет любые текущие исключения, а за- тем, после установки конфигурации вычислителя в со- ответствии со значением переменной, адресуемой па- раметром envptr, возбуждает эти исключения. Объект, адресуемый параметром envptr, должен быть получен путем вызова функции fegetenvO или функции feholdexcept() В Заголовок <stdint.h> В заголовке <stdint.h> версии С99 не объявлено ни одной функции, но он опре- деляет множество целочисленных типов и макросов. Целочисленные типы используются для объявления целых значений известного размера или значений, несущих информа- цию о некоторых специальных атрибутах. Макросы, имеющие вид int7V_t, определяют целое с разрядностью N бит. Напри- мер, макрос intl6_t задает 16-разрядное целое со знаком. Макросы, имеющие вид uint2V_t, определяют целое значение без знака с разрядностью N бит. Например, макрос uint32_t задает 32-разрядное целое без знака. Макросы, в имени которых N равно 8, 16, 32 или 64, доступны во всех средах, в которых предусмотрено выполнение операций над целыми числами с указанной разрядностью. Макросы, имеющие вид int_least7V_t, определяют целое значение с разрядно- стью не менее N бит. Макросы, имеющие вид uint leastJV t, определяют целое значение без знака с разрядностью не менее чем N бит. Макросы, в имени которых N равно 8, 16, 32 или 64, доступны во всех средах, в которых предусмотрено выполнение операций над целыми числами с указанной разрядностью. Например, макрос int_leastl6_t определяет допустимый тип. Макросы, имеющие вид int_fast7V_t, определяют самый быстродействующий цело- численный тип с разрядностью не менее N бит. Макросы, имеющие вид uint f astTV t, определяют самый быстродействующий целочисленный тип без знака с разрядностью не менее чем ТУбит. Макросы, в имени которых ТУ равно 8, 16, 32 или 64, доступны во всех средах. Например, макрос int fast32_t — это допустимый тип значения для всех сред. Тип intmax_t определяет тип целого максимальной разрядности со знаком, а тип uintmax t — тип целого максимальной разрядности без знака. Глава 20. Библиотечные средства, добавленные в версии С99 431
Также определены типы intptr_t и uintptr_t. Их обычно используют для соз- дания целых значений, в которых можно хранить указатели. Эти типы не являются обязательными. В заголовке <stdint.h> определен ряд макросов с параметрами. Их макрорасши- рения являются константами заданного целочисленного типа. Эти макросы имеют следующую общую форму: I INTW_C(значение) I UINTN_C(значение) Здесь N — разрядность нужного типа в битах. Каждый макрос создает константу раз- рядностью не менее N бит, которая представляет заданное значение. Также в этом заголовке определены следующие макросы: I INTMAX_C(значение) I UINTMAX_C(значение) Они создают константы максимальной разрядности, представляющие заданное значение. Щ Функции для преобразования формата целочисленных значений В версии С99 добавлен ряд специализированных функций для преобразования формата целочисленных значений, которые позволяют преобразовывать целые значения в так на- зываемые значения максимальной разрядности и наоборот — уменьшать разрядность при необходимости. Эти функции описаны в заголовке <inttypes .h>, который включает так- же заголовок <stdint.h>. Заголовок <inttypes.h> определяет один тип: структуру imaxdiv t, в которой хранится значение, возвращаемое функцией imaxdiv (). Функции для преобразования формата целочисленных значений перечислены в табл. 20.3. В заголовке <inttypes.h> также определено множество макросов, которые можно использовать в вызовах функций семейств printf () и scanf () для задания различ- ных преобразований целых чисел. Макросы для функции printf () начинаются с префикса pri, а макросы для функции scanf О — с префикса SCN. За этими префик- сами стоит спецификатор преобразования, например d или и, затем следует имя типа (например, N, max, ptr, fastN или leastTV, где N задает разрядность). Точный спи- сок поддерживаемых макросов для задания различных преобразований целых чисел должен быть описан в документации к компилятору. Таблица 20.3. Функции для преобразования целочисленных значений в формат с максимальной разрядностью и функции, выполняющие обратные преобразования Функция_________________________________________________Описание intmax_t imaxabs (intmax_t arg) ; Возвращает абсолютное значение па* раметра агд imaxdiV— t imaxdiv (intmax_t numerator. Возвращает структуру imaxdivt, ко- intmax_t denominator); торая содержит результат выполнения операции деления числителя numerator на знаменатель denominator. Частное занимает поле quot, а остаток— поле rem. Как поле quot, так и поле rem имеют тип intmax_t 432 Часть III. Стандартная библиотека С
Окончание табл. 20.3 Функция Описание intmax_t strtoimax(const char ** restrict end, int base); char * restrict start, Версия функции strtol о для цело- численных параметров максимальной разрядности uintmax_t strtoumax(const char ** restrict end, int base); char * restrict start, Версия функции strtoul о для цело- численных параметров максимальной разрядности intmax_t wcstoimax(const char ** restrict end, int base); char * restrict start, Версия функции wcstolO для цело- численных параметров максимальной разрядности uintmax_t wcstoumax(const char ** restrict end, int base); char ★ restrict start, Версия функции wcstoulO для цело- численных параметров максимальной разрядности В Математические макросы обобщенного типа Как уже было сказано в главе 15, в Стандарте С99 определены три версии для боль- шинства математических функций — для параметров типа float, double и long double. Например, для вычисления синуса в стандарте С99 определены следующие функции: I double sin( double arg); float sinf(float arg) ; long double sinl(long double arg}; У всех трех функций одно и то же назначение, разница заключается лишь в типе обра- батываемых ими данных. Причем для всех функций версия, работающая с типом double, — это первоначальная функция, определенная в Стандарте С89, а версии для типов float и long double были добавлены в Стандарте С99. Как было отмечено в главе 15, имена функций для типа float имеют суффикс f, а имена функций для типа long double — суффикс 1. (Необходимость в применении различных имен вызвана тем, что язык С не поддерживает перегрузки функций.) Предоставляя три различные функции, стандарт С99 позволяет выбрать ту из них, которая более всего приемлема в каких-то кон- кретных условиях. По тем же причинам каждая из математических функций комплексного аргумента также представлена тремя версиями. Несмотря на очевидную полезность наличия трех версий математических функций и функций комплексных чисел, к сожалению, работать с ними не всегда удобно. Во- первых, при передаче данных определенного типа очень важно не забыть приписать к имени функции надлежащий суффикс. Постоянно помнить об этом довольно утоми- тельно, и потому повышается вероятность возникновения ошибок. Во-вторых, если в процессе разработки проекта изменить тип данных, передаваемых одной из таких функ- ций, следует изменить и суффикс в имени функции. А это, опять-таки, очень способст- вует “размножению” ошибок. Чтобы справиться с этими (и другими) проблемами, в Стандарте С99 определен набор макросов для обобщенного типа, которые можно ис- пользовать вместо математических или комплексных функций. Эти “универсальные” макросы автоматически транслируются в вызов нужной функции в зависимости от типа аргумента. Макросы обобщенного типа определены в заголовке <tgmath.h>, который автоматически включает заголовки <math.h> и <complex.h>. Макросы обобщенного типа имеют те же имена, что и версии математических или комплексных функций для типа double, в вызовы которых они транслируются. (Эти имена также совпадают с именами функций, определенными в стандарте С89.) На- пример, макрос обобщенного типа для функций sin (), sinf () и sinl () использует Глава 20. Библиотечные средства, добавленные в версии С99 433
имя sin (). “Универсальный” макрос для функций csin (), csinf () и csinl () также имеет имя sin(). Как уже упоминалось, соответствующая функция вызывается в за- висимости от типа аргумента. Предположим, например, что в программе определены следующие переменные: Ilong double Idbl ; float complex fcmplx; Тогда вызов | cos(Idbl) транслируется в вызов I cosl(Idbl), а вызов | cos (fcmplx) транслируется в вызов | ccosf(fcmplx). Как показано в приведенных выше примерах, макросы обобщенного типа предостав- ляют программисту удобное средство записи вызовов необходимых функций без потери производительности, точности или совместимости (переносимости) программного кода. S Заголовок <stdbool.h> В стандарт С99 добавлен заголовок <stdbool.h>, который поддерживает тип дан- ных _Воо1. Хотя в нем не определено ни одной функции, на самом деле он определяет следующие четыре макроса. Макрос Расширение bool _Воо1 true false bool_true_false_are_defined 1 0 1 В версии С99 вместо ключевого слова bool определено ключевое слово _Воо1, по- скольку во многих существующих С-программах уже определены собственные поль- зовательские версии типа bool. Определение в версии С99 логического (булева) типа в виде ключевого слова _Воо1 позволяет избежать переписывания созданного ранее программного кода. То же объяснение относится и к ключевым словам true и false. Однако при написании новых программ лучше всего включить в них заголовок <stdbool.h>, а затем использовать макросы bool, true и false. Благодаря этому вы сможете создавать программы, совместимые с языком C++. 434 Часть III. Стандартная библиотека С
Полный справочник по Часть IV Алгоритмы и приложения В части IV показано, как применить язык С для реше- ния разнообразных задач по разработке программ. На примере разработки алгоритмов и приложений демон- стрируется применение средств языка С. Многие при- меры, приведенные в части IV, могут использоваться на начальном этапе при разработке собственных проектов.
Полный справочник по Глава 21 Сортировка и поиск
В мире компьютеров сортировка и поиск принадлежат к числу наиболее распро- страненных и хорошо изученных задач. Процедуры сортировки и поиска исполь- зуются почти во всех программах управления базами данных, а также в компиляторах, интерпретаторах и операционных системах. В настоящей главе представлены основ- ные алгоритмы сортировки и поиска. Как вы сможете убедиться, они также иллюст- рируют некоторые важные приемы программирования на языке С. Вообще говоря, поскольку целью сортировки, является облегчение и ускорение поиска данных, алго- ритмы сортировки рассматриваются в первую очередь. Сортировка Сортировка — это упорядочивание набора однотипных данных по возрастанию или убыванию. Сортировка является одной из наиболее приятных для умственного анализа категорией алгоритмов, поскольку процесс сортировки очень хорошо опреде- лен. Алгоритмы сортировки были подвергнуты обширному анализу, и способ их рабо- ты хорошо понятен. К сожалению, вследствие этой изученности сортировка часто воспринимается как нечто само собой разумеющееся. При необходимости отсортиро- вать данные многие программисты просто вызывают стандартную функцию qsort (), входящую в стандартную библиотеку С. Однако различные подходы к сортировке об- ладают разными характеристиками. Несмотря на то, что некоторые способы сорти- ровки могут быть в среднем лучше, чем другие, ни один алгоритм не является идеаль- ным для всех случаев. Поэтому широкий набор алгоритмов сортировки — полезное добавление в инструментарий любого программиста. Будет полезно кратко остановиться на том, почему вызов qsort () не является универсальным решением всех задач сортировки. Во-первых, функцию общего назна- чения вроде qsort () невозможно применить во всех ситуациях. Например, qsort () сортирует только массивы в памяти. Она не может сортировать данные, хранящиеся в связанных списках. Во-вторых, qsort () - параметризованная функция, благодаря чему она может обрабатывать широкий набор типов данных, но вместе с тем вследст- вие этого она работает медленнее, чем эквивалентная функция, рассчитанная на ка- кой-то один тип данных. Наконец, как вы увидите, хотя алгоритм быстрой сортиров- ки, примененный в функции qsort (), очень эффективен в общем случае, он может оказаться не самым лучшим алгоритмом в некоторых конкретных ситуациях. Существует две общие категории алгоритмов сортировки: алгоритмы, сортирующие объекты с произвольным доступом (например, массивы или дисковые файлы произ- вольного доступа), и алгоритмы, сортирующие последовательные объекты (например, файлы на дисках и лентах или связанные списки1). В данной главе рассматриваются только алгоритмы первой категории, поскольку они наиболее полезны для среднеста- тистического программиста. Чаще всего при сортировке данных лишь часть их используется в качестве ключа сортировки. Ключ — это часть информации, определяющая порядок элементов. Таким образом, ключ участвует в сравнениях, но при обмене элементов происходит переме- щение всей структуры данных. Например, в списке почтовой рассылки в качестве ключа может использоваться почтовый индекс, но сортируется весь адрес. Для про- стоты в нижеследующих примерах будет производиться сортировка массивов симво- лов, в которых ключ и данные совпадают. Далее вы увидите, как адаптировать эти ме- тоды для сортировки структур данных любого типа. 1 В зависимости от этого сортировка называется внутренней или внешней. — Прим. ред. 438 Часть IV. Алгоритмы и приложения
В Классы алгоритмов сортировки Существует три общих метода сортировки массивов1: Обмен Выбор (выборка) Вставка Чтобы понять, как работают эти методы, представьте себе колоду игральных карт. Чтобы отсортировать карты методом обмена1, разложите их на столе лицом вверх и меняйте местами карты, расположенные не по порядку, пока вся колода не будет упо- рядочена. В методе выбора разложите карты на столе, выберите карту наименьшей значимости и положите ее в руку. Затем из оставшихся карт снова выберите карту наименьшей значимости и положите ее на ту, которая уже находится у вас в руке. Процесс повторяется до тех пор, пока в руке не окажутся все карты; по окончании процесса колода будет отсортирована. Чтобы отсортировать колоду методом вставки, возьмите все карты в руку. Выкладывайте их по одной на стол, вставляя каждую сле- дующую карту в соответствующую позицию. Когда все карты окажутся на столе, ко- лода будет отсортирована. Ш Оценка алгоритмов сортировки Существует много различных алгоритмов сортировки. Все они имеют свои поло- жительные стороны, но общие критерии оценки алгоритма сортировки таковы: Насколько быстро данный алгоритм сортирует информацию в среднем? Насколько быстро он работает в лучшем и худшем случаях? Естественно или неестественно он себя ведет? Переставляет ли он элементы с одинаковыми ключами?1 2 Давайте подробнее рассмотрим эти критерии. Очевидно, что скорость работы любого алгоритма сортировки имеет большое значение. Скорость сортировки3 массива непосред- ственно связана с количеством сравнений и количеством обменов, происходящих во время сортировки, причем обмены занимают больше времени. Сравнение происходит тогда, когда один элемент массива сравнивается с другим; обмен происходит тогда, когда два элемента меняются местами. Время работы одних алгоритмов сортировки растет экспоненциально, а время работы других логарифмически зависит от количества элементов. Время работы в лучшем и худшем случаях имеет значение, если одна из этих си- туаций будет встречаться довольно часто. Алгоритм сортировки зачастую имеет хоро- шее среднее время выполнения, но в худшем случае он работает очень медленно. Поведение алгоритма сортировки называется естественным, если время сортиров- ки минимально для уже упорядоченного списка элементов, увеличивается по мере возрастания степени неупорядоченности списка и максимально, когда элементы спи- ска расположены в обратном порядке. Объем работы алгоритма оценивается количе- ством производимых сравнений и обменов. 1 Т.е. обменной сортировкой. — Прим. ред. 2 Если в отсортированном массиве элементы с одинаковыми ключами идут в том же поряд- ке, в котором они располагались в исходном массиве, то алгоритм сортировки называется ус- тойчивым, а в противном случае — неустойчивым. — Прим. ред. 3 Синонимы: быстродействие, эффективность. Глава 21. Сортировка и поиск 439
Чтобы понять, почему переупорядочивание элементов с одинаковыми ключами имеет определенное значение, представьте себе базу данных почтовой рассылки, упорядоченную по главному ключу и подключу. Главным ключом является почтовый индекс, а в пределах одного почтового индекса записи упорядочены по фамилии. При добавлении в список но- вого адреса и пересортировке списка порядок подключей (то есть фамилий внутри почто- вых индексов) не должен меняться. Для гарантии, что это не произойдет, алгоритм сорти- ровки не должен обменивать ключи с одинаковым значением1. Далее будут представлены характерные для каждой группы алгоритмы сортировки с анализом эффективности. После них будут продемонстрированы более совершенные методы сортировки. S Пузырьковая сортировка Самый известный (и пользующийся дурной славой) алгоритм — пузырьковая сор- тировка (bubble sort, сортировка методом пузырька, или просто сортировка пузырьком)1 2. Его популярность объясняется интересным названием и простотой самого алгоритма. Тем не менее, в общем случае это один из самых худших алгоритмов сортировки. Пузырьковая сортировка относится к классу обменных сортировок, т.е. к классу сорти- ровок методом обмена. Ее алгоритм содержит повторяющиеся сравнения (т.е. многократ- ные сравнения одних и тех же элементов) и, при необходимости, обмен соседних элемен- тов. Элементы ведут себя подобно пузырькам воздуха в воде — каждый из них поднимает- ся на свой уровень. Простая форма алгоритма сортировки показана ниже: /* Пузырьковая сортировка */ void bubble(char *items, int count) { register int a, b; register char t; for(a=l; a < count; ++a) for(b=count-l; b >= a; —b) { if(items[b-1] > items[b]) { /* обмен элементов */ t = items[b-1]; items[b-1] = items[b]; items[b] = t; Здесь items — указатель на массив символов, подлежащий сортировке, a count — количество элементов в массиве. Работа пузырьковой сортировки выполняется в двух циклах. Если количество элементов массива равно count, внешний цикл приводит к просмотру массива count - 1 раз. Это обеспечивает размещение элементов в правиль- ном порядке к концу выполнения функции даже в самом худшем случае. Все сравне- ния и обмены выполняются во внутреннем цикле. (Слегка улучшенная версия алго- ритма пузырьковой сортировки завершает работу, если при просмотре массива не бы- ло сделано ни одного обмена, но это достигается за счет добавления еще одного сравнения при каждом проходе внутреннего цикла.) 1 Т.е. должен быть устойчивым. — Прим. ред. 2 На самом деле есть даже два алгоритма пузырьковой сортировки: сортировка пузырьковым вклю- чением и сортировка пузырьковой выборкой. Впрочем, эффективность обоих одинакова. — Прим. ред. 440 Часть IV. Алгоритмы и приложения
С помощью этой версии алгоритма пузырьковой сортировки можно сортировать массивы символов по возрастанию. Например, следующая короткая программа сорти- рует строку, вводимую пользователем: /* Программа, вызывающая функцию сортировки bubble */ #include <string.h> #include <stdio.h> #include <stdlib.h> void bubble(char *items, int count); int main(void) { char s[255]; printf (’’Введите строку: ”) ; gets (s); bubble(s, strlen(s)); printf (’’Отсортированная строка: %s.n”, s) ; return 0; } Чтобы наглядно показать, как работает пузырьковая сортировка, допустим, что ис- ходный массив содержит элементы dcab. Ниже показано состояние массива после ка- ждого прохода: Начало dcab Проход 1 a d с b Проход 2 a b d с Проход 3 abed При анализе любого алгоритма сортировки полезно знать, сколько операций сравнения и обмена будет выполнено в лучшем, среднем и худшем случаях. Поскольку характеристи- ки выполняемого кода зависят от таких факторов, как оптимизация, производимая компи- лятором, различия между процессорами и особенности реализации, мы не будем пытаться получить точные значения этих параметров. Вместо этого сконцентрируем свое внимание на общей эффективности каждого алгоритма. В пузырьковой сортировке количество сравнений всегда одно и то же, поскольку два цикла for повторяются указанное количество раз независимо от того, был список изначально упорядочен или нет. Это значит, что алгоритм пузырьковой сортировки всегда выполняет (л2 - л) /2 сравнений, где п — количество сортируемых элементов. Данная формула выведена на том основании, что внешний цикл выполняется п - 1 раз, а внутренний выполняется в среднем п/2 раз. Произведение этих величин и дает предыдущее выражение. Обратите внимание на член п2 в формуле. Говорят, что пузырьковая сортировка яв- ляется алгоритмом порядка п2, поскольку время ее выполнения пропорционально квад- рату количества сортируемых элементов. Необходимо признать, что алгоритм порядка п2 не эффективен при большом количестве элементов, поскольку время выполнения растет экспоненциально в зависимости от количества сортируемых элементов. На рис. 21.1 по- казан график роста времени сортировки с увеличением размера массива. В алгоритме пузырьковой сортировки количество обменов в лучшем случае равно нулю, если массив уже отсортирован. Однако в среднем и худшем случаях количество обменов также является величиной порядка п2. Глава 21. Сортировка и поиск 441
1000 900 - / 800 - / 700 - / S / g 600 - / о. / £ / о 500 - / к / | 400 - / 0Q / 300 - / 200 - / 100 - .X O' ----------------1---------।---------1--------1 200 400 600 800 1000 Количество записей n Рис. 21.1. Время сортировки порядка п2 в зависимости от размера массива. (На самом деле здесь нарисована кривая у= п2/1000, а не кривая у= п2, крутизна которой в 1000 раз вы- ше. Фактически это все равно, что нарисовать кривую у= п2, выбрав по оси ординат более мелкий масштаб (в 1000 раз). Начертить кривую у= п2 без растяжения вдоль оси абс- цисс, на которой откладываются значения п, практически невозможно. Дело в том, что при выбранном интервале из- менения п (от 0 до 1000) кривая у= п2 практически сливает- ся с осью ординат. — Прим, ред.) Алгоритм пузырьковой сортировки можно немного улучшить, если попытаться повы- сить скорость его работы. Например, пузырьковая сортировка имеет такую особенность: неупорядоченные элементы на “большом” конце массива (например, “а” в примере d с а Ь) занимают правильные положения за один проход, но неупорядоченные элементы в начале массива (например, “d”) поднимаются на свои места очень медленно. Этот факт подсказывает способ улучшения алгоритма. Вместо того чтобы постоянно просматривать массив в одном направлении, в последовательных проходах можно чередовать направле- ния. Этим мы добьемся того, что элементы, сильно удаленные от своих положений, быст- ро станут на свои места. Данная версия пузырьковой сортировки носит название шейкер- сортировки (shaker sort)1, поскольку действия, производимые ею с массивом, напоминают взбалтывание или встряхивание. Ниже показана реализация шейкер-сортировки. I/* Шейкер-сортировка */ void shaker(char *items, int count) < register int a; 1 А также сортировки перемешиванием (cocktail shaker sort), сортировки взбалтыванием, сорти- ровки встряхиванием. Как бы то ни было, это вид пузырьковой сортировки, в которой альтерна- тивные проходы выполняются в противоположном направлении. — Прим. ред. 442 Часть IV. Алгоритмы и приложения
int exchange; char t; do { exchange = 0; for(a=count-l; a > 0; —a) { if(items[a-1] > items [a]) { t = items[a-1]; items[a-1] = items [a]; items [a] = t; exchange = 1; } } for(a=l; a < count; ++a) { if(items[a-1] > items [a]) { t = items [a-1]; items[a-1] = items [a]; items [a] = t; exchange = 1; } } } while(exchange); /* сортировать до тех пор, пока не будет обменов */ } Хотя шейкер-сортировка и является улучшенным вариантом по сравнению с пу- зырьковой сортировкой, она по-прежнему имеет время выполнения порядка п2. Это объясняется тем, что количество сравнений не изменилось, а количество обменов уменьшилось лишь на относительно небольшую константу. Шейкер-сортировка луч- ше пузырьковой, но есть еще гораздо лучшие алгоритмы сортировки. В Сортировка посредством выбора При сортировке посредством выбора1 из массива выбирается элемент с наимень- шим значением и обменивается с первым элементом. Затем из оставшихся п - 1 эле- ментов снова выбирается элемент с наименьшим ключом и обменивается со вторым элементом, и т. д. Эти обмены продолжаются до двух последних элементов. Напри- мер, если применить метод выбора к массиву dcab, каждый проход будет выглядеть так, как показано ниже: Начало dcab Проход 1 а с d b Проход 2 a b d с Проход 3 abed Нижеследующий код демонстрирует простейшую сортировку посредством выбора: /* Сортировка посредством выбора. */ void select(char *items, int count) { register int a, b, c; int exchange; char t; for(a=0; a < count-1; ++a) { exchange - 0; 1 Называется также сортировкой выбором и сортировкой выборками. — Прим. ред. Глава 21. Сортировка и поиск. 443
с = a; t = items[a]; for(b=a+l; b < count; ++b) { if(items[b] < t) { c = b; t = items[b]; exchange = 1; } } if(exchange) { items[c] = items[a]; items[a] = t; } } } К сожалению, как и в пузырьковой сортировке, внешний цикл выполняется п - 1 раз, а внутренний — в среднем п/2 раз. Следовательно, сортировка посредством выбора требует 1/2 (л2 - п) сравнений. Таким образом, это алгоритм порядка л2, из-за чего он считается слишком медленным для сортировки большого количества элементов. Несмотря на то, что ко- личество сравнений в пузырьковой сортировке и сортировке посредством выбора оди- наковое, в последней количество обменов в среднем случае намного меньше, чем в пузырьковой сортировке. В Сортировка вставками Сортировка вставками — третий и последний из простых алгоритмов сортировки. Сна- чала он сортирует два первых элемента массива. Затем алгоритм вставляет третий элемент в соответствующую порядку позицию по отношению к первым двум элементам. После этого он вставляет четвертый элемент в список из трех элементов. Этот процесс повторя- ется до тех пор, пока не будут вставлены все элементы. Например, при сортировке массива dcab каждый проход алгоритма будет выглядеть следующим образом: Начало dcab Проход 1 с d а b Проход 2 а с d b Проход 3 abed Пример реализации сортировки вставками показан ниже: /* Сортировка вставками. */ void insert(char *items, int count) { register int a, b; char t; for(a=l; a < count; ++a) { t = items[a]; for(b=a-l; (b >= 0) && (t < items[b]); b—) items[b+1] = items[b]; items[b+1] = t; } } 444 Часть IV. Алгоритмы и приложения
В отличие от пузырьковой сортировки и сортировки посредством выбора, количе- ство сравнений в сортировке вставками зависит от изначальной упорядоченности спи- ска. Если список уже отсортирован, количество сравнений равно п - 1; в противном случае его производительность является величиной порядка п1 2. Вообще говоря, в худших случаях сортировка вставками настолько же плоха, как и пу- зырьковая сортировка и сортировка посредством выбора, а в среднем она лишь немного лучше. Тем не менее, у сортировки вставками есть два преимущества. Во-первых, ее пове- дение естественно. Другими словами, она работает меньше всего, когда массив уже упоря- дочен, и больше всего, когда массив отсортирован в обратном порядке. Поэтому сортиров- ка вставками — идеальный алгоритм для почти упорядоченных списков. Второе преиму- щество заключается в том, что данный алгоритм не меняет порядок одинаковых ключей1. Это значит, что если список отсортирован по двум ключам, то после сортировки вставками он останется упорядоченным по обоим. Несмотря на то, что количество сравнений при определенных наборах данных может быть довольно низким, при каждой вставке элемента на свое место массив необходимо сдвигать. Вследствие этого количество перемещений может быть значительным. Н Улучшенные алгоритмы сортировки Все алгоритмы, рассмотренные в предыдущих разделах, имеют один фатальный недостаток — время их выполнения имеет порядок п2. Это делает сортировку больших объемов данных очень медленной. По существу, в какой-то момент эти алгоритмы становятся слишком медленными, чтобы их применять2. К сожалению, страшные ис- тории о “сортировках, которые продолжались три дня”, зачастую реальны. Когда сор- тировка занимает слишком много времени, причиной этому обычно является неэф- фективность использованного в ней алгоритма. Тем не менее, первой реакцией в та- кой ситуации часто становится оптимизация кода вручную, возможно, путем переписывания его на ассемблере. Несмотря на то, что ручная оптимизация иногда ускоряет процедуру на постоянный множитель3, если алгоритм сортировки не эффек- тивен, сортировка всегда будет медленной независимо от того, насколько оптимально 1 Т.е. устойчив. — Прим. ред. 2 Конечно, это не означает, что функция Дл)=л2 в какой-то точке возрастает скачкообразно. Вовсе нет! Просто при увеличении размера массива п меняется характер сортировки, из внут- ренней она фактически становится внешней, когда массив не помещается в оперативной памяти и начинается интенсивная подкачка страниц, а за ней пробуксовывание механизма виртуальной памяти. Вот эти-то события действительно могут наступить внезапно, и тогда может показаться, что незначительное увеличение сортируемого массива или просто добавление какой-либо со- вершенно незначительной задачи приведет к катастрофическому увеличению времени сорти- ровки (например в десятки раз!). — Прим. ред. 3 Отдельные программисты — “любители рассказов о рыбной ловле” — клянутся об увели- чении эффективности сначала наполовину, затем вдвое-втрое, к середине рассказа — на поря- док, а к концу рассказа — на несколько порядков. (Такое не получается даже в специально по- добранных примерах для рекламного проспекта по языку Ассемблера.) На самом деле произво- дительность может даже упасть. В лучшем случае удается повысить ее на 10-12% для реально значимых производственных задач. При этом чем сложнее алгоритм, тем сложнее переписать его на Ассемблере и тем проще сделать в нем ошибку при переписывании, а тем более сложнее ее найти. Кроме того, следует учитывать и такой фактор: например, программу писал какой-то квалифицированный программист, который выбрал простой (но не очень эффективный — при- чем об этом он знал) алгоритм потому, что менеджеры настаивали на скорейшем завершении программы, а оптимизацию этой программы те же менеджеры поручат весьма не самым квали- фицированным специалистам! Эффект действительно будет на несколько порядков больше, но в совершенно противоположную сторону! Ведь с таким же успехом для “улучшения” трагедий Шекспира за пишущие машинки можно было усадить стадо обезьян! — Прим. ред. Глава 21. Сортировка и поиск 445
написан код. Следует помнить, если время работы процедуры пропорционально п то увеличение скорости кода или компьютера даст лишь небольшое улучшение1, по- скольку время выполнения увеличивается как п1 2. (На самом деле, кривая п2 на рис. 21.1 растянута вправо, но в остальном соответствует действительности.) Сущест- вует правило: если используемый в программе алгоритм слишком медленный сам по себе, никакой объем ручной оптимизации не сделает программу достаточно быстрой. Решение заключается в применении лучшего алгоритма сортировки. Ниже описаны два прекрасных метода сортировки. Первый называется сортиров- кой. Шелла. Второй — быстрая сортировка — обычно считается самым лучшим алго- ритмом сортировки. Оба метода являются более совершенными способами сортиров- ки и имеют намного лучшую общую производительность, чем любой из приведенных выше простых методов. Сортировка Шелла Сортировка Шелла называется так по имени своего автора, Дональда Л. Шелла (Donald Lewis Shell)2. Однако это название закрепилось, вероятно, также потому, что действие этого метода часто иллюстрируется рядами морских раковин, перекрываю- щих друг друга (по-английски “shell” — “раковина”). Общая идея заимствована из сортировки вставками и основывается на уменьшении шагов3. Рассмотрим диаграмму на рис. 21.2. Сначала сортируются все элементы, отстоящие друг от друга на три по- зиции. Затем сортируются элементы, расположенные на расстоянии двух позиций. Наконец, сортируются все соседние элементы. То, что этот метод дает хорошие результаты, или даже то, что он вообще сортирует массив, увидеть не так просто. Тем не менее, это верно. Каждый проход сортировки распространяется на относительно небольшое количество элементов либо на элемен- ты, расположенные уже в относительном порядке. Поэтому сортировка Шелла эффек- тивна, а каждый проход повышает упорядоченность4. 1 Действительно, чтобы увеличить в т раз размер сортируемого массива при сохранении времени сортировки, быстродействие процессора придется увеличить в т1 раз при условии, что время доступа к элементам массива не увеличится, т.е. не уменьшится, например, эффектив- ность подкачки страниц. — Прим. ред. 2 Считается, что Дональд Л. Шелл описал свой метод сортировки 28 июля 1959 года. Дан- ный метод классифицируется как слияние с обменом; часто называется также сортировкой с убы- вающим шагом. — Прим. ред. 3 Шаг — расстояние между сортируемыми элементами на конкретном этапе сортиров- ки. — Прим. ред. 4 Т.е. уменьшает количество беспорядков (инверсий). — Прим. ред. 446 Часть IV. Алгоритмы и приложения
Проход 1 f d а с be Проход 2 Проход 3 а b с е d f Результат a bed е f Рис. 21.2. Сортировка Шелла Конкретная последовательность шагов может быть и другой. Единственное правило со- стоит в том, чтобы последний шаг был равен 1. Например, такая последовательность: 9, 5, 3, 2, 1 дает хорошие результаты и применяется в показанной здесь реализации сортировки Шелла. Следует избегать последовательностей, которые являются степенями числа 2 — по математически сложным соображениям они уменьшают эффективность сорти- ровки (но сортировка по-прежнему работает!). /* Сортировка Шелла. */ void shell(char *items, int count) { register int i, j, gap, k; char x, a[5]; a[0]=9; a[l]=5; a[2]=3; a[3]=2; a[4]=l; for(k=0; k < 5; k++) { gap = a[k]; for(i=gap; i < count; ++i) { x = items [i]; for(j=i-gap; (x < items [j]) && (j >= 0); j=j-gap) items[j+gap] = items [j]; items [j+gap] = x; } } } Вы могли заметить, что внутренний цикл for имеет два условия проверки. Оче- видно, что сравнение x<iterns [j] необходимо для процесса сортировки. Выражение j>=0 предотвращает выход за границу массива items. Эти дополнительные проверки в некоторой степени понижают производительность сортировки Шелла. Глава 21. Сортировка и поиск 447
В слегка модифицированных версиях данного метода сортировки применяются специальные элементы массива, называемые сигнальными метками. Они не принад- лежат к собственно сортируемому массиву, а содержат специальные значения, соот- ветствующие наименьшему возможному и наибольшему возможному элементам1. Это устраняет необходимость проверки выхода за границы массива. Однако применение сигнальных меток элементов требует конкретной информации о сортируемых данных, что уменьшает универсальность функции сортировки. Анализ сортировки Шелла связан с очень сложными математическими задачами, которые выходят далеко за рамки этой книги. Примите на веру, что время сортировки пропорционально Л1’1 2 при сортировке п элементов2. А это уже существенное улучшение по сравнению с сортировками порядка п2. Чтобы понять, насколько оно велико, обратитесь к рис. 21.3, на котором показаны графики функций п2 и л1,2. Тем не менее, не стоит чрезмерно восхищаться сортировкой Шелла — быстрая сортировка еще лучше. Быстрая сортировка Быстрая сортировка, придуманная Ч. А. Р. Хоаром3 (Charles Antony Richard Ноаге) и названная его именем, является самым лучшим методом сортировки из представленных в данной книге и обычно считается лучшим из существующих в настоящее время алгорит- мом сортировки общего назначения. В ее основе лежит сортировка обменами — удиви- тельный факт, учитывая ужасную производительность пузырьковой сортировки! Быстрая сортировка построена на идее деления. Общая процедура заключается в том, чтобы выбрать некоторое значение, называемое компарандом (comparand)4, а затем разбить массив на две части. Все элементы, болыцие или равные компаранду, перемещаются на одну сторону, а меньшие — на другую. Потом этот процесс повторяется для каждой части до тех пор, пока массив не будет отсортирован. Например, если исходный массив состоит из символов fedacb, а в качестве компаранда используется символ d, первый проход быст- рой сортировки переупорядочит массив следующим образом: Начало fedacb Проход 1 b с a d е f 1 -оо и +оо. — Прим. ред. 2 Вообще говоря, время сортировки Шелла зависит от последовательности шагов. (Впрочем, минимум равен, конечно, п log2«.) Оптимальная последовательность не известна до сих пор. Дональд Кнут исследовал различные последовательности (не забыв и последовательность Фибо- наччи). Фактически он пришел к выводу, что в определении наилучшей последовательности есть какое-то “колдовство”. В 1969 г. Воган Пратт обнаружил, что если все шаги выбираются из множества чисел вида 2Р3Ч, меньших п, то время работы будет порядка «(log п)2. А.А. Папернов и Г.В. Стасевич в 1965 г. доказали, что максимальное время сортировки Шелла не превосходит О(п15), причем уменьшить показатель 1.5 нельзя. Большое число экспериментов с сортировкой Шелла провели Джеймс Петерсон и Дэвид Л. Рассел в Стэнфордском университете в 1971 г. Они пытались определить среднее число перемещений при 100<л<250 ООО для последовательно- сти шагов 2к-1. Наиболее подходящими формулами оказались 1.21л126 и .39л(1п л)-2.33л In п. Но при изменении диапазона п оказалось, что коэффициенты в представлении степенной функци- ей практически не изменяются, а коэффициенты в логарифмическом представлении изменяют- ся довольно резко. Поэтому естественно предположить, что именно степенная функция описы- вает истинное асимптотическое поведение сортировки Шелла. — Прим. ред. 3 Встречается также написание Ч. Э. Р. Хоор. — Прим. ред. 4 Компаранд — операнд в операции сравнения. Иногда называется также основой и критери- ем разбиения. — Прим. ред. 448 Часть IV. Алгоритмы и приложения
Рис. 21.3. Попытка наглядного представления кривых п2 и п1’2. Хотя вычертить эти кривые с точным соблюдением масштаба на каком- нибудь значимом для целей сортировки интервале изменения количест- ва записей (п), например, на интервале от 0 до 1000, не представляет- ся возможным, получить представление о поведении этих кривых мож- но с помощью графиков функций у=(п/100)2 и у=(п/100)12. Для сравне- ния построен также график прямой у=п/100. Кроме того, чтобы получить представление о росте этих кривых, можно на оси ординат принять логарифмический масштаб, — это все равно, что начертить логарифмы этих функций Глава 21. Сортировка и поиск 449
Затем сортировка повторяется для обеих половин массива, то есть Ьса и def. Как вы видите, этот процесс по своей сути рекурсивный, и, действительно, в чистом виде быстрая сортировка реализуется как рекурсивная функция1. Значение компаранда можно выбирать двумя способами — случайным образом ли- бо усреднив небольшое количество значений из массива. Для оптимальной сортиров- ки необходимо выбирать значение, которое расположено точно в середине диапазона всех значений. Однако для большинства наборов данных это сделать непросто. В худшем случае выбранное значение оказывается одним из крайних. Тем не менее, да- же в этом случае быстрая сортировка работает правильно. В приведенной ниже вер- сии быстрой сортировки в качестве компаранда выбирается средний элемент массива. /* Функция, вызывающая функцию быстрой сортировки. */ void quick(char *items, int count) { qs(items, 0, count-1); } /* Быстрая сортировка. */ void qs(char *items, int left, int right) { register int i, j; char x, y; i = left; j = right; x = items [(left+right)/2]; /* выбор компаранда */ do { while((items[i] < x) && while((x < items[j]) && (i < right)) i++; (j > left)) j — ; if(i <= j) { у = items[i] ; items[i] = items[j]; items[j] = y; i++; j —; } } while(i <= j); if(left < j) qs(items, left, j); if(i < right) qs(items, i, right); В этой версии функция quick() готовит вызов главной сортирующей функции qs (). Это обеспечивает общий интерфейс с параметрами items и count, но несущественно, так как можно вызывать непосредственно функцию qs () с тремя аргументами. Получение количества сравнений и обменов, которые выполняются при быстрой сортировке, требует математических выкладок, которые выходят за рамки данной книги. Тем не менее, среднее количество сравнений равно п log п а среднее количество обменов примерно равно п/6 log п 1 Если хотите избежать рекурсии, не волнуйтесь, все очень легко переписывается даже для Фортрана IV, в упомянутой ранее литературе вы без труда найдете нужный нерекурсивный ва- риант. — Прим. ред. 450 Часть IV. Алгоритмы и приложения
Эти величины намного меньше соответствующих характеристик рассмотренных ранее алгоритмов сортировки. Необходимо упомянуть об одном особенно проблематичном аспекте быстрой сорти- ровки. Если значение компаранда в каждом делении равно наибольшему значению, бы- страя сортировка становится “медленной сортировкой” со временем выполнения поряд- ка п2. Поэтому внимательно выбирайте метод определения компаранда. Этот метод час- то определяется природой сортируемых данных. Например, в очень больших списках почтовой рассылки, в которых сортировка происходит по почтовому индексу, выбор прост, потому что почтовые индексы довольно равномерно распределены — компаранд можно определить с помощью простой алгебраической функции. Однако в других базах данных зачастую лучшим выбором является случайное значение. Популярный и доволь- но эффективный метод — выбрать три элемента из сортируемой части массива и взять в качестве компаранда значение, расположенное между двумя другими. 1 Выбор метода сортировки Каждый программист должен располагать широким набором алгоритмов сортиров- ки. Несмотря на то, что в среднем случае оптимальной является именно быстрая сор- тировка, она не является лучшей во всех случаях. Например, при сортировке очень маленьких списков (например, менее 100 элементов) дополнительный объем работы, создаваемый рекурсивными вызовами быстрой сортировки, может перекрыть пре- имущества ее более хорошего алгоритма. В таких редких случаях один их простых ме- тодов сортировки — возможно, даже пузырьковая сортировка — может работать быст- рее. Кроме того, если известно, что список уже почти упорядочен или если вы не хо- тите переставлять одинаковые ключи, какой-либо другой алгоритм подойдет лучше, чем быстрая сортировка. Суть сказанного заключается в том, что лишь тот факт, что быстрая сортировка является лучшим алгоритмом общего назначения, не означает, что в конкретных случаях другие подходы не дадут лучших результатов. И Сортировка других структур данных До сих пор мы сортировали только массивы символов. Очевидно, что приведенные выше функции можно переделать для сортировки массивов любого из встроенных ти- пов данных, просто поменяв типы параметров и переменных. Тем не менее, обычно возникает необходимость сортировать составные типы данных, например строки, или агрегированные данные, например структуры. Большинство задач сортировки имеют дело с ключом и информацией, связанной с этим ключом. Чтобы адаптировать алго- ритмы для обработки подобных данных, необходимо модифицировать код сравнения, код обмена или оба фрагмента. Сам алгоритм при этом не меняется. Поскольку быстрая сортировка в настоящее время является одним из лучших методов сортировки общего назначения, она используется в последующих примерах. Тем не менее, тот же принцип относится и ко всем остальным методам, описанным ранее. Сортировка строк Сортировка строк является распространенной задачей программирования. Строки легче всего сортировать, когда они хранятся в таблице строк. Таблица строк — это просто массив строк. А массив строк — это двумерный массив символов, в котором количество строк в таблице определяется размером левого измерения, а максимальная длина строки — размером правого измерения. (О массивах строк рассказывалось в Глава 21. Сортировка и поиск 451
главе 4.) Нижеследующая строковая версия быстрой сортировки принимает массив строк, в котором размер каждой строки ограничен десятью символами. (Можете из- менить эту длину, если хотите.) Данная версия сортирует строки в лексикографиче- ском порядке. /* Быстрая сортировка строк. */ void quick_string(char items[][10], int count) { qs_string(items, 0, count-1); } void qs_string(char items[][10], int left, int right) { register int i, j; char *x; char temp[10]; i = left; j = right; x = items[(left+right)/2]; do { while((strcmp(items[i],x) < 0) && (i < right)) i++; while((strcmp(items[j],x) > 0) && (j > left)) j—; if(i <= j) { stropy(temp, items[i]); strcpy(items[i], items [j]); stropy(items[j], temp); i++; j —; } } while(i <= j); if(left < j) qs_string(items, left, j); if(i < right) qs_string(items, i, right); Обратите внимание, что во фрагменте сравнения теперь используется функция strcmp (). Эта функция возвращает отрицательное число, если первая строка лекси- кографически меньше второй, возвращает ноль, если строки равны, и положительное число, если первая строка лексикографически больше второй. Также следует отметить, что для обмена двух строк требуется три вызова функции strcpy (). Имейте в виду, что функция strcmp () замедляет сортировку по двум причинам. Во-первых, в программе появляется вызов функции, что всегда отнимает время. Во- вторых, сама функция strcmp () выполняет несколько сравнений, чтобы определить, какая из двух строк больше. В первом случае, если скорость очень важна, можно по- местить код сравнения строк непосредственно в функцию сортировки, продублировав код функции strcmpO. Во втором случае нет никакого способа избежать сравнения строк, поскольку по определению это именно то, что требуется в данной задаче. Те же рассуждения относятся и к функции strcpy(). Обмен двух строк с помощью strcpy () включает в себя вызов функции и посимвольный обмен содержимого строк — каждая из этих операций занимает время. Накладные расходы на вызов функции можно устранить, вставив код копирования прямо в алгоритм сортировки. Однако тот факт, что обмен двух строк означает обмен отдельных символов (один за другим), изменить невозможно. Ниже приведена простая функция main (), демонстрирующая работу функции бы- строй сортировки строк quick_string (): 452 Часть IV. Алгоритмы и приложения
#include <stdio.h>. #include <string.h> void quiok_string(char items[][10], int count); void qs_string(char items [] [10], int left, int right); char str[][10] = { "один", "два", "три", "четыре" }; int main(void) { int i; quick_string(str, 4); for(i=0; i<4; i++) printf("%s ", str[i]); return 0; } Сортировка структур В большинстве прикладных программ, в которых используется сортировка, преду- смотрена сортировка совокупностей данных. Например, списки почтовой рассыпки, складские базы данных и журналы сотрудников содержат наборы разнотипных дан- ных. Как вам известно, в программах на языке С совокупности данных обычно хра- нятся в структурах. Хотя структура обычно содержит несколько членов, структуры, как правило, сортируются только по одному полю-члену, который используется в ка- честве ключа сортировки. За исключением выбора ключа, приемы сортировки струк- тур ничем не отличаются от приемов сортировки других типов данных. Чтобы проиллюстрировать пример сортировки структур, давайте создадим структу- ру под называнием address, в которой можно хранить почтовый адрес. Подобная структура может применяться в программе почтовой рассылки. Описание структуры address показано ниже: struct address { char name[40]; /* имя ★ / char street[40]; /* улица */ char city[20]; /* город */ char state[3]; /* штат */ char zip[ll]; /* индекс */ }; Поскольку представляется разумным организовать список адресов в виде массива структур, в данном примере предположим, что процедура сортировки будет сортиро- вать массив структур типа address. Такая процедура показана ниже. Она сортирует адреса по почтовому индексу. /* Быстрая сортировка структур типа address. */ void quick_struct(struct address items[], int count) { qs_struct(items,0,count-1) ; } -4 void qs_struct(struct address items [], int left, int right) Глава 21. Сортировка и поиск 453
register int i, j; char *x; struct address temp; i = left; j = right; x = items [(left+right)/2].zip; /* сортировка по почтовому индексу */ do { while ( (strcmp(items[i].zip,x) < 0) && (i < right)) i++; while (• (strcmp (items [ j ]. zip, x) > 0) && (j > left)) j — ; if (i <= j) { temp = items [i]; items[i] = items[j]; items[j] = temp; i++; j —; } } while(i <= j ) ; if(left < j) qs_struct(items, left, j); if(i < right) qs_struct(items, i, right); sl Сортировка дисковых файлов с произвольной выборкой Дисковые файлы бывают двух типов: с последовательной выборкой (доступом) и с произвольной выборкой. Если дисковый файл любого типа достаточно мал, его можно считать в память и отсортировать одной из процедур сортировки массивов, представ- ленных выше. Однако многие дисковые файлы слишком велики для того, чтобы сор- тировать их в памяти, а поэтому требуют особенных приемов сортировки. Многие приложения баз данных работают с файлами с произвольной выборкой. В данном разделе показан один способ сортировки таких файлов. Дисковые файлы с произвольной выборкой доступа имеют два больших преимущества перед файлами с последовательной выборкой. Во-первых, с ними легко работать. Для об- новления информации не нужно копировать весь список. Во-вторых, их можно рассмат- ривать как очень большой массив на диске, что значительно упрощает сортировку. Тот факт, что файл с произвольной выборкой можно рассматривать как массив, означает, что к нему можно применить быструю сортировку лишь с небольшим коли- чеством изменений. Вместо обращения к элементам массива по индексу, дисковая версия быстрой сортировки должна пользоваться функцией f seek (), чтобы найти со- ответствующую запись на диске. В каждой реальной ситуации сортировка определяется конкретной структурой сор- тируемых данных и ключом сортировки. Тем не менее, общую идею сортировки дис- ковых файлов с произвольной выборкой можно понять на примере короткой про- граммы, сортирующей структуры типа address — почтовые структуры, описанные ранее. Эта программа, приведенная ниже, сначала создает дисковый файл, содержа- щий неупорядоченные адреса. Затем она сортирует этот файл. Количество адресов, подлежащих сортировке, указано константой NUM_ELEMENTS (которая равна 4 в дан- ной программе). Однако в реальной жизни количество записей придется отслеживать 454 Часть IV. Алгоритмы и приложения
динамически. Поэкспериментируйте с этой программой самостоятельно, пробуя раз- личные типы структур, содержащие разнообразные данные: /* Дисковая сортировка структур типа address. */ #include <stdio.h> ttinclude <stdlib.h> ttinclude <string.h> #define NUM_ELEMENTS 4 /* Количество элементов — произвольное число, которое должно определяться динамически для каждого списка. */ struct address { char name[30]; char street[40]; char city[20]; char state[3]; char zip[11]; } ainfo; struct address addrs[NUM_ELEMENTS] = { "A. Alexander", "101 1st St", "Olney", "Ga", "55555", "B. Bertrand", "22 2nd Ave", "Oakland", "Pa", "34232", "C. Carlisle", "33 3rd Blvd", "Ava", "Or", "92000", "D. Dodger", "4 Fourth Dr", "Fresno", "Mi", "45678" }; void quick_disk(FILE *fp, int count); void qs_disk(FILE *fp, int left, int right); void swap_all_fields(FILE *fp, long i, long j); char *get_zip(FILE *fp, long rec); int main(void) { FILE *fp; /* сначала создадим файл с данными, подлежащий сортировке */ if((fp=fopen("mlist", "wb"))==NULL) { printf("Невозможно открыть файл на запись.п"); exit(1); } printf("Запись неупорядоченных данных в дисковый файл.п"); fwrite(addrs, sizeof(addrs), 1, fp); fclose(fp); /* теперь отсортируем файл */ if((fp=fopen("mlist", "rb+"))==NULL) { printf("Невозможно открыть файл на чтение/запись.n"); exit(1); } printf("Сортировка дискового файла.n"); quick_disk(fp, NUM_ELEMENTS); fclose(fp); printf("Файл отсортирован.n"); return 0; Глава 21. Сортировка и поиск 455
/★ Быстрая сортировка файлов. */ void quick_disk(FILE *fp, int count) { qs_disk(fp, 0, count-1); void qs_disk(FILE *fp, int left, int right) { long int i, j; char x[100]; i = left; j = right; strcpy(x, get_zip(fp, (long)(i+j)/2)); /* получить' средний почтовый индекс */ do { while((strcmp(get_zip(fp, i) , х) < 0) && (i < right)) i++; ' while((strcmp(get_zip(fp, j),x) > 0) && (j > left)) j — ; if(i <= j) { swap_all_fields(fp, i, j); i++; j —; } } while(i <= j) ; if(left < j) qs_disk(fp, left, (int) j); if(i < right) qs_disk(fp, (int) i, right); void swap_all_fields(FILE *fp, long i, long j) { char a[sizeof(ainfo)], b[sizeof(ainfo)]; /* считать в память записи i и j */ fseek(fp, sizeof(ainfo)*i, SEEK_SET); fread(a, sizeof(ainfo), 1, fp); fseek(fp, sizeof(ainfo)*j, SEEK_SET); fread(b, sizeof(ainfo), 1, fp); /* потом записать их на диск, поменяв местами */ fseek(fp, sizeof(ainfo)*j, SEEK_SET); fwrite(a, sizeof(ainfo), 1, fp); fseek(fp, sizeof(ainfo)*i, SEEK_SET); fwrite(b, sizeof(ainfo), 1, fp); /* Получение указателя на почтовый код записи */ char *get_zip(FILE *fp, long rec) { struct address *p; p = &ainfo; fseek(fp, rec*sizeof(ainfo), SEEK_SET); 456 Часть'IV. Алгоритмы и приложения
I fread(p, sizeof(ainfo)r 1, fp); I return ainfo.zip; I } Как вы, наверное, уже заметили, для сортировки адресных записей пришлось на- писать две вспомогательные функции. Во фрагменте сравнения используется функция get_zip(), возвращающая указатель на поле zip сравниваемых записей. Функция swap_all_fields () выполняет обмен данных двух записей. Порядок операций чте- ния и записи оказывает большое влияние на скорость выполнения сортировки. При обмене двух записей программа перемещает указатель текущей записи в файле снача- ла на запись i, а потом на запись j. Пока головка диска находится над записью j, за- писываются данные записи j. Это значит, что в этот момент головку не придется пе- ремещать на большое расстояние. Если бы код был составлен так, что первой записы- валась бы запись i, понадобилось бы еще одно перемещение головки. И Поиск Базы данных существуют для того, чтобы время от времени пользователи могли найти нужную запись, введя ее ключ. Существует один метод поиска информации в неупорядоченном массиве, и другой для поиска в упорядоченном массиве. В набор стандартной библиотеки компиляторов языка С входит стандартная функция bsearchO. Тем не менее, как и в случае сортировки, процедуры общего назначения иногда совсем не эффективны при использовании в критических ситуациях из-за на- кладных расходов, связанных с их обобщением. Кроме того, функцию bsearchO не- возможно применить к неупорядоченным данным. Методы поиска Для нахождения информации в неупорядоченном массиве требуется последова- тельный поиск, начинающийся с первого элемента и заканчивающийся при обнару- жении подходящих данных либо при достижении конца массива. Этот метод приме- ним для неупорядоченной информации, но также можно использовать его и на отсор- тированных данных. Однако если данные уже отсортированы, можно применить двоичный поиск, который находит данные быстрее. Последовательный поиск Последовательный поиск очень легко запрограммировать. Приведенная ниже функция осуществляет поиск в массиве символов известной длины, пока не будет найден элемент с заданным ключом: /* Последовательный поиск */ int sequential_search(char *items, int count, char key) { register int t; for(t=0; t < count; ++t) if(key == items [t]) return t; return -1; /* ключ не найден */ } Здесь items — указатель на массив, содержащий информацию. Функция возвращает индекс подходящего элемента, если таковой существует, либо -1 в противном случае. Глава 21. Сортировка и поиск 457
Понятно, что последовательный поиск в среднем просматривает п/2 элементов. В лучшем случае он проверяет только один элемент, а в худшем — п. Если информация хранится на диске, поиск может занимать продолжительное время. Но если данные не упорядочены, последовательный поиск — единственно возможный метод. Двоичный поиск Если данные, в которых производится поиск, отсортированы, для нахождения эле- мента можно применять метод, намного превосходящий предыдущий — двоичный по- иск1. В нем применяется метод половинного деления. Сначала проверим средний элемент. Если он больше, чем искомый ключ, проверим средний элемент первой по- ловины, в противном случае — средний элемент второй половины. Будем повторять эту процедуру до тех пор, пока искомый элемент не будет найден либо пока не оста- нется очередного элемента. Например, чтобы найти число 4 в массиве 123456789 при двоичном поиске сначала проверяется средний элемент — число 5. Поскольку оно больше, чем 4, поиск продолжается в первой половине: 1 2 3 4 5 Средний элемент теперь равен 3. Это меньше, чем 4, поэтому первая половина отбра- сывается. Поиск продолжается в части 45 На этот раз искомый элемент найден. В двоичном поиске количество сравнений в худшем случае равно log2n В среднем случае количество немного ниже, а в лучшем — количество сравнений равно 1. Ниже приведена функция двоичного поиска для массивов символов. Этот поиск мож- но адаптировать для произвольных структур данных, изменив фрагмент сравнения. /* Двоичный поиск. */ int binary_search(char *items, int count, char key) { int low, high, mid; low = 0; high = count-1; while(low <= high) { mid = (low+high)/2; if(key < items[mid]) high = mid-1; else if(key > items[mid]) low = mid+1; else return mid; /* ключ найден */ } return -1; } 1 Есть и другие названия: дихотомический поиск, логарифмический поиск, поиск делением пополам. Этот метод поиска данных состоит в том, что все множество данных делится пополам и определяется, в какой из половин находится искомое данное, после чего половина, в которой находится данное, в свою очередь делится пополам и т.д. Процесс продолжается до тех пор, пока очередное полученное множество не станет равным единственному данному, которое будет искомым, либо будет установлен факт отсутствия искомого данного в этом множестве. — Прим. ред. 458 Часть IV. Алгоритмы и приложения
Полный справочник по Глава 22 Очереди, стеки, связанные списки и деревья
Как известно, программы состоят из двух частей — алгоритмов и структур данных. В хорошей программе эти составляющие эффективно дополняют друг друга. Вы- бор и реализация структуры данных насколько же важны, как и процедуры для обра- ботки данных. Способ организации и доступа к информации обычно определяется природой программируемой задачи. Таким образом, для программиста важно иметь в своем распоряжении приемы, подходящие для различных ситуаций. Степень привязки типа данных к своему машинному представлению находится в об- ратной зависимости от его абстракции. Другими словами, чем более абстрактными стано- вятся типы данных, тем больше концептуальное представление о способе хранения этих данных отличается от реального, фактического способа их хранения в памяти компьютера. Простые типы, например, char или int, тесно связаны со своим машинным представле- нием. Например, машинное представление целочисленного значения хорошо аппрокси- мирует соответствующую концепцию программирования. По мере своего усложнения ти- пы данных становятся концептуально менее похожими на свои машинные эквиваленты. Так, действительные числа с плавающей точкой более абстрактны, чем целые числа. Фак- тическое представление типа float в машине весьма приблизительно соответствует пред- ставлению среднего программиста о действительном числе. Еще более абстрактной являет- ся структура, принадлежащая к составным типам данных. На следующем уровне абстракции сугубо физические аспекты данных отходят на второй план вследствие введения механизма доступа (data engine) к данным, то есть механизма сохранения и получения информации. По существу, физические данные свя- зываются с механизмом доступа, который управляет работой с данными из програм- мы. Именно механизмам доступа к данным и посвящена эта глава. Существует четыре механизма доступа: Очередь (queue) Стек (stack)1 Связанный список (linked list)1 2 Двоичное дерево (binary tree)3 Каждый из этих методов дает возможность решать задачи определенного класса. Эти методы по существу являются механизмами, выполняющими определенные операции со- хранения и получения передаваемой им информации на основе получаемых ими запросов. Они все сохраняют и получают элемент, здесь под элементом подразумевается информа- ционная единица. В этой главе показано, как создавать такие механизмы доступа на языке С. При этом проиллюстрированы некоторые распространенные приемы программирова- ния в С, включая динамическое выделение памяти и использование указателей. И Очереди Очередь — это линейный список информации, работа с которой происходит по принципу “первым пришел — первым вышел” (first-in, first-out); этот принцип (и очередь как структура данных) иногда еще называется FIFO4. Это значит, что первый помещенный в очередь элемент будет получен из нее первым, второй помещенный 1 Другие названия: магазин, стековая память, магазинная память, память магазинного типа, запоминающее устройство магазинного типа, стековое запоминающее устройство. — Прим. ред. 2 Другие названия: цепной список, список с использованием указателей, список со ссылками, список на указателях. — Прим. ред. 3 Другие названия: дерево двоичного поиска. — Прим. ред. 4 Этот принцип имеет и другие названия: “первым пришел — первым обслужен”, “в порядке поступления”, ''первым пришел — первым вышел”, "обратного магазинного типа”. — Прим. ред. 460 Часть IV. Алгоритмы и Приложения
элемент будет извлечен Вторым и т.д. Это единственный способ работы с очередью; произвольный доступ к отдельным элементам не разрешается. Очереди очень часто встречаются в реальной жизни, например, около банков или ресторанов быстрого обслуживания. Чтобы представить себе работу очереди, давайте введем две функции: qstoreO и qretrieveO (от “store”— “сохранять”, “retrieve” — “получать”). Функция qstoreO помещает элемент в конец очереди, а функция qretrieve () удаляет элемент из начала очереди и возвращает его значение. В табл. 22.1 показано действие последовательности таких операций. Следует иметь в виду, что операция извлечения удаляет элемент из очереди и уничтожает его, если он не хранится где-нибудь в другом месте. Поэтому после из- влечения всех элементов очередь будет пуста. В программировании очереди применяются при решении многих задач. Один из наиболее популярных видов таких задач — симуляция. Очереди также применяются в планировщиках задач операционных систем и при буферизации ввода/вывода. Чтобы проиллюстрировать работу очереди, мы напишем простую программу пла- нирования встреч. Эта программа позволяет сохранять информацию о некотором ко- личестве встреч; потом по мере прохождения каждой встречи она удаляется из списка. Для упрощения описание встреч ограничено 255 символами, а количество встреч — произвольным числом 100. При разработке этой простой программы планирования необходимо прежде всего реализовать описанные здесь функции qstore () и qretrieve (). Они будут хранить указатели на строки, содержащие описания встреч. Таблица 22.1. Работа очереди Действие Содержимое очереди qstore(A) А qstore(B) АВ qstore(C) АВС qretrieve() возвращает A ВС qstore(D) BCD qretrieveO возвращает В CD qretrieveO возвращает С D #define МАХ 100 char *р[МАХ]; int spos = 0; int rpos = 0; /* Сохранение встречи. */ void qstore(char *q) { if(spos==MAX) { printf("Список переполненп"); return; } p[spos] = q; spos++; /* Получение встречи. */ char *qretrieve() { if(rpos==spos) { Глава 22. Очереди, стеки, связанные списки и деревья 461
printf("Встреч больше нет.п"); return ’’; } rpos++; return p[rpos-l]; } Обратите внимание, что этим двум функциям требуются две глобальные перемен- ные: spos, в которой хранится индекс следующего свободного места в списке, и rpos, в которой хранится индекс следующего элемента, подлежащего выборке. С по- мощью этих функций можно организовать очередь данных другого типа, просто по- меняв базовый тип обрабатываемого ими массива. Функция qstore () помещает описания новых встреч в конец списка и проверяет, не переполнен ли список. Функция qretrieveO извлекает встречи из очереди, если таковые имеются. При назначении встреч увеличивается значение переменной spos, а по мере их прохождения увеличивается значение переменной rpos. По существу, rpos “догоняет” spos в очереди. На рис 22.1 показано, что может происходить в па- мяти при выполнении программы. Если rpos и spos равны, назначенные события отсутствуют. Даже несмотря на то, что функция qretrieveO не уничтожает храня- щуюся в очереди информацию физически, эту информацию можно считать уничто- женной, так как повторно получить доступ к ней невозможно. Рис. 22.1. Индекс выборки “догоняет ” индекс вставки 462 Часть IV. Алгоритмы и приложения
Текст программы простого планировщика встреч целиком приведен ниже. Вы мо- жете доработать эту программу по своему усмотрению. /* Мини-планировщик событий */ #include <string.h> #include <stdlib.h> #include <stdio.h> #include <ctype.h> #define MAX 100 char *p[MAX], *qretrieve(void) ; int spos =0; > int rpos = 0; void enter(void), qstore(char *q) , review(void), delete_ap(void); int main(void) { char s [80]; register int t; for(t=0; t < MAX; ++t) p[t] = NULL; /* инициализировать массив пустыми указателями */ for(;;) { printf (’’Ввести (E) , Список (L) , Удалить (R) , Выход (Q) : ”); gets (s); *s = toupper(*s); switch(*s) { case ’E’: enter(); break; case ’L’: review(); break; case ’R’: delete_ap(); break; case ’Q’: exit(0); } } return 0; } /* Вставка в очередь новой встречи. */ void enter(void) { char s [256], *p; do { printf("Введите встречу %d: ", spos+1); gets (s); if(*s=-0) break; /* запись не была произведена */ р = (char *) malloc(strlen(s)+1); if(!p) { Глава 22. Очереди, стеки, связанные списки и деревья 463
printf("Не хватает памяти.n"); return; } strcpy(p, s) ; if(*s) qstore(p); } while(*s); /* Просмотр содержимого очереди. */ void review(void) { register int t; for(t=rpos; t < spos; ++t) printf("%d. %sn", t+1, p[t]); } /* Удаление встречи из очереди. */ void delete_ap(void) { char *p; if((p=qretrieve())==NULL) return; printf("%sn", p); /* Вставка встречи. */ void qstore(char *q) { if(spos==MAX) { printf("Список полонп"); return; } p[spos] = q; spos++; } /* Извлечение встречи. */ char *qretrieve(void) { if(rpos==spos) { printf("Встреч больше нет.п"); return NULL; } rpos++; return p[rpos-l]; S Циклическая очередь При изучении предыдущего примера программы планирования встреч, вероятно, вам в голову мог прийти следующий способ ее улучшения: при достижении конца массива, в котором хранится очередь, можно не останавливать программу, а устанав- ливать индексы вставки (spos) и извлечения (rpos) так, чтобы они указывали на на- чало массива. Это позволит помещать в очередь любое количество элементов при ус- ловии их своевременного извлечения. Такая реализация очереди называется цикличе- 464 Часть IV. Алгоритмы и приложения
ской очередью, поскольку массив используется так, будто он представляет собой не линейный список, а кольцо. Чтобы организовать в программе-планировщике циклическую очередь, функции qstore () и qretrieve () необходимо переписать следующим образом: void qstore(char *q) { /* Очередь переполняется, когда spos на единицу меньше rpos, либо когда spos указывает на конец массива, a rpos — на начало. */ if(spos+l==rpos || (spos+l==MAX && !rpos)) { printf("Список полонп"); return; p[spos] = q; spos++; if(spos==MAX) spos = 0; /* установить на начало */ } char *qretrieve(void) { if(rpos==MAX) rpos = 0; /* установить на начало */ if(rpos==spos) { printf("Встреч больше нет.п"); return NULL; } rpos++; return p[rpos-l]; В данной версии программы очередь переполняется, когда индекс записи находит- ся непосредственно перед индексом извлечения; в противном случае еще есть место для вставки события. Очередь пуста, когда rpos равняется spos. Вероятно, чаще всего циклические очереди применяются в операционных систе- мах для хранения информации, считываемой и записываемой в дисковые файлы или на консоль. Циклические очереди также используются в программах обработки реаль- ного времени, которые должны продолжать обрабатывать информацию, буферизируя при этом запросы на ввод/вывод. Многие текстовые процессоры используют этот прием во время переформатирования абзаца или выравнивания строки. Вводимый текст не отображается на экране до окончания процесса. Для этого прикладная про- грамма должна проверять нажатие клавиш во время выполнения другой задачи. Если какая-либо клавиша была нажата, введенный символ быстро помещается в очередь, и процесс продолжается. После его завершения символы извлекаются из очереди. Чтобы ощутить на практике данное применение циклических очередей, давайте рассмотрим простую программу, состоящую из двух процессов. Первый процесс в программе выводит на экран числа от 1 до 32 000. Второй процесс помещает символы в очередь по мере их ввода, не отображая их на экране, пока пользователь не нажмет <Enter>. Вводимые символы не отображаются, поскольку первому процессу дан при- оритет в отношении вывода на экран. После нажатия <Enter> символы из очереди из- влекаются и печатаются. Чтобы программа работала, как описано выше, в ней необходимо использовать две функции, не определенные в стандартном языке С: _kbhit () и _getch (). Функция _kbhit() возвращает значение ИСТИНА, если на клавиатуре была нажата клавиша; в противном случае она возвращает ЛОЖЬ. Функция _getch () считывает введенный сим- Глава 22. Очереди, стеки, связанные списки и деревья 465
вол, но не дублирует его на экране. В стандарте языка С не предусмотрены функции для проверки состояния клавиатуры или считывания символов без отображения на экране, по- скольку эти функции зависят от операционной системы. Тем не менее, в большинстве библиотек компиляторов есть функции, выполняющие данные задачи. Приведенная здесь небольшая программа компилируется с помощью компилятора Microsoft. /* Пример циклической очереди в качестве буфера клавиатуры. */ #include <stdio.h> #include <conio.h> #include <stdlib.h> #define MAX 80 char buf[MAX+l]; int spos = 0; int rpos = 0; void qstore(char q); char qretrieve(void); int main(void) { register char ch; int t; buf[80] = ’’; /* Принимать вводимые символы до нажатия <Enter>. */ for(ch=’ ’,t=0; t<32000 && ch!='r'; ++t) { if(_kbhit()) { ch = _getch(); qstore(ch); } printf("%d ", t); if(ch == ’r’) { /* Вывести на экран содержимое буфера клавиатуры и освободить буфер. */ printf("п"); while((ch=qretrieve()) != ’’) printf("%с", ch); printf("n"); } } return 0; } /* Занесение символа в очередь. */ void qstore(char q) { if(spos+l==rpos || (spos+l==MAX && !rpos)) { printf("Список полонп"); return; } buf[spos] = q; spos++; if (spos==MAX) spos = 0; /* установить на начало */ • I /* Получение символа из очереди. */ 466 Часть IV. Алгоритмы и приложения
char qretrieve(void) if(rpos==MAX) rpos = 0; /* установить на начало */ if(rpos==spos) return ’’; rpos++; return buf[rpos-l]; S Стеки Стек (stack) является как бы противоположностью очереди, поскольку он работает по принципу “последним пришел — первым вышел” (last-in, first-out, LIFO)1. Чтобы наглядно представить себе стек, вспомните стопку тарелок. Первая тарелка, стоящая на столе, будет использована последней, а последняя тарелка, положенная наверх — первой. Стеки часто применяются в системном программном обеспечении, включая компиляторы и интерпретаторы. При работе со стеками операции занесения и извлечения элемента являются ос- новными. Данные операции традиционно называются “затолкать в стек*' (push)1 2 и “вытолкнуть из стека"' (pop)3. Поэтому для реализации стека необходимо написать две функции: push (), которая “заталкивает” значение в стек, и pop (), которая “выталкивает” значение из стека. Также необходимо выделить область памяти, кото- рая будет использоваться в качестве стека. Для этой цели можно отвести массив или динамически выделить фрагмент памяти с помощью функций языка С, предусмот- ренных для динамического распределения памяти. Как и в случае очереди, функция извлечения получает из списка элемент и удаляет его, если он не хранится где-либо еще. Ниже приведена общая форма функций push() и рор(), работающих с цело- численным массивом. Стеки данных другого типа можно организовывать, изменив базовый тип данных массива. int stack[МАХ]; int tos=0; /* вершина стека */ /* Затолкать элемент в стек. */ void push(int i) { if(tos >= MAX) { printf("Стек полонп"); return; } stack[tos] = i; tos++; /* Получить верхний элемент стека. */ int pop(void) { 1 Иными словами, в магазинном порядке. — Прим. ред. 2 А также: проталкивать (в стек), помещать на стек, класть в стек, поместить в стек, по- ложить в стек, сохранить в стеке. — Прим. ред. 3 А также: выталкивать данные из стека, выталкивание из стека, выталкивание данных из стека, снимать со стека, вынимать из стека, считывать из стека, вытаскивать из стека. — Прим. ред. Глава 22. Очереди, стеки, связанные списки и деревья 467
tos— ; if(tos < 0) { printf("Стек пустп"); return 0; } return stackftos]; } Переменная tos (“top of stack” — “вершина стека”1) содержит индекс вершины стека. При реализации данных функций необходимо учитывать случаи, когда стек за- полнен или пуст. В нашем случае признаком пустого стека является равенство tos нулю, а признаком переполнения стека — такое увеличение tos, что его значение ука- зывает куда-нибудь за пределы последней ячейки массива. Пример работы стека по- казан в табл. 22.2. Прекрасный пример использования стека — калькулятор с четырьмя действиями. Большинство современных калькуляторов воспринимают стандартную запись выра- жений, называемую инфиксной записью1 2, общая форма которой выглядит как операнд- оператор-операнд. Например, чтобы сложить 100 и 200, необходимо ввести 100, на- жать кнопку “плюс” (“+”), затем ввести 200 и нажать кнопку “равно” (“=”). Напро- тив, во многих ранних калькуляторах (и некоторых из производимых сегодня) приме- няется постфиксная запись3, в которой сначала вводятся оба операнда, а затем опера- тор. Например, чтобы сложить 100 и 200 в постфиксной записи, необходимо ввести 100, затем 200, а потом нажать клавишу “плюс”. В этом методе операнды при вводе заталкиваются в стек. При вводе оператора операнды извлекаются (выталкиваются) из стека, а результат помещается обратно в стек. Одно из преимуществ постфиксной формы заключается в легкости ввода длинных сложных выражений. Следующий пример демонстрирует использование стека в программе, реализую- щей постфиксный калькулятор для целочисленных выражений. Для начала необходи- мо модифицировать функции push () и pop (), как показано ниже. Следует знать, что стек будет размещаться в динамически распределяемой памяти, а не в массиве фик- сированного размера. Хотя применение динамического распределения памяти и не требуется в таком простом примере, мы увидим, как использовать динамическую па- мять для хранения данных стека. Таблица 22.2. Действие стека Действие Содержимое стека push(A) А push(B) В А push(C) С В А рор() извлекает С В А push(F) FBA рор() извлекает F В А рор() извлекает В А рор() извлекает А пусто int *р; /* указатель на область свободной памяти */ int *tos ; /* указатель на вершину стека ★/ int *bos; /* указатель на дно стека */ 1 Называется также верхушкой. — Прим. ред. 2 Другие названия: инфиксное представление, инфиксная нотация. — Прим. ред. 3 Другие названия: постфиксная нотация, польская инверсная запись. — Прим. ред. 468 Часть IV. Алгоритмы и приложения
/★ Занесение элемента в стек. */ void push(int i) { if(p > bos) { printf("Стек полонп"); return; } *p = i; P++; } /* Получение верхнего элемента из стека. */ int pop(void) { p—; , if(p < tos) { printf("Стек пустп"); return 0; } return *p; } Перед использованием этих функций необходимо выделить память из области сво- бодной памяти с помощью функции malloc () и присвоить переменой tos адрес на- чала этой области, а переменной bos — адрес ее конца. Текст программы постфиксного калькулятора целиком приведен ниже. /* Простой калькулятор с четырьмя действиями. */ #include <stdio.h> #include <stdlib.h> #define MAX 100 , kioX int *p; /* указатель на область свободной памя'^Й’*/ int *tos; /* указатель на вершину стека */ int *bos; /* указатель на дно стека */ sxr* void push(int i) ; int pop(void); int main(void) { int a, b; char s[80] ; p = (int *) malloc(MAX*sizeof(int)) ; /* получить память для стека */ if(!р) { printf (‘"Ошибка при выделении памяти !п"); exit(1); } tos = р; bos = р + МАХ-1; printf("Калькулятор с четырьмя действиямип"); printf("Нажмите ’q’ для выходап"); do { printf(": "); Глава 22. Очереди, стеки, связанные списки и деревья 469
gets (s); switches) { case ’+’: a = pop(); b = pop(); printf("%dn", a+b); push(a+b); break; case '-': a = ppp(); b = pop(); printf ("%dn", b-a) ; push(b-a); break; case ’*’: a = pop(); b = pop(); printf("%dn", b*a); push(b*a); break; case ’/’: a = pop(); b = pop(); if(a==0) { printf("Деление на O.n"); break; } printf("%dn", b/a); push(b/a); break; case ’.': /* показать содержимое вершины стека */ а = pop(); push(а); printf("Текущее значение на вершине стека: %dn", а); break; default: push(atoi(s)); } } while(*s != •q'); return 0; /* Занесение элемента в стек. */ void push(int i) { if(p > bos) { printf("Стек полонп"); return; } *P = i; p++; } /* Получение верхнего элемента из стека. */ int pop(void) { p—; 470 Часть IV. Алгоритмы и приложения
if(p < tos) { printf (’’Стек пустп”) ; return 0; } return *p; В Связанные списки Очереди и стеки имеют две характерные особенности: обе структуры данных имеют строгие правила доступа к хранящимся в них данным, причем в результате выполнения операций извлечения данные, в сущности, уничтожаются. Другими словами, доступ к эле- менту в стеке или очереди приводит к его удалению, и если этот элемент не сохранить где- либо в другом месте, он теряется. Кроме того, и в стеке, и в очереди используется один последовательный участок памяти. В отличие от стека или очереди, связанный список до- пускает гибкие способы доступа, поскольку каждый фрагмент информации имеет ссылку на следующий элемент данных в цепочке. Кроме того, операция извлечения не приводит к удалению из списка и уничтожению элемента данных. В принципе, для этой цели необхо- димо ввести дополнительную специальную операцию удаления. Связанные списки могут быть односвязными и двусвязными1. Односвязный спи- сок содержит ссылку на следующий элемент данных. Двусвязный список содержит ссылки как на последующий, так и на предыдущий элементы списка. Выбор типа применяемого списка зависит от конкретной задачи. ES Односвязные списки В односвязном списке каждый элемент информации содержит ссылку на следую- щий элемент списка. Каждый элемент данных обычно представляет собой структуру, которая состоит из информационных полей и указателя связи. Концептуально одно- связный список выглядит так, как показано на рис. 22.2. Существует два основных способа построения односвязного списка. Первый спо- соб — помещать новые элементы в конец списка1 2. Второй — вставлять элементы в определенные позиции списка, например, в порядке возрастания. От способа по- строения списка зависит алгоритм функции добавления элемента. Давайте начнем с более простого способа создания списка путем помещения элементов в конец. Как правило, элементы связанного списка являются структурами, так как, помимо данных, они содержат ссылку на следующий элемент. Поэтому необходимо опреде- лить структуру, которая будет использоваться в последующих примерах. Поскольку списки рассылки обычно хранятся в связанных списках, хорошим выбором будет структура, описывающая почтовый адрес. Ее описание показано ниже: I struct address { char name[40]; char street[40]; char city[20]; 1 Связанные списки часто называются связными. Односвязные списки называются еще одно- связными линейными списками, однонаправленными списками, а также однонаправленными цепочка- ми. Двусвязные списки иногда называются также дважды связанными’, кроме того, их называют двусвязными линейными списками, а также двунаправленными цепочками. — Прим. ред. 2 Не забывайте, что у односвязного списка, как и у веревки, два конца: начало и конец. — Прим. ред. Глава 22. Очереди, стеки, связанные списки и деревья 471
char state[3]; char zip[11]; struct address *next; /* ссылка на следующий адрес */ } info; Рис. 22.2. Односвязный список Приведенная ниже функция slstore() создает односвязный список путем поме- щения каждого очередного элемента в конец списка. В качестве параметров ей пере- даются указатель на структуру типа address, содержащую новую запись, и указатель на последний элемент списка. Если список пуст, указатель на последний элемент должен быть равен нулю. void slstore(struct address *i, struct address **last) { if(!*last) *last = i; /* первый элемент в списке */ else (*last)->next = i; i->next = NULL; *last = i; > '! Несмотря на то, что 'Розданный с помощью функции slstore () список можно от- сортировать отдельной операцией уже после его создания, легче сразу создавать упо- рядоченный список, вставляя каждый новый элемент в нужное место в последова- тельности. Кроме того, если список уже отсортирован, имеет смысл поддерживать его упорядоченность, вставляя новые элементы в соответствующие позиции. Для вставки элемента таким способом требуется последовательно просматривать список до тех пор, пока не будет найдено место нового элемента, затем вставить в найденную пози- цию новую запись и переустановить ссылки. При вставке элемента в односвязный список может возникнуть одна из трех ситуаций: элемент становится первым, элемент вставляется между двумя другими, элемент становится последним. На рис. 22.3 показана схема изменения ссылок в каждом случае. Следует помнить, что при вставке элемента в начало (первую позицию) списка необходимо также изменить адрес входа в список где-то в другом месте програм- мы. Чтобы избежать этих сложностей, можно в качестве первого элемента списка хранить служебный сторожевой элемент1. В случае упорядоченного списка необ- ходимо выбрать некоторое специальное значение, которое всегда будет первым в списке, чтобы начальный элемент оставался неизменным. Недостатком данного метода является довольно большой расход памяти на хранение сторожевого эле- мента, но обычно это не столь важно. 1 Часто называется еще сигнальной меткой. — Прим. ред. 472 Часть IV. Алгоритмы и приложения
Вставка в начало списка Вставка в середину списка Вставка в конец списка Рис, 22.3. Вставка нового элемента new в односвязный список (в котором info — поле данных) Следующая функция, sls_store(), вставляет структуры типа address в список рассылки, упорядочивая его по возрастанию значений в поле name. Функция прини- мает указатели на указатели на первый и последний элементы списка, а также указа- тель на вставляемый элемент. Поскольку первый или последний элементы списка могут измениться, функция sls_store() при необходимости автоматически обнов- ляет указатели на начало и конец списка. При первом вызове данной функции указа- тели first и last должны быть равны нулю. /* Вставка в упорядоченный односвязный список. */ void sls_store(struct address *i, /* новый элемент */ struct address **start, /* начало списка */ struct address **last) /* конец списка */ { struct address *old, *p; p = *start; if(’*last) { /* первый элемент в списке */ i->next = NULL; Глава 22. Очереди, стеки, связанные списки и деревья 473
*last = i; *start = i; return; } old = NULL; while(p) { if(strcmp(p->name, i->name)<0) { old = p; p = p->next; } else { if(old) { /* вставка в середину */ old->next = i; i->next = p; return; } i->next = p; /* вставка в начало */ *start = i; return; } } (*last)->next = i; /* вставка в конец */ i->next = NULL; *last = i; } Последовательный перебор элементов связанного списка осуществляется очень просто: начать с начала и следовать указателям. Обычно фрагмент кода перебора на- столько мал, что его вставляют в другую процедуру — например, функцию поиска, удаления или отображения содержимого. Так, приведенная ниже функция выводит на экран все имена из списка рассылки: void display(struct address *start) { while(start) { printf("%sn", start->name); start = start->next; } } При вызове функции display () параметр start должен быть указателем на первую структуру в списке. После этого функция переходит к следующему элементу, на который указывает указатель в поле next. Процесс прекращается, когда next равно нулю. Для получения элемента из списка нужно просто пройти по цепочке ссылок. Ниже приведен пример функции поиска по содержимому поля name: struct address *search(struct address *start, char *n) { while(start) { if(!strcmp(n, start->name)) return start; start = start->next; } return NULL; /* подходящий элемент не найден */ } Поскольку функция search () возвращает указатель на элемент списка, содержа- щий искомое имя, возвращаемый тип описан как указатель на структуру address. При отсутствии в списке подходящих элементов возвращается нуль (null). 474 Часть IV. Алгоритмы и приложения
Удаление элемента из односвязного списка выполняется просто. Так же, как и при вставке, возможны три случая: удаление первого элемента, удаление элемента в сере- дине, удаление последнего элемента. На рис. 22.4 показаны все три операции. Ниже приведена функция, удаляющая заданный элемент из списка структур address. void sldelete( struct address *p, /* предыдущий элемент */ struct address *i, /* удаляемый элемент */ struct address **start, /* начало списка */ struct address **last) /* конец списка */ { if(p) p->next = i->next; else *start = i->next; if(i==*last && p) *last = p; } Удаление первого элемента списка превращается Удаление среднего элемента списка превращается Рис. 22.4. Удаление элемента из односвязного списка Функции si delete () необходимо передавать указатели на удаляемый элемент, предшествующий удаляемому, а также на первый и последний элементы. При удале- нии первого элемента указатель на предшествующий элемент должен быть равен ну- лю (NULL). Данная функция автоматически обновляет указатели start и last, если один из них ссылается на удаляемый элемент. У односвязных списков есть один большой недостаток: односвязный список не- возможно прочитать в обратном направлении. По этой причине обычно применяются двусвязные списки. Глава 22. Очереди, стеки, связанные списки и деревья 475
[I Двусвязные списки Двусвязный список состоит из элементов данных, каждый из которых содержит ссылки как на следующий, так и на предыдущий элементы. На рис. 22.5 показана организация ссылок в двусвязном списке. Наличие двух ссылок вместо одной предоставляет несколько преимуществ. Веро- ятно, наиболее важное из них состоит в том, что перемещение по списку возможно в обоих направлениях. Это упрощает работу со списком, в частности, вставку и удале- ние. Помимо этого, пользователь может просматривать список в любом направлении. Еще одно преимущество имеет значение только при некоторых сбоях. Поскольку весь список можно пройти не только по прямым, но и по обратным ссылкам, то в случае, если какая-то из ссылок станет неверной, целостность списка можно восстановить по другой ссылке. Рис. 22.5. Двусвязный список При вставке нового элемента в двусвязный список могут быть три случая: элемент вставляется в начало, в середину и в конец списка. Эти операции показаны на рис. 22.6. Построение двусвязного списка выполняется аналогично построению односвяз- ного за исключением того, что необходимо установить две ссылки. Поэтому в струк- туре данных должны быть описаны два указателя связи. Возвращаясь к примеру спи- ска рассылки, для двусвязного списка структуру address можно модифицировать следующим образом: struct address { ' char name[40]; char street[40]; char city[20]; char state [3]; ‘s--' char zip[11]; struct address *next; struct address *prior; } info; Следующая функция, distore (), создает двусвязный список, используя структуру address в качестве базового типа данных: void distore(struct address *i, struct address **last) { if(!*last) *last = i; /* вставка первого элемента */ else (*last)->next = i; i->next = NULL; i->prior = *last; *last = i; } Функция dlstore () помещает новые записи в конец списка. В качестве парамет- ров ей необходимо передавать указатель на сохраняемые данные, а также указатель на конец списка, который при первом вызове должен быть равен нулю (NULL). 476 Часть IV. Алгоритмы и приложения
Вставка элемента в начало списка Вставка элемента в середину списка Подобно односвязным, двусвязные списки можно создавать с помощью функции, которая будет помещать элементы в определенные позиции, а не только в конец спи- ска. Показанная ниже функция dls_store() создает список, упорядочивая его по возрастанию имен: /* Создание упорядоченного двусвязного списка. */ void dls_store( struct address *i, /* новый элемент */ struct address **start, /* первый элемент в списке ★/ struct address **last /* последний элемент в списке */ ) { struct address *old, *p; if(*last==NULL) { /* первый элемент в списке */ i->next = NULL; i->prior = NULL; *last = i; *start = i; Глава 22. Очереди, стеки, связанные списки и деревья 477
return; } p = *start; /* начать с начала списка */ old.= NULL; while(p) { if(strcmp(p->name, i->name)<0){ old = p; p = p->next; } else { if(p->prior) { p->prior->next = i; i->next = p; i->prior = p->prior; p->prior = i; return; } i->next = p; /* новый первый элемент */ i->prior = NULL; p->prior = i; *start = i; return; } } old->next = i; /* вставка в конец */ i->next = NULL; i->prior = old; *last = i; } Поскольку первый и последний элементы списка могут меняться, функция dls store () автоматически обновляет указатели на начало и конец списка посредст- вом параметров start и last. При вызове функции необходимо передавать указатель на сохраняемые данные и указатели на указатели на первый и последний элементы списка. В первый раз параметры start и last должны быть равны нулю (NULL). Как и в односвязных списках, для получения элемента данных двусвязного списка не- обходимо переходить по ссылкам до тех пор, пока не будет найден искомый элемент. При удалении элемента двусвязного списка могут возникнуть три случая: удаление первого элемента, удаление элемента из середины и удаление последнего элемента. На рис. 22.7 показано, как при этом изменяются ссылки. Показанная ниже функция dldelete () удаляет элемент двусвязного списка: void dldelete( struct address *i, /* удаляемый элемент */ struct address **start, /* первый элемент */ struct address **last) /* последний элемент */ { if(i->prior) i->prior->next = i->next; else { /* удаление первого элемента */ *start = i->next; if(start) start->prior = NULL; } if(i->next) i->next->prior = i->prior; else /* удаление последнего элемента */ *last = i->prior; } 478 Часть IV. Алгоритмы и приложения
Поскольку первый или последний элементы списка могут быть удалены, функция dldeleteO автоматически обновляет указатели на начало и конец списка посредст- вом параметров start и last. При вызове ей передаются указатель на удаляемый элемент и указатели на указатели на начало и конец списка. Удаление первого элемента списка Удаление последнего элемента списка превращается Рис. 22.7. Удаление элемента двусвязного списка удален О О В Пример списка рассылки Чтобы завершить обсуждение двусвязных списков, в данном разделе представлена простая, но законченная программа для работы со списком рассылки. Во время рабо- ты весь список хранится в оперативной памяти. Тем не менее, его можно сохранять в файле и загружать для дальнейшей работы. /* Простая программа для обработки списка рассылки, иллюстрирующая работу с двусвязными списками. */ ttinclude <stdio.h> tfinclude <stdlib.h> ttinclude <string.h> struct address { char name[30]; char street[40]; char city[20]; char state[3]; char zip[11]; struct address *next; /* указатель на следующую запись */ struct address *prior; /* указатель на предыдущую запись */ }; struct address *start; /* указатель на первую запись списка */ Глава 22. Очереди, стеки, связанные списки и деревья 479
struct address *last; /* указатель на последнюю запись */ struct address *find(char *); void enter(void), search(void), save(void); void load(void), list(void); void mldelete(struct address **, struct address **); void dls_store(struct address *i, struct address **start, struct address **last); void inputs(char *, char *, int), display(struct address *); int menu_select(void); int main(void) { start = last = NULL; /* инициализация указателей на начало и конец */ for(;;) { switch(menu_select()) { case 1: enter(); /* ввод адреса */ break; case 2: mldelete(&start, &last); /* удаление адреса */ break; case 3: list(); /* отображение списка */ break; case 4: search(); /* поиск адреса */ break; case 5: save(); /* запись списка в файл */ break; case 6: load(); /* считывание с диска */ break; case 7: exit (0); } } return 0; } /* Выбор действия пользователя. */ int menu_select(void) { char s[80];• int c ; printf("1. Ввод именип"); printf("2. Удаление именип"); printf("3. Отображение содержимого спискап"); printf(”4. Поискп"); printf(”5. Сохранить в файлп"); printf("6. Загрузить из файлап"); printf("7. Выходп"); do { printf("пВаш выбор: "); gets (s); с = atoi (s) ; } while(c<0 || c>7); return c; /* Ввод имен и адресов. */ void enter(void) { 480 Часть IV. Алгоритмы и приложения
struct address *info; for(;;) { info « (struct address *)malloc(sizeof(struct address)); if(!info) { printf("пНет свободной памяти"); return; } inputs("Введите имя: ", info->name, 30); if(!info->name[0]) break; /* завершить ввод */ inputs("Введите улицу: ", info->street, 40); inputs("Введите город: ", info->city, 20); inputs("Введите штат: ", info->state, 3); inputs("Введите почтовый индекс: ", info->zip, 10); dls_store(info, &start, &last); } /* цикл ввода */ } /* Следующая функция вводит с клавиатуры строку длиной не больше count и предотвращает переполнение строки. Кроме того, она выводит на экран подсказку. */ void inputs(char *prompt, char *s, int count) { char p[255]; do { printf(prompt); fgets(p, 254, stdin); if(strlen(p) > count) printf("ХпСлишком длинная строкап"); } while(strlen(p) > count); p[strlen(p)-1] = 0; /* удалить символ перевода строки */ strcpy(s, p); /* Создание упорядоченного двусвязного списка. */ void dls_store( struct address *i, /* новый элемент */ struct address **start, /* первый элемент списка */ struct address **last /* последний элемент списка */ ) { struct address *old, *p; if(*last==NULL) { /* первый элемент списка */ i->next = NULL; i->prior = NULL; *last = i; *start = i; return; } p = *start; /* начать с начала списка */ old = NULL; while(p) { if(strcmp(p->name, i->name)<0){ old = p; Глава 22. Очереди, стеки, связанные списки и деревья 481
р = p->next; } else { if(p->prior) { p->prior->next = i; i->next = p; i->prior = p->prior; p->prior = i; return; } i->next = p; /* новый первый элемент */ i->prior = NULL; p->prior = i; *start = i; return; } } old->next = i; /* вставка в конец */ i->next = NULL; i->prior = old; *last = i; /* Удаление элемента из списка. */ void mldelete(struct address **start, struct address **last) { struct address *info; char s[80] ; inputs("Введите имя: "f sf 30); info = find(s); if(info) { if(*start==info) { *start=info->next; if(*start) (*start)->prior = NULL; else *last = NULL; } else { info->prior->next = info->next; if(info!=*last) info->next->prior = info->prior; else *last = info->prior; } free(info); /* освободить память */ } /* Поиск адреса. */ struct address *find( char *name) { struct address *info; info = start; while(info) { if(!strcmp(name, info->name)) return info; info = info->next; /* перейти к следующему адресу */ } printf("Имя не найдено.п"); 482 Часть IV. Алгоритмы и приложения
return NULL; /* нет подходящего элемента */ /* Отобразить на экране весь список. */ void list(void) { struct address *info; info = start; while(info) { display(info); info = info->next; /* перейти к следующему адресу */ } printf(”nn”); /* Данная функция выполняет собственно вывод на экран всех полей записи, содержащей адрес. */ void display(struct address *info) { printf(”%sn”, info->name); printf(”%sn”, info->street) ; printf(”%sn”, info->city) ; printf(”%sn”, info->state); printf(”%sn”, info->zip); printf(”nn”); /* Поиск имени в списке. */ void search(void) { char name[40]; struct address *info; printf (’’Введите имя: ”); gets(name); info = find(name); if(!info) printf(”He найденоп”); else display(info); } /* Сохранить список в дисковом файле. */ void save(void) { struct address *info; FILE *fp; fp = fopen (’’mlist”, ”wb”) ; if(lfp) { printf (’’Невозможно открыть файл.п’’); exit(1); } printf(”пСохранение в файлеп"); info = start; while(info) { fwrite(info, sizeof(struct address), 1, fp); info = info->next; /* перейти к следующему адресу */ Глава 22. Очереди, стеки, связанные списки и деревья 483
} fclose(fp); /* Загрузка адресов из файла. */ void load() { struct address *info; FILE *fp; fp = fopen("mlist", "rb”); if('fp) { printf("Невозможно открыть файл.п"); exit(1); } /* освободить память, если в памяти уже есть список */ while(start) { info = start->next; free(info); start = info; } /* сбросить указатели на начало и конец */ start = last = NULL; printf("ХпЗагрузка из файлап"); while(!feof(fp)) { info = (struct address *) malloc(sizeof(struct address)); if(!info) { printf("Нет свободной памяти"); return; } if (1 != fread(info, sizeof(struct address), 1, fp) ) break; dls_store(info, &start, &last); } fclose(fp); Bl Двоичные деревья Напоследок мы рассмотрим структуру данных, которая называется двоичное дерево (binary tree). Несмотря на то, что бывает много различных типов деревьев, двоичные деревья играют особую роль, так как в отсортированном состоянии позволяют очень быстро выполнять вставку, удаление и поиск. Каждый элемент двоичного дерева со- стоит из информационной части и указателей на левый и правый элементы. На рис. 22.8 показано небольшое двоичное дерево. При обсуждении деревьев применяется специальная терминология. Программисты не являются специалистами в области филологии, и поэтому терминология, приме- няемая в теории графов (а ведь деревья представляют собой частный случай графов!), является классическим примером неправильного употребления слов. Первый элемент дерева называется корнем (root). Каждый элемент данных называется вершиной дерева (node), а любой фрагмент дерева называется поддеревом (subtree). Вершина, к которой не присоединены поддеревья, называется заключительным узлом (terminal node) или листом (leaf). Высота (height) дерева равняется максимальному количеству уровней от 484 Часть IV. Алгоритмы и приложения
корня до листа. При работе с деревьями можно допустить, что в памяти они сущест- вуют в том же виде, что и на бумаге. Но помните, что дерево — всего лишь способ логической организации данных в памяти, а память линейна. ень Рис. 22.8. Пример двоичного дерева, высота которого равна 3 В некотором смысле двоичное дерево является особым видом связанного списка. Элементы можно вставлять, удалять и извлекать в любом порядке. Кроме того, опера- ция извлечения не является разрушающей. Несмотря на то, что деревья легко пред- ставить в воображении, в теории программирования с ними связан ряд сложных за- дач. В данном разделе деревья затрагиваются лишь поверхностно. Большинство функций, работающих с деревьями, рекурсивны, поскольку дерево по своей сути является рекурсивной структурой данных. Другими словами, каждое поддерево, в свою очередь, является деревом. Поэтому разрабатываемые здесь функ- ции будут рекурсивными. Существуют и не рекурсивные версии этих функций, но их код понять намного сложнее. Способ упорядочивания дерева зависит от того, как к нему впоследствии будет осуществляться доступ. Процесс поочередного доступа к каждой вершине дерева на- зывается обходом (вершин) дерева (tree traversal). Рассмотрим следующее дерево: Глава 22. Очереди, стеки, связанные списки и деревья 485
Существует три порядка обхода дерева: обход симметричным способом, или симмет- ричный обход (inorder), обход в прямом порядке, прямой обход, упорядоченный обход, обход сверху, или обход в ширину (preorder) и обход в обратном порядке, обход в глубину, об- ратный обход, обход снизу (postorder). При симметричном обходе обрабатывается сна- чала левое поддерево, затем корень, а затем правое поддерево. При прямом обходе обрабатывается сначала корень, затем левое поддерево, а потом правое. При обходе снизу сначала обрабатывается левое поддерево, затем правое и, наконец корень. По- следовательность доступа при каждом методе обхода показана ниже: Симметричный обход a b с d е f g Прямой обход d b а с f е g Обход снизу а с b е g f d Несмотря на то, что дерево не должно быть обязательно упорядоченным, в боль- шинстве задач используются именно такие деревья. Конечно, структура упорядочен- ного дерева зависит от способа его обхода. В оставшейся части данной главы предпо- лагается симметричный обход. Поэтому упорядоченным двоичным деревом будет счи- таться такое дерево, в котором левое поддерево содержит вершины, меньшие или равные корню, а правое содержит вершины, большие корня. Приведенная ниже функция stree () создает упорядоченное двоичное дерево: struct tree { char info; struct tree *left; struct tree *right; }; struct tree *stree( struct tree *root, struct tree *x, char info) { if(’r) { r = (struct tree *) malloc(sizeof(struct tree)); if(’r) { printf(”He хватает памятип"); exit(0) ; } r->left = NULL; r->right = NULL; r->info = info; if(’root) return r; /* первый вход */ if(info < root->info) root->left = r; else root->right = r; return r; } if(info < r->info) stree(r,r->left,info); else stree(r,r->right,info); return root; } Приведенный выше алгоритм просто следует по ссылкам дерева, переходя к левой или правой ветви очередной вершины на основании содержимого поля info до дос- тижения места вставки нового элемента. Чтобы использовать эту функцию, необхо- димо иметь глобальную переменную-указатель на корень дерева. Этот указатель изна- 486 Часть IV. Алгоритмы и приложения
чально должен иметь значение нуль (NULL). При первом вызове функция stree() возвращает указатель на корень дерева, который нужно присвоить глобальной пере- менной. При последующих вызовах функция продолжает возвращать указатель на ко- рень. Допустим, что глобальная переменная, содержащая корень дерева, называется rt. Тогда функция stree () вызывается следующим образом: I/* вызов функции stree() */ rt = stree(rt, rt, info); Функция stree () использует рекурсивный алгоритм, как и большинство процедур работы с деревьями. Точно такая же функция, основанная на итеративных методах, была бы в несколько раз длиннее. Функцию stree () необходимо вызывать со сле- дующими параметрами (слева направо): указатель на корень всего дерева, указатель на корень следующего поддерева, в котором осуществляется поиск, и сохраняемые дан- ные. При первом вызове оба первых параметрах указывают на корень всего дерева. Для простоты в вершинах дерева хранятся одиночные символы. Тем не менее, вместо них можно использовать любой тип данных. Чтобы обойти созданное функцией stree () дерево в симметричном порядке и распечатать поле info в каждой вершине, можно применить приведенную ниже функцию inorder(): void inorder(struct tree *root) { if(’root) return; inorder(root->left) ; if(root->info) printf(”%c ", root->info); inorder(root->right); } Данная рекурсивная функция завершает работу тогда, когда находит заключитель- ный узел (нулевой указатель). В следующем листинге показаны функции, выполняющие обход дерева в ширину и в глубину. void preorder(struct tree *root) { if(’root) return; if(root->info) printf("%c ", root->info); preorder(root->left); preorder(root->right); } void postorder(struct tree *root) { if(I root) return; postorder(root->left) ; postorder(root->right) ; if(root->info) printf("%c ", root->info); } Теперь давайте рассмотрим короткую, но интересную программу, которая строит упорядоченное двоичное дерево, а затем, обходя его симметричным образом, отобра- жает его на экране боком. Для отображения дерева требуется лишь слегка модифици- ровать функцию inorder (). Поскольку на экране дерево распечатывается боком, для корректного отображения правое поддерево необходимо печатать прежде левого. Глава 22. Очереди, стеки, связанные списки и деревья 487
(Технически это противоположность симметричного обхода.) Новая функция называ- ется print_tree (), а ее код показан ниже: void print—tree(struct tree *r, int 1) { int i; if(r == NULL) return; print—tree(r->right, 1+1); for(i=0; icl; ++i) printf(" "); printf("%cn", r->info); print—tree(r->left, 1+1); } Далее следует текст всей программы печати дерева. Попробуйте вводить различные деревья, чтобы увидеть, как они строятся. /* Эта программа выводит на экран двоичное дерево. */ #include <stdlib.h> #include <stdio.h> struct tree { char info; struct tree deft; struct tree *right; }; struct tree *root; /* начальная вершина дерева */ struct tree *stree(struct tree *root, struct tree *r, char info); void print—tree(struct tree *root, int 1); int main(void) { char s [80]; root = NULL; /* инициализация корня дерева */ do { printf("Введите букву: "); gets (s); root = stree(root, root, *s); } while(*s); print—tree(root, 0); return 0; } struct tree *stree ( struct tree *root, struct tree *r, char info) if(!r) { r = (struct tree *) malloc(sizeof(struct tree)); if (!r)’ { 488 Часть IV. Алгоритмы и приложения
printf("He хватает памятип"); exit(0) ; } r->left = NULL; r->right = NULL; r->info = info; if(!root) return r; /* первый вход */ if(info < root->info) root->left = r; else root->right = r; return r; } if(info < r->info) stree(r, r->left, info); else stree(r, r->right, info); return root; } void print_tree(struct tree *r, int 1) { int i ; if(!r) return; print—tree(r->right, 1+1); for(i=0; i<l; + + i) printf(" "); printf("%cn", r->info); print—tree(r->left, 1+1); } По существу, данная программа сортирует вводимую информацию. Метод сорти- ровки является одной из разновидностей сортировки методом вставок, которая была рассмотрена в предыдущей главе. В среднем случае производительность может быть вполне хорошей. Если вы запускали программу печати дерева, вы, вероятно, заметили, что некото- рые деревья являются сбалансированными (balanced), т.е. каждое поддерево имеет при- мерно такую же высоту, как и остальные, а некоторые деревья очень далеки от этого состояния. Например, дерево a^b^c^d выглядит следующим образом: В этом дереве нет левых поддеревьев. Такое дерево называется вырожденным, по- скольку фактически оно выродилось в линейный список. В общем случае, если при построении дерева вводимые данные являются случайными, то получаемое дерево оказывается близким к сбалансированному. Если же информация предварительно от- сортирована, создается вырожденное дерево. (Поэтому иногда при каждой вставке де- Глава 22. Очереди, стеки, связанные списки и деревья 489
рево корректируют так, чтобы оно было сбалансированным, но этот процесс довольно сложен и выходит за рамки данной главы.) В двоичных деревьях легко реализовываются функции поиска. Приведенная ниже функция возвращает указатель на вершину дерева, в которой информация совпадает с ключом поиска, либо нуль (null), если такой вершины нет. struct tree *search_tree(struct tree *root, char key) { if(!root) return root; /* пустое дерево */ while(root->info != key) { if(key<root->info) root = root->left; else root = root->right; if(root == NULL) break; } return root; } К сожалению, удалить вершину дерева не так просто, как отыскать. Удаляемая вершина может быть либо корнем, либо левой, либо правой вершиной. Помимо того, к вершине могут быть присоединены поддеревья (количество присоединенных подде- ревьев может равняться 0, 1 или 2). Процесс переустановки указателей подсказывает рекурсивный алгоритм, приведенный ниже: struct tree *dtree(struct tree *root, char key) { struct tree *p,*p2; if(!root) return root; /* вершина не найдена */ if(root->info == key) { /* удаление корня */ /* это означает пустое дерево */ if(root->left == root->right){ free(root); return NULL; } /* или если одно из поддеревьев пустое */ else if(root->left == NULL) { p = root->right; free(root); return p; } else if(root->right == NULL) { p = root->left; free(root); return p; } /* или есть оба поддерева */ else { р2 = root->right; р = root->right; while(p->left) p = p->left; p->left = root->left; free(root); return p2; } } if(root->info < key) root->right = dtree(root->right, key); else root->left = dtree(root->left, key); return root; } 490 Часть IV. Алгоритмы и приложения
Необходимо также следить за правильным обновлением указателя на корень дере- ва, описанного вне данной функции, поскольку удаляемая вершина может быть кор- нем. Лучше всего с этой целью указателю на корень присваивать значение, возвра- щаемое функцией dtree (): | root = dtree(root, key); Двоичные деревья — исключительно мощное, гибкое и эффективное средство. По- скольку при поиске в сбалансированном дереве выполняется в худшем случае log2w сравнений, оно намного лучше, чем связанный список, в котором возможен лишь по- следовательный поиск. Глава 22. Очереди, стеки, связанные списки и деревья 491
Глава 23 Разреженные массивы
Одна из наиболее интересных задач программирования — реализация разреженных массивов. Разреженный массив, или разреженная матрица (sparse array), — это мас- сив, в котором не все элементы используются, имеются в наличии или нужны в данный момент. Разреженные массивы полезны в тех случаях, когда выполняются два условия: размер массива, который требуется приложению, достаточно большой (возможно, пре- вышает объем доступной памяти), и когда не все элементы массива используются. Та- ким образом, разреженный массив — это, как правило, большой, но редко заполненный массив. Как будет показано далее, есть несколько способов реализации разреженных массивов. Но перед тем как приступить к их рассмотрению, давайте уделим внимание задачам, которые решаются с помощью разреженных массивов. И Зачем нужны разреженные массивы? Чтобы понять, зачем нужны разреженные массивы, вспомните следующие два факта: При описании обычного массива в языке С вся память, требуемая для разме- щения массива, выделяется сразу после его создания. Большие массивы, особенно многомерные, могут занимать огромные объемы памяти. Тот факт, что память для массива выделяется при его создании, означает, что раз- мер самого большого массива, который вы сможете описать в своей программе, огра- ничен (в частности) объемом доступной памяти. Если вам понадобится массив боль- шего размера, чем позволяют возможности компьютера, придется реализовывать его каким-то другим образом. (Например, для работы с полностью заполненными боль- шими массивами обычно применяется та или иная форма виртуальной памяти.) Даже если большой массив разместится в памяти, создание его может существенно умень- шить доступные ресурсы системы, поскольку память, занятая большим массивом, оказывается недоступной для остальной части программы и других процессов, рабо- тающих в системе. А это в свою очередь может отрицательно сказаться на общей про- изводительности программы или компьютера в целом. В тех ситуациях, когда будут использоваться не все элементы массива, выделение памяти под весь массив пред- ставляется особенно расточительной тратой системных ресурсов. Чтобы избавиться от проблем, вызванных потребностью в памяти для больших редко заполненных массивов, были придуманы некоторые приемы работы с разре- женными массивами. Все они характеризуются одной общей чертой: память для эле- ментов массива выделяется только при необходимости. Поэтому преимущество раз- реженного массива состоит в том, что для его хранения требуется ровно столько па- мяти, сколько нужно для хранения только тех элементов, которые действительно используются. При этом остальная память может использоваться для других целей. Кроме того, эти приемы позволяют создавать очень большие массивы, размер кото- рых значительно больше, чем допускаемый системой размер обычных массивов. Существуют многочисленные примеры приложений, требующих обработки разре- женных массивов. Многие из них относятся к работе с матрицами или к научным и инженерным задачам, которые понятны лишь экспертам в соответствующих областях. Однако есть одно очень популярное приложение, в котором обычно применяется раз- реженный массив — электронная таблица. Несмотря на то, что матрица в средней электронной таблице очень большая, в любой момент времени используется лишь не- которая ее часть. В ячейках электронных таблиц хранятся формулы, значения и стро- ки. При использовании разреженного массива память для каждого элемента выделя- ется только при необходимости. Поскольку фактически используется лишь небольшая 494 Часть IV. Алгоритмы и приложения
часть элементов массива, весь он (то есть электронная таблица) кажется очень боль- шим, но занимает минимально необходимую память. В этой главе будут часто повторяться два термина: логический массив и физический массив. Логический массив — это массив, который мыслится как существующий в системе. Например, если матрица электронной таблицы имеет размер 1 000x1 000, то логический массив, реализующий матрицу, также будет иметь размер 1 000x1 000 — даже несмотря на то, что физически такой массив не существует в компьютере. Физи- ческий массив — это массив, который реально существует в памяти компьютера. Так, если используются только 100 элементов матрицы электронной таблицы, то для хра- нения физического массива требуется память, необходимая для хранения лишь этих 100 элементов. Методы обработки разреженных массивов, раскрытые в данной главе, обеспечивают связь между логическими и физическими массивами. Ниже будут рассмотрены четыре различных способа создания разреженного масси- ва: связанный список, двоичное дерево, массив указателей и хеширование. Несмотря на то, что программа работы с электронными таблицами не разрабатывается целиком, все примеры ориентируются на матрицу электронной таблицы, показанную на рис. 23.1. На этом рисунке элемент X расположен в ячейке В2. В Представление разреженного массива в виде связанного списка При реализации разреженного массива с помощью связанного списка первым де- лом необходимо создать структуру, содержащую следующие элементы: Хранимые в ячейке данные Логическая позиция ячейки в массиве Ссылки на предыдущий и следующий элементы Рис. 23.1. Организация простой электронной таблицы Каждая новая структура помещается в список так, что элементы остаются упорядочен- ными по индексу в массиве. Доступ к массиву производится путем перехода по ссылкам. Например, в качестве носителя элемента разреженного массива в электронной таблице можно использовать следующую структуру: struct cell { char cell_name[9]; /* имя ячейки, напр. Al, В34 */ Глава 23. Разреженные массивы 495
I char formula[128]; /* информация, напр. 10/B2 */ struct cell *next; /* указатель на следующую запись */ struct cell *prior; /* указатель на предыдущую запись */ } ; Поле cell_name содержит строку, соответствующую имени ячейки, например, А1, В34 или Z19. Строковое поле formula хранит формулу (данные) из соответствующей ячейки таблицы. Полная программа обработки электронных таблиц была бы слишком большой1, чтобы использовать ее в качестве примера. Вместо этого в данной главе рассмотрены ключевые функции, обеспечивающие реализацию разреженного массива на основе связанного списка. Следует помнить, что существует очень много способов реализа- ции программы обработки электронных таблиц. Показанные здесь функции и струк- тура данных — лишь примеры приемов работы с разреженными массивами. Следующие глобальные переменные указывают на начало и конец связанного списка: I struct cell *start = NULL; /* первый элемент списка */ struct cell *last = NULL; /* последний элемент списка */ В большинстве электронных таблиц при вводе формулы в ячейку создается новый элемент разреженного массива. Если электронная таблица построена на основе свя- занного списка, этот элемент вставляется в список с помощью функции, аналогичной функции dls store (), приведенной в главе 22. Помните, что список упорядочен по именам ячеек; например, А12 предшествует А13 и т.д. /* Вставка ячеек в упорядоченный список. */ void dls_store(struct cell *i, /* указатель на вставляемую ячейку */ struct cell **start, struct cell **last) { struct cell *old, *p; if(!*last) { /* первый элемент в списке */ i->next = NULL; i->prior = NULL; *last = i; *start = i; return; } p = *start; /* начать с головы списка */ old = NULL; while(p) { if(strcmp(p->cell_name, i->cell_name) < 0){ old = p; p = p->next; } else { if(p->prior) { /* это элемент из середины */ p->prior->next = i; i->next = p; i->prior = p->prior; p->prior = i; return; 1 Многие пользователи шутят, что сама Microsoft не знает, сколько же точно занимает ее Excel. Конечно, это только шутка, но лично я иногда думаю, что в ней 100 % правды. — Прим. ред. 496 Часть IV. Алгоритмы и приложения
} i->next = p; /* новый первый элемент */ i->prior = NULL; p->prior = i; *start = i; return; } } old->next = i; /* вставка в конец */ i->next = NULL; i->prior = old; *last = i; return; } В приведенной выше функции параметр i — указатель на новую вставляемую ячейку. Параметры start и last являются соответственно указателями на указатели на начало и конец списка. Нижеследующая функция deletecell () удаляет из списка ячейку, имя которой передается в качестве параметра. void deletecell(char *cell_name, struct cell **start, struct cell **last) { struct cell *info; info = find(cell_name, *start); if(info) { if(*start==info) { *start = info->next; if(*start) (*start)->prior = NULL; else *last = NULL; } else { if(info->prior) info->prior->next = info->next; if(info != *last) info->next->prior = info->prior; else *last = info->prior; } free(info); /* освободить системную память */ } } Последняя функция, которая понадобится для реализации разреженного массива на основе связанного списка — это функция find(), находящая указанную ячейку. Для на- хождения ячейки данной функции приходится выполнять линейный поиск, но, как было показано в главе 21, среднее количество сравнений при линейном поиске равно л/2, где п — количество элементов в списке. Ниже приведен текст функции find (): struct cell *find(char *cell_name, struct cell *start) { struct cell *info; info = start; while(info) { if(!strcmp(cell_name, info->cell_name)) return info; info = info->next; /* перейти к следующей ячейке */ Глава 23. Разреженные массивы 497
} printf (’’Ячейка не найдена. n”) ; return NULL; /* поиск неудачный */ Анализ метода представления в виде связанного списка Принципиальное преимущество метода реализации разреженного массива с по- мощью связанного списка заключается в том, что он позволяет эффективно использо- вать память — место выделяется только для тех элементов массива, которые действи- тельно содержат информацию. Кроме того, он прост в реализации. Тем не менее, у этого метода есть один большой недостаток: для доступа к ячейкам в нем применяет- ся линейный поиск. Причем процедура сохранения ячейки также использует линей- ный поиск, чтобы найти место вставки нового элемента. Эти проблемы можно разре- шить, построив разреженный массив на основе двоичного дерева, как показано ниже. S Представление разреженного массива в виде двоичного дерева По сути, двоичное дерево — это просто видоизмененный двусвязный список. Его основное преимущество заключается в возможности быстрого поиска. Именно благо- даря этому удается очень быстро выполнять вставки и затрачивать совсем немного времени на доступ к элементам. (Ведь двоичные деревья идеально подходят для при- ложений, в которых требуется структура связанного списка, в которой поиск должен занимать совсем немного времени.) Чтобы использовать двоичное дерево для реализации электронной таблицы, необ- ходимо изменить структуру cell следующим образом: struct cell { char cell_name[9]; /* имя ячейки, напр. Al, В34 */ char formula[128]; /* данные, напр. 10/В2 */ struct cell *left; /* указатель на левое поддерево */ struct cell *right; /* указатель на правое поддерево */ } list_entry; Функцию stree () из главы 22 можно модифицировать так, чтобы она строила дерево на основании имени ячейки. В следующем коде предполагается, что параметр п является указателем на вставляемый элемент дерева. struct cell *stree( struct cell *root, struct cell *r, struct cell *n) { if(!r) { /* первая вершина в поддереве */ n->left = NULL; n->right = NULL; if(!root) return n; /* первый вход в дерево */ if(strcmp(n->cell_name, root->cell_name) < 0) root->left = n; else root->right = n; return n; } 498 Часть IV. Алгоритмы и приложения
if (strcmp (r->cell_name, n->cell__name) о 0) stree(r, r->right, n); else stree(r, r->left, n); return root; } При вызове функции stree () ей необходимо передавать указатели на корень де- рева в качестве первых двух параметров и указатель на новую ячейку в качестве третьего. Функция возвращает указатель на корень. Чтобы удалить ячейку электронной таблицы, можно воспользоваться показанной ниже модифицированной функцией dtree (), принимающей в качестве ключа имя ячейки: struct cell *dtree( struct cell *root, char *key) { struct cell *p, *p2; if(!root) return root; /* элемент не найден */ if(!strcmp(root->cell_name, key)) { /* удаление корня */ /* это означает пустое дерево */ if(root->left == root->right){ free(root); return NULL; } /* или если одно из поддеревьев пустое ★/ else if(root->left == NULL) { p = root->right; free(root); return p; } else if(root->right == NULL) { p = root->left; free(root); return p; } /* или если оба поддерева непустые */ else { р2 = root->right; р = root->right; while(p->left) p = p->left; p->left = root->left; free(root); return p2; } } if(strcmp(root->cell_name, key)<=0) root->right = dtree(root->right, key); else root->left = dtree(root->left, key); return root; } Наконец, для быстрого поиска ячейки электронной таблицы по ее имени можно воспользоваться модифицированной версией функции search (). struct cell *search_tree( struct cell *root, Глава 23. Разреженные массивы 499
char *key) { if(!root) return root; /* пустое дерево */ while(strcmp(root->cell_name, key)) { if (strcmp (root->cell_name, key)4 ’<= 0) root = root->right; else root = root->left; if(root == NULL) break; } return root; } Анализ метода представления в виде двоичного дерева Применение двоичного дерева значительно уменьшает время вставки и поиска элементов по сравнению со связанным списком. Следует помнить, что последова- тельный поиск требует в среднем и/2 сравнений, где п — количество элементов спи- ска. По сравнению с этим двоичный поиск требует только log2w сравнений (если дере- во сбалансировано). Кроме того, двоичные деревья так же экономно расходуют па- мять, как и связанные списки. Тем не менее, в некоторых ситуациях есть лучшие альтернативы, чем двоичные деревья. Ц| Представление разреженного массива в виде массива указателей Допустим, что электронная таблица имеет размер 26x100 (от А1 до Z100), то есть состоит из 2 600 элементов. Теоретически можно хранить элементы таблицы в сле- дующем массиве структур: I struct cell { char cell_name[9]; char formula[128]; } list_entry[2600] ; /* 2600 ячеек */ Ho 2 600, умноженное на 137 байтов (размер этой структуры в байтах), равняется 356 200 байтов памяти. Это слишком большой объем памяти, чтобы тратить его на не полностью используемый массив. Тем не менее, можно создать массив указателей (pointer array) на структуры типа cell. Для хранения массива указателей требуется на- много меньше памяти, чем для массива структур. При каждом присвоении ячейке логи- ческого массива данных под эти данные выделяется память, а соответствующему указа- телю в массиве указателей присваивается адрес выделенного фрагмента. Такая схема по- зволяет добиться более высокой производительности, чем при связанном списке или двоичном дереве. Описание массива указателей выглядит следующим образом: struct cell { char cell_name[9]; char formula[128] ; } list_entry; struct cell *sheet[2600]; /* массив из 2600 указателей */ Этот меньший по объему занимаемой памяти массив используется для хранения указателей на вводимые в электронную таблицу данные. При вводе очередной записи в соответствующую) ячейку массива заносится указатель на введенные данные. На 500 Часть IV. Алгоритмы и приложения
рис. 23.2 показано, как выглядит в памяти разреженный массив, представленный в виде массива указателей. Перед использованием массива указателей каждый его элемент необходимо про- инициализировать нулем (NULL), что означает отсутствие данной записи. Ниже пока- зана функция инициализации массива: void init_sheet(void) { register int t; for(t=0; t < 2600; ++t) sheet[t] = NULL; } Представление разреженного массива в виде массива указателей 0 0 0 0 0 0 0 0 > А [8] > А[4] > А[1] L—►! А[0] Рис. 23.2. Представление разреженного массива в виде массива указателей Когда пользователь вводит формулу в ячейку, занимающую определенное положение в электронной таблице (а положение ячейки, как известно, определяется ее именем), вы- числяется индекс в массиве указателей sheet. Этот индекс получается путем преобразова- ния строкового представления имени ячейки в число, как показано в следующем листинге: void store(struct cell *i) { int loc; char *p; /* вычисление индекса по заданному имени */ loc = *(i->cell_name) - ’А’; /* столбец */ р = &(i->cell_name[1]); loc += (atoi(p)-l) * 26; /* количество строк * ширина строки + столбец */ if(loc >= 2600) { printf("Ячейка за пределами массива.п”); return; } sheet[loc] = i; /* поместить указатель в массив */ } При вычислении индекса в функции store () предполагается, что все имена ячеек начинаются с прописной буквы, за которой следует целое число, например, В34, С19 и т. д. Поэтому в результате вычислений по формуле, запрограммированной в функ- ции store (), имя ячейки А1 соответствует индексу 0, имя В1 соответствует индексу Глава 23. Разреженные массивы 501
1, А2 — 26 и т. д. Поскольку имена ячеек уникальны, индексы также уникальны и указатель на каждую запись хранится в соответствующей позиции массива. Если сравнить эту процедуру с версиями, использующими связанный список или двоичное дерево, становится понятно, насколько она проще и короче. Функция удаления ячейки deletecell () также становится очень короткой. При вызове она просто обнуляет указатель на элемент и возвращает системе память. void deletecell(struct cell *i) { int loc; char *p; /* вычисление индекса по заданному имени ячейки */ loc = *(i->cell_name) - ’А’; /* столбец */ р = &(i->cell_name[1]); loc += (atoi(p)-l) * 26; /* количество строк * ширина строки + столбец */ if(loc >= 2600) { printf("Ячейка за пределами массива.п"); return; } if(!sheet[loc]) return; /* не освобождать, если указатель нулевой (null) */ free(sheet[loc]); /* освободить системную память */ sheet[loc] = NULL; } Этот код также намного быстрее и проще, чем в версии со связанным списком. Процесс поиска ячейки по имени прост, поскольку имя ячейки однозначно определяет индекс в массиве указателей. Поэтому функция поиска принимает следующий вид: struct cell *find(char *cell_name) { int loc; char *p; /* вычисление индекса по заданному имени ячейки */ loc = *(i->cell_name) - ’А’; /* столбец */ р = &(i->cell_name[1]); loc += (atoi(p)-l) * 26; /* количество строк * ширина строки + столбец */ if(loc>=2600 || !sheet[loc]) { /* эта ячейка пустая */ printf("Ячейка не найдена.п"); return NULL; /* поиск неуспешный */ } else return sheet[loc]; } Анализ метода представления разреженного массива в виде массива указателей Метод реализации разреженного массива на основе массива указателей обеспечи- вает намного более быстрый доступ к элементам, чем методы на основе связанного списка и двоичного дерева. Если массив не очень большой, выделение памяти для массива указателей лишь незначительно уменьшает объем свободной памяти системы. 502 Часть IV. Алгоритмы и приложения
Тем не менее, в массиве указателей для каждой ячейки выделяется некоторый объем памяти независимо от того, используется она или нет. В некоторых приложениях это может быть ограничением, хотя в общем случае это не является проблемой. Isl Хэширование Хэширование (hashing) — это процесс получения индекса элемента массива непо- средственно в результате операций, производимых над ключом, который хранится вместе с элементом или даже совпадает с ним. Генерируемый индекс называется хэш- адресом (hash). Традиционно хэширование применяется к дисковым файлам как одно из средств уменьшения времени доступа. Тем не менее, этот общий метод можно применить и с целью доступа к разреженным массивам. В предыдущем примере с массивом указателей использовалась специальная форма хэширования, которая назы- вается прямая адресация. В ней каждый ключ соответствует одной и только одной ячейке массива1. Другими словами, каждый индекс, вычисленный в результате хэши- рования, уникальный. (При представлении разреженного массива в виде массива ука- зателей хэш-функция не должна обязательно реализовывать прямую адресацию — просто это был очевидный подход к реализации электронной таблицы.) В реальной жизни схемы прямого хэширования встречаются редко; обычно требуется более гиб- кий метод. В данном разделе показано, как можно обобщить метод хэширования, придав ему большую мощь и гибкость. В примере с электронной таблицей понятно, что даже в самых сложных случаях используются не все ячейки таблицы. Предположим, что почти во всех случаях фак- тически занятые ячейки составляют не более 10 процентов потенциально доступных мест. Это значит, что если таблица имеет размер 260x100 (2 600 ячеек), в любой мо- мент времени будет использоваться лишь примерно 260 ячеек. Этим подразумевается, что самый большой массив, который понадобится для хранения всех занятых ячеек, будет в обычных условиях состоять Только из 260 элементов. Но как ячейки логиче- ского массива сопоставить этому меньшему физическому массиву? И что происходит, когда этот массив переполняется? Ниже предлагается одно из возможных решений. Когда пользователь вводит данные в ячейку электронной таблицы (т.е. заполняет элемент логического массива), позиция ячейки, определяемая по ее имени, использу- ется для получения индекса (хэш-адреса) в меньшем физическом массиве. При вы- полнении хэширования физический массив называется также первичным массивом. Индекс в первичном массиве получается из имени ячейки, которое преобразуется в число, точно так, как и в примере с массивом указателей. Но затем это число делится на 10, в результате чего получается начальная точка входа в первичный массив. (Помните, что в данном случае размер физического массива составляет только 10 % размера логического массива.) Если ячейка физического массива по этому индексу свободна, в нее заносятся логический индекс и данные. Но поскольку 10 логических позиций соответствуют одной физической позиции, могут возникнуть коллизии при вычислении хэш-адресов1 2. Когда это происходит, записи сохраняются в связанном списке, иногда называемом списком коллизий (collision list). С каждой ячейкой первич- ного массива связан отдельный список коллизий. Конечно, до возникновения колли- зии эти списки имеют нулевую длину, как показано на рис. 23.3. Предположим, требуется найти элемент в физическом массиве по его логическому индексу. Сначала необходимо преобразовать логический индекс в соответствующее 1 Иными словами, хэш-функция является биекцией. — Прим. ред. 2 Т.е. ситуации, когда разным ключам к^ к2 соответствует один и тот же хэш-адрес: h(kj)=h(k2) (здесь h — хэш-функция). — Прим. ред. Глава 23. Разреженные массивы 503
значение хэш-адреса и проверить, соответствует ли логический индекс, хранящийся в полученной позиции физического массива, искомому. Если да, информацию можно извлечь. В противном случае необходимо просматривать список коллизий для данной позиции до тех пор, пока не будет найден требуемый логический индекс или пока не будет достигнут конец цепочки. Указатель на Рис. 23.3. Пример хэширования В примере хэширования используется массив структур под названием primary: tfdefine MAX 260 struct htype { int index; /* логический индекс */ int val; /* собственно значение элемента данных */ struct htype *next; /* указатель на следующий элемент с таким же хэш-адресом */ } primary[МАХ]; Перед использованием этого массива необходимо его инициализировать. Следующая функция присваивает полю index значение -1 (значение, которое по определению нико- 504 Часть IV. Алгоритмы и приложения
гда не будет сгенерировано в качестве индекса); это значение обозначает пустой элемент. Значение NULL в поле next соответствует пустой цепочке хэширования1. /★ Инициализация хэш-массива. */ void init(void) { register int i; for (i=0; i<MAX; i++) { primary[i].index = -1; primary[i].next = NULL; /* пустая цепочка */ primary[i].val = 0; } } Процедура store () преобразует имя ячейки в хэш-адрес в первичном массиве primary. Если позиция, на которую указывает значение хэш-адрес, занята, процедура ав- томатически добавляет запись в список коллизий с помощью модифицированной версии функции sis tore () из предыдущей главы. Логический индекс также сохраняется, по- скольку он понадобится при извлечении элемента. Данные функции показаны ниже: /* Вычисление хэш-адреса и сохранение значения. */ void store(char *cell_name, int v) { int h, loc; struct htype *p; /* получение хэш-адреса */ loc = *cell_name - ’A’; /* столбец */ loc += (atoi(&cell_name[1])-1) * 26; /* строка * ширина + столбец */ h = loc/10; /* хэш-адрес */ /* Сохранить в полученной позиции, если она не занята либо если логические индексы совпадают - то’ есть, при обновлении. */ if(primary[h].index==-l I I primary[h].index==loc) { primary[h].index = loc; primary[h].val - v; return; } /* в противном случае, создать список коллизий либо добавить в него элемент */ р = (struct htype *) malloc(sizeof(struct htype)); if(!p) { printf("He хватает памятип"); return; } p->index = loc; p->val = v; slstore(p, &primary[h]); } /* Добавление элементов в список коллизий. */ void slstore(struct htype *i, 1 Цепочка хэширования (hash chain) — цепочка, соединяющая элементы хэш-таблицы с од- ним и тем же хэш-кодом. Ранее автор назвал ее списком коллизий (collision list). Иногда она на- зывается также пакетом переполнения. — Прим. ред. Глава 23. Разреженные массивы 505
struct htype *start) { struct htype *old, *p; old = start; /* найти конец списка */ while(start) { old = start; start = start->next; } /* связать с новой записью */ old->next = i; i->next = NULL; } Для того чтобы получить значение элемента, программа должна сначала вычислить хэш-адрес и проверить, совпадает ли с искомым логический индекс, хранящийся в полученной позиции физического массива. Если совпадает, возвращается значение ячейки; в противном случае — производится поиск в списке коллизий. Функция find (), выполняющая эти задачи, показана ниже: /* Вычисление хэш-адреса и получение значения. */ int find(char *cell_name) { int h, loc; struct htype *p; /* получение значения хэш-адреса */ loc = *cell_name - 'A'; /* столбец */ loc += (atoi(&cell_name [1])-1) * 26; /* строка * ширина + столбец */ h = loc/10; /* хэш-адрес */ /* вернуть значение, если ячейка найдена */ if(primary[h].index == loc) return(primary[h].val); else { /* в противном случае просмотреть список коллизий */ р = primary[h].next; while(р) { if(p->index == loc) return p->val; p = p->next; } printf("Ячейки нет в массивеп"); return -1; } } Создание функции удаления оставлено читателю в качестве упражнения. (Подсказка. Просто обратите процесс вставки.) Показанный выше алгоритм хэширования очень прост. Как правило, на практике применяются более сложные методы, обеспечивающие более равномерное распреде- ление индексов в первичном массиве, что устраняет длинные цепочки хэширования. Тем не менее, основной принцип остается таким же. Анализ метода хэширования В лучшем случае (довольно редком) каждый физический индекс, вычисляемый хэш-функцией, уникален, а время доступа примерно равно времени доступа при пря- мой адресации. Это значит, что списки коллизий не создаются, а все операции вы- борки являются по сути операциями прямого доступа. Однако так бывает редко, по- 506 Часть IV. Алгоритмы и приложения
скольку для этого требуется, чтобы логические индексы равномерно распределялись в пространстве физических индексов. В худшем случае (также редком) схема хэширова- ния вырождается в связанный список. Это происходит, когда значения хэш-адресов всех логических индексов совпадают. В среднем (и наиболее вероятном) случае время доступа при хэшировании равно времени доступа при прямой адресации плюс неко- торая константа, пропорциональная средней длине цепочек хэширования. Самый важный фактор при реализации разреженных массивов методом хэширования — вы- бор такого алгоритма хэширования, при котором равномерно распределяются физиче- ские индексы, что позволяет избежать образования длинных списков коллизий. В Выбор метода При выборе одного из методов представления разреженного массива — с помощью связанного списка, двоичного дерева, массива указателей или хэширования — необ- ходимо руководствоваться требованиями к скорости и эффективному использованию памяти. Кроме того, необходимо учесть, насколько плотно будет заполнен разрежен- ный массив. Если логический массив заполнен очень редко, самыми эффективными по исполь- зованию памяти оказываются связанные списки и двоичные деревья, поскольку в них память выделяется только для тех элементов, которые действительно используются. Для самих связей требуется очень небольшой дополнительный объем памяти, кото- рым обычно можно пренебречь. Представление в виде массива указателей предпола- гает создание целиком всего массива из указателей, даже если некоторые элементы не используются. При этом в памяти должен не только поместиться весь массив, но еще должна остаться свободная память для работы приложения. В некоторых случаях это может быть серьезной проблемой. Обычно можно заранее оценить примерный объем свободной памяти и решить, достаточен ли он для вашей программы. Метод хэширо- вания занимает промежуточное место между представлениями с помощью массива указателей и связанного списка или двоичного дерева. Несмотря на то, что весь пер- вичный массив, даже если он не используется, должен находиться в памяти, этот мас- сив все равно будет меньше, чем массив указателей. Если логический массив будет заполнен довольно плотно, ситуация существенно меняется. В этом случае создание массива указателей и хэширование становятся более приемлемыми методами. Более того, время поиска элемента в массиве указателей по- стоянно и не зависит от степени заполнения логического массива. Хотя время поиска при хэшировании и не постоянно, оно ограничено сверху некоторым небольшим зна- чением. Но в случае связанного списка и двоичного дерева среднее время поиска уве- личивается по мере заполнения массива. Это следует помнить, если время доступа яв- ляется критическим фактором. Глава 23. Разреженные массивы 507
Полный справочник по Глава 24 Синтаксический разбор и вычисление выражений
Как написать программу, которая будет получать на входе строку, содержащую число- вое выражение, например (10 - 5) * 3, и выдавать соответствующий результат? Если среди программистов и есть “высшие священники”, то это те, кто знает, как решить по- добную задачу. Многие, притом высококвалифицированные в других областях програм- мисты не имеют представления о том, как трансляторы, разработанные для компиляции программ, написанных на языках высокого уровня, преобразовывают алгебраические выражения в команды, выполняемые компьютером. Эта процедура называется синтак- сический разбор выражений (expression parsing) и является основой всех компиляторов и интерпретаторов языков, электронных таблиц и всех остальных программ, в которых требуется превращать числовые выражения в форму, понятную компьютеру. Несмотря на свою загадочность, синтаксический разбор выражений является до- вольно прямолинейным процессом и во многих аспектах проще, чем некоторые дру- гие задачи программирования. Это обусловлено тем, что задача синтаксического раз- бора четко определена и решается в соответствии со строгими правилами алгебры. В настоящей главе будет разработан рекурсивный нисходящий синтаксический анализатор. или синтаксический анализатор методом рекурсивного спуска (recursive-descent parser), а также все функции, необходимые для вычисления выражений. Освоив принцип дей- ствия этой программы, вы с легкостью сможете доработать и модифицировать ее в соответствии со своими задачами. На заметку В интерпретаторе языка С, представленном в части VI данной книги, приме- няется улучшенный вариант разработанного здесь синтаксического анализа- тора. Если вы будете изучать интерпретатор С, материал данной главы бу- дет вам особенно полезен. S Выражения Несмотря на то, что выражения можно составлять из данных любых типов, в на- стоящей главе рассматриваются только числовые выражения. Для наших целей мы ус- ловимся, что числовые выражения будут состоять из следующих элементов: Числа Операторы (знаки операций) %, = Скобки Переменные Оператор Л означает возведение в степень, как в языке BASIC, а символ = обозна- чает оператор присваивания. Перечисленные элементы можно комбинировать в вы- ражения согласно правилам алгебры, например: 10-8 (100 - 5) * 14/6 а 4- Ь - с 10Л5 а = 10 - b Пусть операторы имеют следующий приоритет: высший унарные + и - */% 4- - НИЗШИЙ = 5Г0 Часть IV. Алгоритмы и приложения
Если выражение содержит операторы, имеющие одинаковый приоритет, то вычис- ления выполняются слева направо1. В примерах данной главы все переменные имеют имена из одной буквы (другими сло- вами, допускается 26 переменных, от А до Z). Имена переменных не чувствительны к ре- гистру (заглавных или строчных букв). Например, а и А обозначают одну и ту же перемен- ную. Каждое числовое значение имеет тип double, хотя не составляет труда написать про- цедуры для обработки значений других типов. Наконец, чтобы логика программ была простой и понятной, будет производиться лишь минимальный контроль за ошибками. Если вы еще не задумывались о процессе синтаксического разбора выражений, попробуйте вычислить следующее выражение: 10 - 2 ♦ 3 Вы знаете, что оно равно 4. Несмотря на то, что можно легко создать программу, которая вычислит данное конкретное выражение, вопрос состоит в том, как написать программу, выдающую правильный результат для произвольного выражения. В начале вам может придти в голову следующий алгоритм: а = получить первый операнд while(ecTb операнды) { ор = получить оператор b = получить второй операнд а = a op b } Эта процедура получает первый операнд, оператор и второй операнд, выполняет над ними первую операцию, а затем читает следующий оператор и операнд (если они есть) и выполняет следующую операцию, обозначенную полученным оператором и т. д. Однако при данном подходе при вычислении выражения 10 - 2 * 3 в результате получается 24 (т. е. 8 * 3) вместо 4, поскольку описанная процедура не учитывает приоритет операторов. Нельзя просто выбирать операнды и операторы слева направо, поскольку правила алгебры гласят, что умножение производится прежде вычитания. Некоторые начинающие программисты думают, что эту проблему легко преодолеть. В очень редких случаях им это удается. Но проблема усложняется при добавлении ско- бок, возведении в степень, появлении переменных, вызове функций и т.п. Несмотря на то, что существует несколько способов написания программы вычисления выражений, описываемый нами способ наиболее прост для кодирования человеком. Он также является самым распространенным. (При некоторых других методах создания син- таксических анализаторов в них применяются сложные таблицы, генерируемые другой компьютерной программой. Такие анализаторы иногда называются таблично управляемыми (table-driven).) Описанный здесь метод называется методом рекурсивного спуска, и, читая главу, вы, несомненно, догадаетесь, почему он так называется. В Разбиение выражения на лексемы Для того чтобы вычислять выражения, необходимо уметь разбивать их на отдель- ные составляющие. Например, выражение А * В - (W + 10) состоит из таких элемен- тов: А, ♦, В, -, (, W, +, 10и). Каждый из них представляет единую неделимую часть 1 Впрочем, здесь есть одно часто встречающееся исключение — оператор возведения в сте- пень. X**Y**Z означает, как правило, не (X**Y)**Z, a X**(Y**Z). (Оператор ** — обычное обо- значение операции возведения в степень во многих языках программирования, наиболее рас- пространенным из которых является Фортран.) Точно то же происходит и в алгебре: выражение аь' означает а{Ь'}, а не (аь)с =аЬс. — Прим. ред. Глава 24. Синтаксический разбор и вычисление выражений 511
выражения. В общем случае необходима функция, которая возвращает один за другим все элементы выражения. Эта функция также должна уметь пропускать пробелы и символы табуляции и определять конец выражения. Каждый элемент выражения называется лексемой (token). Поэтому функция, воз- вращающая очередную лексему, часто называется get token (). В этой функции ис- пользуется глобальный указатель на строку с разбираемым выражением. В показанной здесь версии функции get_token () этот глобальный указатель называется prog. Пе- ременная prog описана глобально, поскольку она должна сохранять свое значение между вызовами функции get token () и быть доступной другим функциям. Помимо значения возвращаемой лексемы, необходимо знать ее тип. Для анализатора, разраба- тываемого в данной главе, понадобятся только три типа: переменная, число и разде- литель. Им соответствуют константы variable, number и delimiter, (delimiter используется как для операторов, так и для скобок.) Ниже приведен текст функции get_token() вместе с необходимыми глобальными описаниями, константами и вспомогательной функцией: #define DELIMITER 1 #define VARIABLE 2 #define NUMBER 3 extern char *prog; /* указатель на анализируемое выражение */ char token[80]; char tok_type; /* Данная функция возвращает очередную лексему. */ void get_token(void) { register char *temp; tok_type = 0; temp = token; *temp = ’’ ; if(!*prog) return; /* конец выражения */ while(isspace(*prog)) ++prog; /* пропустить пробелы, символы табуляции и пустой строки */ if(strchr("+-*/%Л=()", *prog)){ tok_type = DELIMITER; /* продвинуться к следующему символу */ *temp++ = *prog++; } else if(isalpha(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = VARIABLE; } else if(isdigit(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = NUMBER; } *temp = ’ 0 ’; } /* Возвращает значение ИСТИНА, если с является разделителем. */ int isdelim(char с) { 512 Часть IV. Алгоритмы и приложения
|if(strchr(" +-/*%л=0", с) || с==9 II с==’г’ || с==0) return 1; return 0; } Давайте рассмотрим приведенные выше функции более подробно. После несколь- ких инициализаций функция get_token () проверяет, не достигнут ли символ конца строки ( ’О' ), завершающий выражение. Если в выражении еще есть неразобранная часть, функция get_token () сначала пропускает ведущие пробелы, если они имеют- ся. После этого переменная prog указывает на число, переменную, оператор или — если выражение завершалось пробелами — на символ конца строки ( 'О' ). Если оче- редной символ является оператором, он возвращается в виде строки, хранимой в гло- бальной переменной token, а переменной tok_type, содержащей тип полученной лексемы, присваивается значение DELIMITER. Если же следующий символ является буквой, он считается именем переменной и возвращается в строковой переменной token. При этом tok_type получает значение VARIABLE. В случае, когда очередной символ является цифрой, считывается все число, причем оно помещается в перемен- ную token, а его типом будет NUMBER. Наконец, если следующий символ не является ни одним из перечисленных выше, считается, что достигнут конец выражения. В этом случае token содержит пустую строку, возврат которой означает конец выражения. Как уже было сказано ранее, чтобы не усложнять код этой функции, были опуще- ны некоторые средства контроля за ошибками и сделаны некоторые допущения. На- пример, любой нераспознанный символ завершает выражение. Кроме того, в данной версии программы имена переменных могут иметь любую длину, но значащей являет- ся только первая буква. В соответствии с требованиями конкретной задачи вы можете усложнить средства контроля за ошибками и добавить другие подробности. Функцию get_token() можно доработать или модифицировать, чтобы она выбирала из вход- ного выражения строки символов, числа других типов или лексемы другого типа. Чтобы лучше понять принцип действия функции get_token (), ниже приведены возвращаемые ей лексемы и типы лексем для следующего входного выражения: А + 100 - (В ♦ С) / 2 Лексема Тип лексемы А VARIABLE + 100 DELIMITER NUMBER - DELIMITER ( В * DELIMITER VARIABLE DELIMITER с ) / 2 нуль (конец строки) VARIABLE DELIMITER DELIMITER NUMBER 0 (нуль) Следует помнить, что переменная token всегда содержит строку, завершающуюся символом конца строки ( 'О' ), даже если эта строка состоит только из одного символа. Глава 24. Синтаксический разбор и вычисление выражений 513
S Разбор выражений Существуют различные способы синтаксического разбора и вычисления выражений. При работе с рекурсивным нисходящим синтаксическим анализатором молено представ- лять себе выражения в виде рекурсивных структур данных, т.е. определение выражения рекурсивно. Иными словами, понятие выражения определяется через понятие выраже- ния. Например, если принять, что в выражениях можно использовать только +, -, *, / и скобки, то все выражения можно определить следующими правилами: выражение -> слагаемое [+ слагаемое] [- слагаемое] слагаемое -> множитель [♦ множитель] [/ множитель] множитель -> переменная, число или (выражение) Квадратные скобки означают необязательный элемент, а символ -> означает “порождает”. Подобные правила обычно называются порождающими правилами, или продукциями. Поэтому в качестве определения слагаемого можно привести следующее: “Слагаемое порождает множитель, умноженный на множитель, или множитель, де- ленный на множитель”. Обратите внимание на то, что приоритет операций заложен в определении выражения. Давайте рассмотрим пример. Выражение 10 + 5 * в состоит из двух слагаемых: 10 и 5 * в. Второе слагаемое состоит из двух множителей: 5 и с. Эти множители представляют собой одно число и одну переменную. С другой стороны, выражение 14 * (7 - С) содержит два множителя: 14 и (7 - С). Эти множители представляют собой одно число и одно выражение в скобках. Вы- ражение в скобках состоит из двух слагаемых: числа и переменной. Описанный процесс анализа выражений составляет основу работы рекурсивного нисходящего синтаксического анализатора, который, по существу, состоит из набора взаимно рекурсивных функций, вызывающих друг друга по цепочке. На каждом этапе своей работы анализатор выполняет указанные операции в алгебраически корректной последовательности. Чтобы увидеть, как это происходит, давайте разберем приведен- ное ниже выражение в соответствии с вышеуказанными порождающими правилами и выполним арифметические операции: 9/ 3 -(100 + 56) Если выражение разобрано корректно, разбор происходил в следующем порядке: 1. Получить первое слагаемое, 9/3. 2. Получить каждый множитель и выполнить деление чисел. В результате полу- чилось число 3. 3. Получить второе слагаемое, (100 + 56). В этот момент начинается рекурсив- ная обработка данного подвыражения. 4. Получить оба слагаемых и выполнить сложение. В результате получилось число 156. 5. Вернуться из рекурсивного вызова и вычесть 156 из 3. Окончательным отве- том является -153. Если вас это несколько сбило с толку, не расстраивайтесь. Синтаксический разбор выражений — довольно сложное занятие, к которому нужно привыкнуть. Необходимо помнить о двух основных моментах, когда речь идет о таком рекурсивном представле- нии выражений. Во-первых, приоритет операций неявно закладывается в порождаю- щие правила. Во-вторых, этот метод разбора и вычисления выражений очень похож на тот способ, которым люди вычисляют математические выражения. 514 Часть IV. Алгоритмы и приложения
!1 Простая программа синтаксического анализа выражений В оставшейся части данной главы приведены два синтаксических анализатора. Первый из них разбирает и вычисляет только константные выражения, т.е. выраже- ния, в которых нет переменных. Второй анализатор способен работать с 26 перемен- ными, от А до Z. Ниже приводится полная версия простого рекурсивного нисходящего синтаксиче- ского анализатора, вычисляющего выражения, в которых при вычислении операнды представляются в формате с плавающей запятой. /* Этот модуль содержит простой синтаксический анализатор, который не распознает переменные. */ #include <stdlib.h> #include <ctype.h> #include <stdio.h> #include <string.h> #define DELIMITER 1 #define VARIABLE 2 #define NUMBER 3 extern char *prog; /* содержит анализируемое выражение */ char token[80]; char tok_type; void eval_exp(double *answer), eval_exp2(double *answer); void eval__exp3(double *answer), eval_exp4(double *answer); void eval_exp5(double *answer), eval_exp6(double *answer); void atom(double *answer); void get_token(void)r putback(void); void serror(int error); int isdelim(char c); /* Точка входа анализатора. */ void eval_exp(double *answer) { get_token(); if(!*token) { serror(2); return; } eval_exp2(answer); if(*token) serror(O); /* последней лексемой должен быть нуль */ } /* Сложение или вычитание двух слагаемых. */ void eval_exp2(double *answer) { register char op; double temp; Глава 24. Синтаксический разбор и вычисление выражений 515
eval_ехрЗ(answer); while((op = *token) == •+• || op == { get_token() ; eval_exp3(&temp); switch(op) { case •- •: ♦ answer = *answer - temp; break; case '+’: ♦ answer = *answer + temp; break; } } } /* Умножение или деление двух множителей. */ void eval_exp3(double *answer) { register char op; double temp; eval__exp4 (answer) ; while((op = *token) == ’*’ || op == ’/’ II op == ’%’) { get_token(); eval_exp4(&temp); switch(op) { case ’* ’ : ♦ answer = *answer * temp; break; case ’/’: if(temp ==0.0) { serror(3); /* деление на 0 */ ♦answer = 0.0; } else *answer = *answer / temp; break; case ’% : ♦ answer = (int) *answer % (int) temp; break; } } } /* Возведение в степень */ void eval—exp4(double *answer) { double temp, ex; register int t; eval_exp5(answer); if(*token == ’Л’) { get—token(); eval—exp4(&temp); ex = *answer; if(temp==0.0) { ♦answer = 1.0; return; } 516 Часть IV. Алгоритмы и приложения
for(t=temp-l; t>0; —t) *answer = (*answer) * (double)ex; } } /* Вычисление унарных операторов + и -. */ void eval_exp5(double *answer) { register char op; op = 0; if((tok_type == DELIMITER) && *token==’+’ || *token == ’-’) { op = *token; get_token(); } eval—exp6(answer); if(op == •-•) *answer = -(*answer); } /* Вычисление выражения в скобках. */ void eval_exp6(double *answer) { if((*token ==’(’)) { get_token(); eval_exp2(answer); if(*token != ’)•) serror(1); get_token(); } else atom(answer); /* Получение значения числа. */ void atom(double *answer) { if(tok_type == NUMBER) { *answer = atof(token); get_token(); return; } serror(0); /* иначе синтаксическая ошибка в выражении */ } /* Возврат лексемы во входной поток. */ void putback(void) { char *t; t = token; for(; *t; t++) prog—; } /* Отображение сообщения об ошибке. */ void serror(int error) { static char *e[]= { "Синтаксическая ошибка", ’’Несбалансированные скобки”, Глава 24. Синтаксический разбор и вычисление выражений 517
"Нет выражения", "Деление на нуль" }; printf("%sn", е[error]); . } /* Возврат очередной лексемы. */ void get—token(void) { register char *temp; tok_type = 0; temp = token; *temp = ’’; if(!*prog) return; /* конец выражения */ while(isspace(*prog)) ++prog; /* пропустить пробелы, символы табуляции и пустой строки */ if(strchr("+-*/%Л=()", *prog)){ tok_type = DELIMITER; /* перейти к следующему символу */ *temp++ = *prog++; } else if(isalpha(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = VARIABLE; } else if(isdigit(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = NUMBER; } *temp = •’; } /* Возвращает значение ИСТИНА, если с является разделителем. */ int isdelim(char с) { if(strchr(" +-/*%Л=О", с) || с==9 || с=='г’ || с==0) return 1; return 0; } В приведенном здесь виде анализатор поддерживает следующие операторы: +, -, ♦, /, %. Кроме того, он умеет возводить в целочисленную степень О и вычислять унар- ный минус. А еще анализатор умеет корректно распознавать скобки. Обратите внима- ние, что он состоит из шести уровней, а также функции atom, которая возвращает значение числа. Как уже обсуждалось ранее, в глобальной переменной token возвра- щается очередная лексема из строки, содержащей выражение, а в tok_type —тип лексемы. Переменная-указатель prog указывает на строку, содержащую выражение. Следующая простая функция main () демонстрирует использование этого анализатора: I/* Демонстрационная программа для анализатора. */ #include <stdlib.h> #include <ctype.h> #include <stdio.h> 518 Часть IV. Алгоритмы и приложения
#include <string.h> char *prog; void eval_exp(double *answer); int main (void)- { double answer; char *p; p = (char *) malloc(100); if(’p) { printf("Ошибка при выделении памяти.n") ; exit(1); } /* Обработка выражений до ввода пустой строки. */ do { prog = р; printf("Введите выражение: "); gets(prog); if(!*prog) break; eval__exp (&answer) ; printf("Результат: %.2fn", answer); } while(*p); return 0; } Чтобы понять, как же в действительности анализатор вычисляет выражение, давайте проработаем следующий пример. (Допустим, что prog указывает на начало выражения.) 10 - 3 ♦ 2 При вызове функции eval_exp() — входной точки анализатора — из входной строки выбирается лексема. Если она является пустой строкой, то функция печатает сообщение “Нет выражения” и завершается. Однако в данном случае лексемой явля- ется число 10. Поскольку это не пустая строка, вызывается функция eval_exp2 (). В результате, функция eval_exp2() вызывает eval_exp3(), a eval_exp3() вызывает eval_exp4(), а та в свою очередь вызывает eval_exp5(). Затем функция eval_exp5 () проверяет, не является ли лексема унарным плюсом или минусом. В данном случае это не так, поэтому вызывается функция eval_exp6 (). В этот момент eval_exp6 () может рекурсивно вызвать либо eval_exp2 () (в случае выражения, за- ключенного в скобки), либо atom(), чтобы определить значение числа. Поскольку лексема не является открывающей скобкой, выполняется функция atom() и пере- менной * answer присваивается значение 10. Затем происходит выборка следующей лексемы и возврат из цепочки вызовов функций. Лексемой становится оператор -, а управление возвращается функции eval_exp2 (). То, что происходит дальше, очень важно. Поскольку текущей лексемой является символ -, он сохраняется в переменной ор. Затем анализатор выбирает следующую лексему и спуск по цепочке начинается снова. Как и раньше, вызывается функция atom(). Полученное значение 3 возвращается в переменной *answer, и считывается лексема ♦. Это вызывает возврат по цепочке до eval_exp3(), где считывается по- следняя лексема 2. В этот момент происходит первая арифметическая операция — умножение 3 на 2. Полученный результат возвращается функции eval_exp2 (), где происходит вычитание. В результате вычитания в ответе получается 4. Несмотря на Глава 24. Синтаксический раэбор и вычисление выражений 519
то, что этот процесс может поначалу показаться сложным, самостоятельная проработ- ка других примеров поможет вам разобраться в работе анализатора. Данный анализатор подошел бы для настольного калькулятора, что было проде- монстрировано предыдущей программой, или для небольшой базы данных. Однако перед тем как использовать его для разбора языка программирования или в сложном калькуляторе, в него необходимо добавить средства работы с переменными. Это явля- ется предметом следующего раздела. И Работа с переменными в анализаторе Во всех языках программирования, многих калькуляторах и электронных таблицах предусмотрены переменные, позволяющие сохранять значения для дальнейшего ис- пользования. Для того чтобы синтаксический анализатор из предыдущего примера обладал такой возможностью, в него необходимо внести некоторые дополнения. Во- первых, это, конечно, сами переменные. Как уже было сказано выше, анализатор бу- дет распознавать только переменные с именами от А до Z. (Впрочем, при желании вы можете избавиться от этого ограничения.) Каждая переменная хранится в одной ячейке массива из 26 элементов типа double. Поэтому в исходный текст анализатора необходимо добавить следующий фрагмент: I double vars[26] = { /* 26 пользовательских переменных, A-Z */ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; Как вы заметили, для удобства пользователя все переменные инициализируют- ся нулями. Кроме этого, понадобится процедура для получения значения заданной перемен- ной. Поскольку имена переменных являются буквами от А до Z, их можно использо- вать для индексации массива vars, вычитая код ASCII буквы А из имени перемен- ной. Ниже показана функция f ind_var (), возвращающая значение переменной: /* Получение значения переменной. */ double find_var(char *s) { if(!isalpha(*s) ) { serror(1); return 0; } return vars[toupper(*token)-’A’]; } Данная функция написана так, что она принимает имена любой длины, но только первый символ является значимым. Данное ограничение можно изменить в соответ- ствии с вашими потребностями. Также необходимо модифицировать функцию atom(), чтобы она обрабатывала как числа, так и переменные. Ниже показана ее новая версия: /* Получение значения числа или переменной. */ void atom(double *answer) { switch(tok_type) { case VARIABLE: *answer = find__var (token) ; get_token(); 520 Часть IV. Алгоритмы и приложения
return; case NUMBER: *answer = atof(token); get_token(); return; default: serror(0) ; } } С технической точки зрения, это все, что требуется анализатору для корректной обработки переменных. Однако пока нет способа присвоить этим переменным значе- ния. Часто это делается за пределами анализатора, но в анализаторе можно рассмат- ривать знак равенства как знак операции присваивания и сделать обработку этого знака частью анализатора. Этого можно достичь несколькими способами. Один из них — добавить в анализатор функцию eval_expl (), показанную ниже: /* Обработка присваивания. */ void eval_expl(double *result) { int slot, ttok_type; char temp_token[80]; if(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 { get—token(); /* получить следующую часть выражения */ eval—ехр2(result); vars[slot] = *result; return; } } eval—exp2(result); } Как вы видите, этой функции приходится заглядывать вперед, чтобы определить, выполняется ли на самом деле присваивание. Это связано с тем, что имя переменной всегда находится перед оператором присваивания, но само по себе наличие имени пе- ременной не гарантирует, что за ней последует присваивание. Другими словами, ана- лизатор воспримет выражение А = 100 как присваивание, причем он может опреде- лить, что А / 10 им не является. Для этого функция eval_expl () считывает из вход- ного потока следующую лексему. Если эта лексема не является знаком равенства, она с помощью функции putback () возвращается во входной поток для последующего использования: Глава 24. Синтаксический разбор и вычисление выражений 521
/★ Возврат лексемы во входной поток. */ void putback(void) { char *t; t = token; for(; *t; t++) prog—; } Ниже приведен полный текст улучшенного анализатора: /* Данный модуль содержит рекурсивный нисходящий синтаксический анализатор, распознающий переменные. */ #include <stdlib.h> #include <ctype.h> #include <stdio.h> #include <string.h> #define DELIMITER 1 #define VARIABLE 2 #define NUMBER 3 extern char *prog; /* указатель на анализируемое выражение */ char token[80]; char tok_type; double vars [26] = { /* 26 пользовательских переменных, A-Z */ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, O'.O, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; void eval_exp(double *answer), eval_exp2(double *answer); void eval_expl(double *result); void eval_exp3(double *answer), eval_exp4(double *answer); void eval_exp5(double *answer), eval_exp6(double *answer); void atom(double *answer); void get_token(void), putback(void); void serror(int error); double find_var(char *s); int isdelim(char c); /* Точка входа анализатора. */ void eval_exp(double *answer) { get_token() ; if(!*token) { serror(2); return; } eval_expl(answer); if(*token) serror(0); /* последняя лексема должна быть нулем */ } /* Обработка присваивания. */ void eval__expl (double *answer) { 522 Часть IV. Алгоритмы и приложения
int slot; char ttok_type; char temp_token[80] ; if(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 { get_token(); /* получить следующую часть выражения */ eval_exp2(answer); vars[slot] = *answer; return; } } eval—exp2(answer) ; } /* Сложение или вычитание двух слагаемых. */ void eval—ехр2(double *answer) { register char op; double temp; eval—exp3(answer); while((op = *token) == •+• || op == ’-•) { get—token(); eval_exp3(&temp); switch(op) { case ’-': *answer = *answer - temp; break; case ’+’: *answer = *answer + temp; break; } } } /* Умножение или деление двух множителей. */ void eval—ехрЗ(double *answer) { register char op; double temp; eval—exp4(answer); while((op = *token) == ’*• I I op == ’/• I I op == ’%’) { I get—token(); Глава 24. Синтаксический разбор и вычисление выражений 523
eval__exp4 (&temp) ; switch(op) { case '*’: ♦answer = *answer * temp; break; case '/’: if(temp ==0.0) { serror(3); /* деление на нуль */ ♦answer = 0.0; } else *answer = *answer / temp; break; case '%: ♦answer = (int) *answer % (int) temp; break; } } } /* Возведение в степень */ void eval__exp4 (double *answer) { double temp, ex; register int t; eval_exp5(answer); if(*token == 'A') { get_token(); eval_exp4(Atemp); ex = *answer; if(temp==0.0) { ♦answer = 1.0; return; } for(t=temp-l; t>0; —t) *answer = (*answer) * (double)ex; } /* Вычисление унарного + и -. */ void eval_exp5(double *answer) { register char op; op = 0; if((tok_type == DELIMITER) && *token=='+’ || *token == ’-') { op = *token; get_token() ; } eval__exp6 (answer) ; if (op == '-') *answer = -(*answer); } /♦ Обработка выражения в скобках. */ void eval_exp6(double *answer) { if((*token == ' (')) { get__token () ; eval_exp2(answer); if(*token != ')') 524 Часть IV. Алгоритмы и приложения
serror(1); get_token(); } else atom(answer); } /* Получение значения числа или переменной. */ void atom(double *answer) { switch(tok_type) { case VARIABLE: *answer = find_var(token); get_token(); return; case NUMBER: *answer = atof(token); get_token(); return; default: serror(0); } I /* Возврат лексемы во входной поток. */ void putback(void) { char *t; t = token; for(; *t; t++) prog—; } /* Отображение сообщения о синтаксической ошибке. */ void serror(int error) { static char *e[]= { ’’Синтаксическая ошибка”, ’’Несбалансированные скобки”, ’’Нет выражения", "Деление на нуль” }; printf("%sn", е[error]); } /* Получение очередной лексемы. */ void get_token(void) { register char *temp; tok_type = 0; temp = token; *temp = ’’; if(!*prog) return; /* конец выражения */ while(isspace(*prog)) ++prog; /* пропустить пробелы, символы табуляции и пустой строки */ Глава 24. Синтаксический разбор и вычисление выражений 525
if(strchr(”+-*/%л=()", *prog)){ tok_type = DELIMITER; /* перейти к следующему символу */ *temp++ = *prog++; } else if(isalpha(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = VARIABLE; } else if(isdigit(*prog)) { while(!isdelim(*prog)) *temp++ = *prog++; tok_type = NUMBER; } *temp = •’; } /* Возвращает значение ИСТИНА, если с является разделителем. */ int isdelim(char с) { if(strchr(" +-/*%Л=О", с) || с==9 || c==’r’ || с==0) return 1; return 0; } /* Получение значения переменной. */ double find__var (char *s) { if(!isalpha(*s)){ serror(1); return 0.0; } return vars[toupper(*token)-’A’]; } Для демонстрации работы данного анализатора можно использовать ту функцию main (), которая использовалась для демонстрации работы простого анализатора. Усо- вершенствованный анализатор позволяет вводить выражения, подобные следующим: А = 10/4 А- В C = A*(F-21) В Проверка синтаксиса в рекурсивном нисходящем анализаторе При разборе выражений синтаксическая ошибка — это просто ситуация, в которой входное выражение не соответствует строгим правилам анализатора. В большинстве случаев это происходит из-за ошибки человека — обычно из-за опечаток. Например, следующие выражения не являются правильными с точки зрения анализаторов, рас- смотренных в данной главе: 10**8 (10 - 5) * 9) /8 526 Часть IV. Алгоритмы и приложения
В первом из них встречаются два оператора подряд, во втором не сбалансированы скобки, а последнее начинается со знака деления. Ни одна из таких последовательно- стей не допускается рассмотренными анализаторами. Поскольку при наличии синтак- сических ошибок анализатор может выдать неправильный результат, необходимо сле- дить, чтобы подобных ошибок не было. При изучении кода анализаторов вы, вероятно, заметили функцию serrorO, вы- зываемую в определенных ситуациях. Эта функция сообщает об ошибках. В отличие от многих других типов анализаторов, рекурсивный спуск облегчает проверку синтак- сиса, поскольку в большинстве случаев она происходит в функциях atom(), f ind_var () и eval_exp6 (), где выполняется проверка правильной расстановки ско- бок. Единственная проблема, связанная с выявлением синтаксических ошибок, за- ключается в том, что при обнаружении ошибки разбор выражения не прекращается. Это может привести к выводу нескольких сообщений об ошибках. Лучший способ реализации функции serrorO — заставить ее выполнять нечто вроде восстановления правильного состояния анализатора. Например, все современ- ные компиляторы поставляются вместе с парой вспомогательных функций setjmpO и longjmp (). Эти функции позволяют осуществить в программе передачу управления из одной функции в другую. Например, функция serrorO могла бы выполнять длинный переход с помощью longjmp () в безопасную точку программы за пределами анализатора. Если вы оставите код анализатора без изменений, могут выводиться сразу не- сколько сообщений о синтаксических ошибках. Конечно, в одних ситуациях это мо- жет мешать, но в других может быть очень полезным, поскольку появляется возмож- ность выявить сразу несколько ошибок. Тем не менее, в общем случае перед тем как использовать анализатор в коммерческих программах, необходимо доработать его блок синтаксического контроля. Глава 24. Синтаксический разбор и вычисление выражений 527
Глава 25 Решение задач с помощью искусственного интеллекта I
Хотя искусственный интеллект (ИИ) как область знаний в последнее время являет- ся предметом увлекательного изучения в самых разных аспектах, но в основе большинства его приложений все же лежит решение задач. В сущности, задачи эти делятся на два вида. Задачи первого вида можно решить с помощью какой-либо де- терминированной процедуры, которая гарантирует достижение успеха — другими сло- вами, путем простого вычисления. Методы решения таких задач часто легко переводят- ся в алгоритмы, выполняемые на компьютере. Однако в повседневной жизни мало найдется таких задач, решение которых сводится к простому вычислению. В действи- тельности большинство задач относится, наоборот, ко второму виду, т.е. к нечисло- вым задачам, для решения которых как раз и применяется поиск решения— метод, разработанный с помощью искусственного интеллекта. Одной из целей искусственного интеллекта является создание универсального решателя задач (general problem solver, GPS). Универсальным решателем задач называется программа, которая может находить решения любых задач, не располагая специфическими знаниями в областях, к которым относятся решаемые задачи. В этой главе рассказывается о преиму- ществах и недостатках программ, близких к универсальным решателям задач. Первоначально исследования по искусственному интеллекту в основном были нацеле- ны на разработку хороших методов поиска. Тому имелось две причины: необходимость и желание. Чаще всего препятствием для использования приемов искусственного интеллекта в решении повседневных задач является огромный объем данных, а также сложность за- дач. Для решения этих задач требуются хорошие методы поиска. Кроме того, на началь- ном этапе исследователи верили, да и сейчас верят, что при решении задач главным явля- ется поиск решения и что именно он является решающим компонентом интеллекта. Ш Представление и терминология Представьте, что вы потеряли ключи от своей машины. Известно, что они нахо- Вы стоите там, где находится входная дверь (указанная буквой X). Начиная поиск, вы проверяете гостиную. Потом проходите через холл в первую спальню, затем, опять пересе- кая холл, — во вторую спальню, возвращаетесь в холл и проходите в большую спальню. Не найдя ключи, вы возвращаетесь, снова проходя через гостиную, и находите свои ключи на кухне. Такую ситуацию легко представить в виде графа (рис. 25.1). 530 Часть IV. Алгоритмы и приложения
Тот факт, что задачи поиска можно представить в виде графа, является достаточно важным, потому что граф наглядно показывает, как работают разные приемы поиска1. (Кроме того, возможность представлять задачи в виде графов позволяет исследовате- лям искусственного интеллекта применять разные теоремы теории графов. Однако эти теоремы в книге не рассматриваются.) Помня о такой возможности, познакомь- тесь с такими определениями из теории графов: Вершина (графа) Дискретная точка Лист Вершина дерева, не имеющая дочерних вершин Область поиска Множество всех вершин Цель Разыскиваемая вершина Эвристика Процедура предпочтения при выборе вершины Цепочка, ведущая к решению (путь к Ориентированный граф с вершинами, решению) через которые пришлось пройти при поиске решения Начало спальня Рис. 25.1. Цепочка, ведущая к решению при поиске потерянных ключей В примере с потерянными ключами каждая комната в доме — это вершина, весь дом — это область поиска; целью, при достижении которой поиск завершается, явля- ется кухня, а цепочка, ведущая к решению, показана на рис. 25.1. Спальни, кухня и ванная являются листами, потому что никуда дальше не ведут. Хотя в приведенном примере эвристика не используется, но в данной главе мы рассмотрим ее чуть позже. 1 Поиск связан с методами исследования древовидных структур, с помощью которых пред- ставляется предметная область. Поэтому особое значение в методах искусственного интеллекта уделяется поиску в специальных графах — деревьях. По этой же причине и терминология в этой области испытывает сильное влияние теории деревьев. — Прим. ред. Глава 25. Решение задач с помощью искусственного интеллекта 531
Комбинаторные взрывы Сейчас вы, возможно, думаете, что поиск решения в данном случае не представля- ет особого труда — надо лишь методично искать с самого начала и до конца. В этом крайне простом случае с потерянными ключами это не такой уж и плохой метод. Но в процессе поиска решения большинства задач складывается совсем другая ситуация. Обычно компьютер используется для решения задач, в которых количество вершин в области поиска очень большое, а по мере роста области поиска растет и число воз- можных путей к цели. Проблема состоит в том, что при добавлении новой вершины в области поиска появляется больше чем один новый путь. Значит, количество потен- циальных цепочек, ведущих к решению (т.е. путей к цели), растет быстрее, чем коли- чество вершин. Например, проанализируем количество способов расположения трех объектов: А, Б и В. Вот шесть возможных перестановок: А Б В А В Б Б В А Б А В В Б А В А Б Вы можете легко убедиться, что это все перестановки множества из трех объектов А, Б и В. Однако чтобы подсчитать число перестановок, не обязательно выписывать их, достаточно вспомнить математику, точнее одну из первых теорем комбинаторики, в комбинаторике, как вы помните, изучаются конечные множества. В самом начале комбинаторики доказывается, что число перестановок множества из N элементов рав- няется Л! (N факториал). Факториал числа — это произведение всех натуральных чи- сел, расположенных между самим этим числом и 1. Например, 3! равен 3 х 2 х 1, то есть 6. Число перестановок четырехэлементного множества равно 4!, т.е. 24. Для мно- жества из пяти элементов это число равняется 120, а для множества из шести элемен- тов — уже 720. Количество перестановок 1000 элементов равно 714 632 610 197 073 759 601 418 849 977 417 474 588 249 090 878 609 675 000 261 264 810 080 821 847 728 319 101 034 352 178 114 862 967 671 616 012 187 465 832 143 974 652 682 660 176 138 558 543 799 196 058 918 823 278 094 012 476 349 347 808 126 297 308 871 348 683 151 213 092 333 186 421 889 723 355 625 200 194 545 146 674 762 179 361 745 036 786 286 933 272 807 999 612 600 301 402 387 910 429 631 666 627 727 646 496 632 889 553 428 867 838 431 392 312 025 027 341 761 244 116 811 679 646 556 602 015 888 257 223 697 562 168 909 992 642 906 117 061 690 075 781 83L 860 435 694 260 077 938 512 872 994 188 732 291 056 835 955 646 576 374 559 844 403 478 589 827 977 896 359 553 615 244 945 139 450 535 147 865 541 911 234 779 911 956 581 260 158 897 968 391 858 788 386 527 224 093 773 398 629 808 558 519 779 393 887 735 432 611 667 731 746 281 231 320 767 704 784 928 705 836 546 160 765 399 736 331 611 461 062 082 439 903 754 746 628 783 520 482 590 178 889 150 279 206 344 543 702 020 592 901 323 505 950 437 886 513 185 797 396 136 085 558 611 169 132 635 868 114 964 984 046 353 408 280 750 702 103 892 187 208 160 031 274 302 955 751 516 125 458 652 208 465 955 631 797 433 923 044 208 829 669 995 276 487 337 323 958 668 820 379 534 036 976 448 426 170 164 975 419 708 975 198 901 137 837 968 175 960 223 153 780 622 289 570 299 284 225 327 168 164 348 131 156 460 594 003 985 486 969 944 590 120 874 119 181 463 075 291 207 524 221 801 357 236 131 365 024 909 342 602 900 385 442 615 307 921 510 838 971 889 893 988 005 024 324 540 265 226 458 344 825 552 036 682 573 719 374 404 800 997 424 975 462 045 825 557 409 379 143 586 593 304 216 412 508 153 691 221 566 950 537 487 984 127 761 907 788 476 088 964 518 195 444 153 181 170 483 066 526 993 266 093 988 103 790 864 210 479 988 504 087 497 043 783 647 114 262 853 719 201 928 168 747 780 208 398 281 832 572 616 475 959 953 926 849 019 393 506 276 263 243 414 282 617 210 304 226 769 958 043 367 180 612 084 024 532 Часть IV. Алгоритмы и приложения
432 438 465 657 245 014 402 821 885 565 513 958 720 559 654 228 749 774 230 483 865 688 976 461 927 383 814 765 904 339 901 886 018 566 526 485 729 918 311 021 171 229 845 901 641 908 519 296 819 372 388 642 614 839 137 428 531 926 649 875 337 218 940 051 399 694 290 153 483 077 644 569 321 139 083 506 217 095 002 597 389 344 220 207 573 630 569 498 825 087 956 121 450 994 871 701 244 516 461 182 154 399 457 156 805 941 872 748 741 785 160 829 230 135 358 081 840 243 416 909 004 153 690 105 933 983 ООО 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 Поистине колоссально! 252 470 935 190 620 929 023 136 493 273 497 011 413 346 962 715 422 845 862 377 387 538 900 140 767 310 446 640 259 899 490 222 221 061 799 702 356 193 897 017 860 040 811 889 921 068 884 387 121 855 646 124 960 798 722 657 382 291 123 125 024 186 649 353 143 970 694 281 434 118 520 158 014 123 344 828 015 099 073 152 433 278 288 269 864 602 789 864 863 554 277 196 742 822 248 757 586 765 752 968 928 162 753 848 863 396 909 959 826 280 260 379 029 309 120 889 086 942 028 510 640 998 094 254 742 173 582 401 063 677 404 595 096 996 372 524 230 560 855 903 700 624 271 835 777 939 410 970 027 753 472 000 000 000 ООО 000 000 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 000 000 ООО 000 000 000 000 000 000 000 000 000 000 N! 0 1 2 3 4 5 Рис. 25.2. Комбинаторный взрыв, происходящий с факториалами Глава 25. Решение задач с помощью искусственного интеллекта 533
График на рис. 25.2 дает возможность получить наглядное представление о том, что исследователи искусственного интеллекта называют комбинаторным взрывом. И как только количество объектов превысит какое-то сравнительно небольшое число, рост количества комбинаторных объектов (например, путей в графе), перебираемых в процессе решения, становится поистине неудержимым; трудности могут возникнуть даже не при проверке такого огромного количества объектов, а гораздо раньше — при пересчете. Ведь каждая дополнительная вершина в области поиска увеличивает число возможных решений не на 1, а на число, значительно большее 1. Поэтому после дос- тижения некоторого критического количества объектов, добавление еще одного объ- екта к исходным данным, например, новой вершины, приводит к тому, что возмож- ных “кандидатов в решение” становится так много, что проверить их за обозримое время практически невозможно. Именно из-за того, что количество возможностей растет столь быстро, лишь в самых простых задачах можно применять такую “роскошь”, как исчерпывающий поиск. Исчерпывающим называется поиск, при кото- ром проверяются все вершины; этот вид поиска можно считать приемом “грубой си- лы”. Хотя прием “грубой силы” теоретически применим всегда, на практике он часто требует слишком много времени или слишком много компьютерных ресурсов, или того и другого вместе. Поэтому исследователи разработали другие методы поиска. И Методы поиска Для поиска решения применяется несколько методов1. Вот четыре самых главных: в глубину; в ширину1 2; восходящий3; с использованием частичного пути минимальной стоимости. О каждом из них рассказывается в этой главе. 1 Поскольку поиск решения часто выполняется путем перебора, то термины поиск и перебор в искусственном интеллекте часто рассматриваются как взаимозаменяемые. А поскольку поиск связан с методами исследования графов, с помощью которых представляется предметная об- ласть, то поиск (или перебор) выполняется с помощью обхода графов. Так как в качестве графов чаще всего выступают древовидные структуры, то в процессе перебора выполняется обход дере- ва. Поэтому едва ли стоит удивляться, что вводимые далее термины практически заимствованы из теории обхода деревьев. — Прим. ред. 2 Называемый также полным перебором или полным поиском. — Прим. ред. 3 Называемый также методом наискорейшего подъема, наискорейшего спуска и нисходящим. Доволь- но странная терминология, если в ней отождествляются противоположные понятия (наискорейший подъем = наискорейший спуск, восходящий = нисходящий), не правда ли? Все дело в том, как растут де- ревья. Если мне будет позволено так выразиться, я скажу, что согласно учебникам ботаники деревья, конечно же, в основном растут снизу вверх, т.е. корень находится внизу, а листья — вверху. В компью- терах (и учебниках по информатике) то ли гравитация не является решающим фактором при опреде- лении места расположения корней и листьев, то ли программисты (и авторы учебников по информа- тике) умеют поворачивать вектор гравитации на 180°. Как бы то ни было, но в большинстве учебни- ков по информатике корни деревьев располагаются вверху страницы, а сами деревья растут вниз (а куда же им в таком случае расти?). Поэтому при обходе таких деревьев от корня к листьям приходит- ся двигаться вниз. Если же корень расположить внизу, то дерево будет расти вверх (а куда же ему в таком случае расти?). Но в этом случае при обходе деревьев от корня к листьям приходится двигаться вверх. Сам алгоритм обхода дерева, естественно, не зависит от направления, в котором растет дерево. Вот и получается, что один и тот же алгоритм называется (разными авторами) по-разному, т.е. полу- чается, что его название зависит от направления, в котором растет дерево. Если хотите, добавьте сюда немного сомнительной философии и получите равенство вверх=вниз. — Прим. ред. 534 Часть IV. Алгоритмы и приложения
Оценка поиска Иногда бывает очень сложно оценить, насколько эффективен метод поиска. Фактически оценка методов поиска и составляет значительную часть искусствен- ного интеллекта. Впрочем, для нас наиболее важными являются два критерия: 1) насколько быстро при поиске находится решение; 2) насколько хорошим явля- ется найденное решение. Имеется несколько видов задач, в процессе решения которых особенно важным является первый из двух критериев, т.е. главным аспектом здесь является то, чтобы решение, возможно, даже любое из решений, было найдено с минимальными уси- лиями. Однако в других ситуациях решение обязательно должно быть хорошим, воз- можно, даже оптимальным. Быстрота поиска определяется как длиной пути решения, так и количеством вершин, через которые фактически приходится пройти в процессе поиска реше- ния. Следует помнить, что при возврате из тупика усилия на самом деле оказы- ваются потраченными впустую. Поэтому необходимо выработать такую стратегию поиска, благодаря которой к минимуму сводится возможность попадания в тупик. Необходимо понимать разницу между нахождением хорошего и оптимального ре- шения. Чтобы найти оптимальное решение, может потребоваться исчерпывающий поиск, так как иногда именно он является единственным способом проверки того, что было найдено наилучшее решение. А найти хорошее решение — это означает найти такое решение, которое удовлетворяет набору ограничений; при этом не важно, имеется ли решение, которое еще лучше. Как будет видно из дальнейшего изложения, методы поиска, описанные в этой главе, не во всех ситуациях работают одинаково хорошо. Поэтому трудно сказать, всегда ли какой-либо метод лучше другого. Впрочем, “в среднем” некоторые методы, будут более приемлемыми, чем остальные. Кроме того, иногда сам способ постановки задачи подсказывает подходящий метод поиска. Теперь давайте проанализируем задачу, для решения которой воспользуемся разными методами поиска. Представьте, что вы транспортный агент, а какой-то довольно придирчивый клиент хочет заказать у вас билет от Нью-Йорка до Лос- Анджелеса, причем на самолет именно компании XYZ Airlines. Вы пытаетесь объ- яснить клиенту, что у этой компании беспересадочных авиарейсов из Нью-Йорка в Лос-Анджелес нет, но он хочет лететь только на самолетах компании XYZ Air- lines. Самолеты компании XYZ Airlines по расписанию совершают следующие авиарейсы: Авиарейс Расстояние, в милях Нью-Йорк — Чикаго 1000 Чикаго — Денвер 1000 Нью-Йорк — Торонто 800 Нью-Йорк — Денвер 1900 Торонто — Калгари 1500 Торонто — Лос-Анджелес 1800 Торонто — Чикаго 500 Денвер — Урбана 1000 Денвер — Хьюстон 1500 Хьюстон — Лос-Анджелес 1500 Денвер — Лос-Анджелес 1000 Глава 25. Решение задач с помощью искусственного интеллекта 535
Вы быстро понимаете, что из Нью-Йорка в Лос-Анджелес добраться самолетами компании XYZ Airlines можно только в том случае, если заказать билеты на несколько промежуточных авиарейсов, что вы и делаете. Ваша задача состоит в том, чтобы написать несколько С-программ, которые будут выбирать маршрут лучше, чем это получается у вас. В Представление в виде графа Информацию об авиарейсах компании XYZ Airlines можно представить в виде ориентированного графа (рис. 25.3). Ориентированным графом (или орграфом) на- зывается граф, в котором каждое ребро (линия, соединяющая вершины графа) рассматривается как направленное. На рисунке это направление движения по ребру, изображаемому линией, показывает стрелка. В этом графе по ребру нельзя двигаться в направлении, противоположном тому, которое показано его стрелкой. Рис. 25.3. Ориентированный (и даже нагруженный — с весами на ребрах) граф авиарейсов компании XYZ Airlines Чтобы дать иное, более понятное представление графа авиарейсов компании XYZ Airlines, его изобразили в виде дерева (рис. 25.4). Теперь этот вариант будет использоваться нами вплоть до конца главы. Цель, т.е. конечный пункт путеше- ствия — Лос-Анджелес, — обведена кружком. Кроме того, обратите внимание, что для того чтобы граф было проще представить в виде дерева, некоторые города в нем показаны несколько раз. Теперь можно приступить к разработке разных программ поиска, с помощью ко- торых можно будет выбирать маршрут от Нью-Йорка до Лос-Анджелеса. 536 Часть IV. Алгоритмы и приложения
Рис. 25.4. Авиарейсы компании XYZ Airlines , показанные в виде дерева J Поиск в глубину При поиске в глубину каждая из цепочек, которые могут привести к решению, про- веряется до своего листа, а лишь затем начинается проверка следующей цепочки (т.е. пути). Чтобы более точно представить себе, как работает такой поиск, проанализируй- те следующее дерево. Целью является вершина F. Глава 25. Решение задач с помощью искусственного интеллекта 537
При поиске в глубину вершины графа обходятся в порядке ABDBEBACF. Если вы знакомы с деревьями, то увидите, что в этом виде поиска используется разновидность обхода неориентированного дерева. То есть путь продолжается налево до тех пор, по- ка не будет достигнут лист или не будет найдена цель. Если лист достигнут, то путь поворачивает назад, поднимается на один уровень вверх, затем продолжается направо, потом снова влево, пока не встретится цель или очередной лист. А вся эта процедура продолжается до тех пор, пока не будет найдена цель или не проверена последняя вершина области поиска. Как видите, поиск в глубину, — это такой метод поиска цели, который в наихуд- шем случае вырождается в исчерпывающий поиск. В нашем примере это случится тогда, когда целью является вершина G. В С-программе, предназначенной для выбора маршрута из Нью-Йорка в Лос- Анджелес, используется база данных с информацией об авиарейсах компании XYZ Airlines. Каждая запись этой базы содержит сведения о городе-пункте вылета и городе-пункте прибытия, о расстоянии между ними, а также флаг, который помогает при поиске с возвратом (вы скоро увидите, каким именно образом). Вся эта инфор- мация хранится в следующей структуре: #define МАХ 100 /* структура базы данных авиарейсов */ struct FL { char from[20]; char to[20]; int distance; char skip; /* используется при поиске с возвратом */ }; struct FL flight[MAX]; /* массив структур БД */ int f_pos =0; /* количество записей в БД авиарейсов */ int find_pos =0; /* индекс для поиска в БД авиарейсов */ Отдельные записи вводятся в базу данных с помощью функции assert_flight (), а всю информацию об авиарейсах инициализирует функция setup (). Индекс послед- ней записи базы данных сохраняется в глобальной переменной f_pos. Вот код нуж- ных нам функций: void setup(void) { assert_flight("Нью-Йорк", "Чикаго", 1000); assert_flight("Чикаго", "Денвер", 1000); assert_flight("Нью-Йорк", "Торонто", 800); assert_flight("Нью-Йорк", "Денвер", 1900); assert_flight("Торонто", "Калгари", 1500); assert_flight("Торонто", "Лос-Анджелес", 1800); assert_flight("Торонто", "Чикаго", 500); assert_flight("Денвер", "Урбана", 1000); assert_flight("Денвер", "Хьюстон", 1500); assert_flight("Хьюстон", "Лос-Анджелес", 1500); assert_flight("Денвер", "Лос-Анджелес", 1000); } /* Записать данные в базу. */ void assert_flight(char *from, char *to, int dist) { if(f_pos < MAX) { strcpy(flight[f_pos].from, from); 538 Часть IV. Алгоритмы и приложения
strcpy(flight[f_pos].to, to); flight[f—pos].distance = dist; flight[f_pos].skip = 0; f_pos++; } else printf (’’База данных авиарейсов переполнена. n”) ; } Чтобы следовать духу науки об искусственном интеллекте (ИИ), считайте, что в базе данных содержатся “факты”. Создаваемая программа будет с помощью этих “фактов” приближаться к решению. По этой причине многие исследователи искусст- венного интеллекта называют базы данных “базами знаний”. В данной главе эти по- нятия используются как взаимозаменяемые. Для написания кода, выполняющего поиск маршрута из Нью-Йорка в Лос- Анджелес, потребуется несколько вспомогательных функций. Во-первых, нужна под- программа для определения того, имеется ли авиарейс между двумя городами или нет. Эта функция называется match (); она возвращает расстояние между двумя городами, если такой авиарейс есть и ноль, если такого рейса нет. Вот эта функция: /* Если между двумя городами имеется авиарейс, то возвращается расстояние между ними, а в противном случае возвращается 0. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) return flight[t].distance; return 0; /* не найден */ } Другой необходимой подпрограммой является find(). Эта функция ищет в базе дан- ных какой-либо авиарейс из указанного города. Если авиарейс с каким-либо другим горо- дом найден, то возвращаются название этого города и расстояние до него от первого горо- да, в противном же случае возвращается нуль. Вот текст подпрограммы find (): /* Зная пункт отправления (параметр from), найти пункт прибытия (параметр anywhere). */ int find(char *from, char *anywhere) { find_pos = 0; while(find_pos < f_pos) { if(!strcmp(flight[find_pos].from, from) && !flight[find_pos].skip) { strcpy(anywhere, flight[find_pos].to); flight[find_pos].skip = 1; /* активизировать */ return flight[find_pos].distance; } find_pos++; } return 0; } Как видите, если поле skip (пропустить) имеет значение 1, то авиарейс между го- родами не выбирается. Кроме того, когда авиарейс найден, то его поле skip отмеча- ется как активное — таким образом реализуется поиск с возвратом из тупиков. Поиск с возвратом — это очень важная часть многих методов, используемых в систе- мах искусственного интеллекта. Он выполняется с помощью рекурсивных подпрограмм и Глава 25. Решение задач с помощью искусственного интеллекта 539
с помощью специального стека для поиска с возвратом. Почти во всех ситуациях, связан- ных с возвратом к предыдущему состоянию, используется нечто, похожее на стек, — то есть нечто, работающее по принципу “последним пришел — первым вышел”. При прохо- де по пути все встретившиеся на нем вершины помещаются в стек. При достижении лис- та, наоборот, происходит как бы остановка без возможности повторного пуска (в глубину) и из стека извлекается последняя помещенная туда вершина и исследуется новый путь, который начинается с этой вершины. Этот процесс продолжается до тех пор, пока не будет найдена цель или не будут исследованы все пути. Далее представлены функции push () и pop () (означают соответственно “поместить в стек, положить в стек, сохранить в стеке” и “выталкивать из стека, снимать со стека, вынимать из стека, считывать из стека”), которые управляют стеком, используемым для поиска с возвратом. Массив, в котором хранятся значения, заталкиваемые в стек, представлен глобальной переменной bt_stack, а указа- тель на вершину стека — глобальной переменной tos. Именно эти переменные использу- ют данные функции при обращении к стеку. /* Подпрограммы обращения к стеку */ void push(char *from, char *to, int dist) { if(tos < MAX) { strcpy(bt_stack[tos].from, from) ; strcpy(bt_stack[tos].to, to); bt_stack[tos].dist = dist; tos++; } else printf("Стек заполнен.n"); } void pop(char *from, char *to, int *dist) { if(tos > 0) { tos--; strcpy(from, bt_stack[tos] .from) ; strcpy(to, bt_stack[tos].to); *dist = bt_stack[tos].dist; } else printf("Стек пуст.Хп"); } Теперь, когда написаны необходимые вспомогательные подпрограммы, проанали- зируйте следующий код. Он определяет функцию isflightO — главную подпро- грамму для поиска маршрута из Нью-Йорка в Лос-Анджелес. /* Определить, имеется ли маршрут между из города, на который указы- вает параметр from (из) в город, на который указывает параметр to (в). ★/ void isflight(char *from, char *to) { int d, dist; char anywhere[20]; /* посмотреть, является ли городом-пунктом прибытия */ if(d=match(from, to)) { push(from, to, d); ' return; } /* проверить другой авиарейс */ if(dist=find(from, anywhere)) { 540 Часть IV. Алгоритмы и приложения
push(from, to, dist); isflight(anywhere, to); } else if(tos > 0) { /* поиск с возвратом */ pop(from, to, &dist); isflight(from, to); } } Эта подпрограмма работает следующим образом. Во-первых, функция match () проверяет базу данных — нет ли в ней авиарейса между городами, на которые указы- вают параметры from и to. Если такой авиарейс имеется, то цель поиска достигну- та — данные по авиарейсу помещаются в стек и функция возвращает управление. В противном случае функция find() проверяет, нет ли авиарейса между городом, на который указывает from, и каким-нибудь другим городом. Если есть, то соответст- вующие данные помещаются в стек и рекурсивно вызывается isflight О. В против- ном случае выполняется поиск с возвратом. Предыдущая вершина удаляется из стека, и рекурсивно вызывается isflight (). Этот процесс повторяется до тех пор, пока не будет найдена цель. Поле skip используется при поиске с возвратом, чтобы не прове- рялись повторно одни и те же авиарейсы. Поэтому если подпрограмму вызвать с такими параметрами, как “Денвер” и “Хьюстон”, то первое условие оператора if будет истинным и isflight () завершит свою работу. Предположим теперь, что isflight () вызывается с параметрами “Чикаго” и “Хьюстон”. В таком случае первое условие оператора if не будет истин- ным, так как авиарейса между этими городами нет. Поэтому второе условие оператора if проверяет наличие авиарейса из Чикаго в какой-либо другой город. В нашем при- мере из Чикаго имеется авиарейс в Денвер, поэтому isflight () рекурсивно вызыва- ется с параметрами “Денвер” и “Хьюстон”. И опять проверяется первое условие. Оно выполнено: на этот раз авиарейс найден! Наконец, все рекурсивные вызовы можно завершить, и isflight () заканчивает свою работу. Проанализируйте — используемая здесь функция isflight () ведет в базе знаний поиск в глубину. Важно понять, что isflight () на самом деле не возвращает решение— она его генерирует. При выходе из функции isflight () в стеке, используемом для поиска с возвратом, находится готовый маршрут между Чикаго и Хьюстоном, то есть собствен- но решение. В действительности успешное или неудачное выполнение isflight () определяется состоянием стека. Пустой стек указывает на неудачу; в противном же случае в стеке будет находиться решение. Поэтому для завершения всей программы нужна еще одна функция. Эта функция называется route (), именно она выводит как путь, так и общее расстояние. Рассмотрим эту функцию: /* Вывести маршрут и общее расстояние. */ void route(char *to) { int dist, t; dist = 0; t = 0; while(t < tos) { printf("%s - ”, bt_stack[t].from); dist += bt_stack[t].dist; t++; } printf(”%sn”, to); printf("Расстояние в милях равно %d.n", dist); Глава 25. Решение задач с помощью искусственного интеллекта 541
Далее следует вся программа, использующая поиск в глубину: /* Поиск в глубину. */ #include <stdio.h> #include <string.h> #define MAX 100 /* структура базы данных авиарейсов */ struct FL { char from[20]; char to[20]; int distance; char skip; /* используется при поиске с возвратом */ }; struct FL flight[MAX]; /* массив структур БД */ int f_pos =0; /* количество записей в БД авиарейсов */ int find_pos =0; /* индекс для поиска в БД авиарейсов */ int tos =0; /* вершина стека */ struct stack { char from[20]; char to[20]; int dist; } ; struct stack bt_stack[MAX]; /* стек, используемый для поиска с возвратом */ void setup(void), route(char *to); void assert_flight(char *from, char *to, int dist); void push(char *from, char *to, int dist); void pop(char *from, char *to, int *dist); void isflight(char *from, char *to); int find(char *from, char *anywhere); int match(char *from, char *to); int main(void) { char from[20], to[20]; setup(); printf("Пункт вылета: "); gets (from) ; printf("Пункт прибытия: "); gets(to); isflight(from,to); route(to); return 0; /* Инициализация базы данных авиарейсов. */ void setup(void) { assert_flight("Нью-Йорк", "Чикаго", 1000); assert_flight("Чикаго", "Денвер", 1000); assert_flight("Нью-Йорк", "Торонто", 800); 542 Часть IV. Алгоритмы и приложения
assert—flight("Нью-Йорк", "Денвер", 1900); assert_flight("Торонто", "Калгари", 1500); assert_flight("Торонто", "Лос-Анджелес", 1800); assert_flight("Торонто", "Чикаго", 500); assert__flight ("Денвер", "Урбана", 1000); assert__flight ("Денвер", "Хьюстон", 1500); assert__f light ("Хьюстон", "Лос-Анджелес", 1500); assert_flight("Денвер", "Лос-Анджелес", 1000); /* Запомнить данные в базе. */ void assert_flight(char *from, char *to, int dist) { if(f_pos < MAX) { strcpy(flight[f_pos].from, from); strcpy(flight[f_pos].to, to); flight [f__pos] .distance = dist; flight[f_pos].skip = 0; f_pos++; } else printf("База данных авиарейсов заполнена An") ; /* Показать маршрут и общее расстояние. */ void route(char *to) int dist, t; dist = 0; t = 0; while(t < tos) { printf("%s - ", bt_stack[t].from) ; dist += bt_stack[t].dist; } printf("%sn", to); printf("Расстояние в милях равно %d.n", dist); /* Если между двумя городами имеется авиарейс, то возвращается расстояние между ними, в противном случае возвращается 0. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) return flight[t].distance; return 0; /* не найден */ /* Зная пункт отправления (параметр from), найти пункт прибытия (параметр anywhere). */ int find(char *from, char *anywhere) { find_pos = 0; Глава 25. Решение задач с помощью искусственного интеллекта 543
while(find_pos < f_pos) { if (! strcmp (flight [find__pos] . from, from) && !flight[find_pos].skip) { strcpy (anywhere, flight [ find__pos ] . to) ; flight[find_pos].skip = 1; /* активизировать */ return flight[find_pos].distance; } find_pos++; } return 0; /* Определить, имеется ли маршрут между из города, на который указывает параметр from (из) в город, на который указывает параметр to (в) . */ void isflight(char *from, char *to) { int d, dist; char anywhere[20]; /* является ли пунктом прибытия */ if(d=match(from, to)) { push(from, to, d); return; } /* проверить другой авиарейс */ if(dist=find(from, anywhere)) { push(from, to, dist); isflight(anywhere, to); } else if(tos > 0) { /* поиск с возвратом */ pop(from, to, &dist); isflight(from, to); } /* Подпрограммы обращения к стеку */ void push(char *from, char *to, int dist) { if(tos < MAX) { strcpy(bt_stack[tos].from,from); strcpy(bt_stack[tos] .to, to); bt_stack[tos].dist = dist; tos++; } else printf("Стек заполнен.n"); void pop(char *from, char *to, int *dist) { if(tos > 0) { tos—; strcpy(from,bt_stack[tos].from); strcpy(to,bt_stack[tos].to); *dist = bt_stack[tos].dist; } else printf("Стек пуст.п"); 544 Часть IV. Алгоритмы и приложения
Обратите внимание, что main () подсказывает ввести пункт вылета и пункт прибы- тия. Это означает, что программу можно использовать для определения маршрутов между любыми двумя городами. Однако в оставшейся части^ главы при анализе про- граммы подразумевается, что вылет осуществляется из Нью-Йорка, а в качестве пунк- та назначения выбран Лос-Анджелес. Если выбраны Нью-Йорк и Лос-Анджелес, то получится такое решение: I Нью-Йорк - Чикаго - Денвер - Лос-Анджелес Расстояние в милях равно 3000. Цепочка, ведущая к решению и являющаяся собственно маршрутом, показана на рис. 25.5. Лос-Анджелес Рис. 25.5. Это решение (цепочка, ведущая к решению, т.е. собственно путь) было най- дено с помощью поиска в глубину На рис. 25.5 видно, что это действительно первое решение, которое можно найти с помощью поиска в глубину. Найденное нами решение не оптимально (оптимальное решение в данном случае — это маршрут Нью-Йорк — Торонто — Лос-Анджелес с расстоянием в 2600 миль), но плохим его тоже назвать нельзя. Анализ поиска в глубину Итак, с помощью поиска в глубину нам удалось найти достаточно хорошее реше- ние. Кроме того, благодаря такому поиску именно в этой задаче было с первой по- пытки найдено решение без возврата, что уже считается очень хорошим показателем. Но то, что для достижения оптимального решения при поиске в глубину приходится пройти почти все вершины, — вот это уже не совсем хороший признак. Глава 25. Решение задач с помощью искусственного интеллекта 545
Обратите внимание, что если изучается особенно длинная ветвь, на конце которой нет решения, то эффективность поиска в глубину может оказаться весьма низкой. Тогда при рассматриваемом способе поиска теряется очень много времени, причем не только на изучение этой цепи, но и на возвращение по ней, чтобы можно было дви- гаться дальше к цели. В Полный перебор, или поиск в ширину Противоположностью поиска в глубину, является поиск в ширину, или полный пере- бор. В соответствии с этим методом вначале обходятся все вершины, находящиеся на одном и том же уровне, а лишь затем выполняется переход на следующий, более низ- кий уровень. Вот как используется этот метод при поиске цели С: Чтобы программа поиска маршрута выполняла поиск в ширину, необходимо изме- нить лишь подпрограмму isflight (): void isflight(char *from, char *to) { int d, dist; char anywhere[20] ; while(dist=find(from, anywhere)) { /* модификация: поиск в ширину */ if(d=match(anywhere, to)) { push(from, to, dist); push(anywhere, to, d); return; } } /* проверить любой авиарейс */ if(dist=find(from, anywhere)) { push(from, to, dist); isf light (anywhere, to);- } else if(tos>0) { pop(from, to, &dist); isflight(from, to); } } 546 Часть IV. Алгоритмы и приложения
Как можно видеть, изменено только первое условие. Теперь проверяются все горо- да, в которые можно попасть авиарейсом из пункта вылета, но из которых нет авиа- рейса в пункт прибытия. Если этой версией isflightO заменить в программе предыдущую реализацию данной функции, то получится следующее решение: I Нью-Йорк - Торонто - Лос-Анджелес Расстояние в милях равно 2600. Это решение является оптимальным. Путь к решению, найденный с помощью по- иска в ширину, показан на рис. 25.6. Лос-Анджелес Хьюстон Урбана Лос-Анджелес Рис. 25.6. Путь к решению, найденный с помощью поиска в ширину Анализ поиска в ширину В этом примере поиск в ширину находит первое решение без возврата. Более того, оказывается, что это решение еще и оптимальное. В действительности первые три решения, которые могли бы быть найдены, как раз и являлись бы самыми лучшими маршрутами. Однако этот результат нельзя обобщить на другие случаи, потому что сгенерированный путь зависит от физической организации хранения информации в компьютере. Зато этот пример хорошо показывает радикальное отличие двух методов поиска: в глубину и в ширину. Недостатки поиска в ширину становятся очевидными, когда цель находится на глубине нескольких слоев. В таком случае для поиска цели приходится затрачивать значительные усилия. В общем, выбирая один из двух методов поиска, в глубину или в ширину, прихо- дится делать обоснованные догадки о том, где вероятнее всего находится цель. Глава 25. Решение задач с помощью искусственного интеллекта 547
И Добавление эвристики К этому времени вы, возможно, догадались, что каждый из методов поиска, как в глубину, так и в ширину, работает вслепую. Это методы поиска решения, которые по- лагаются исключительно на передвижение от одной вершины (цели) к другой без уче- та компьютером каких-либо обоснованных догадок. В некоторых ситуациях, когда объем области поиска контролируется, а также известно, что один метод лучше дру- гого, такой подход может быть приемлемым. Однако для более универсальной про- граммы искусственного интеллекта нужна процедура поиска, которая в среднем луч- ше любого из этих двух методов. Единственный способ получить такую процедуру — это добавить эвристические возможности. Эвристика — это набор простых правил, которые позволяют оценить вероятность того, что поиск ведется в нужном направлении. Например, представьте, что вы заблудились в лесу и вас мучит жажда. В этом лесу деревья растут так густо, что далеко впереди ничего не видно, а сами деревья такие большие, что взобраться на них и осмотреться вокруг нель- зя. Однако вам известно, что реки, ручьи и пруды вероятнее всего находятся в долинах; что животные часто протаптывают тропы к местам водопоя; что находясь поблизости от воды, ее можно чувствовать по “запаху”; а также вы знаете, что шум бегущей воды можно услышать. Таким образом, вы начинаете спускаться с холма вниз, ведь маловероятно, что- бы вода была на его вершине. Затем вы натыкаетесь на оленьи следы, которые также ведут вниз. Зная, что эти следы могут привести к воде, вы по ним и направляетесь. Через неко- торое время с левой стороны начинает доноситься легкий шум. Предполагая, что это мо- жет быть вода, вы осторожно идете в этом направлении. По мере движения обнаруживает- ся, что влажность воздуха усилилась; вода уже чувствуется по запаху. И, наконец, вы нахо- дите ручей и утоляете жажду. Как видите, эвристическая информация, не обязательно должна быть точной или гарантировать успех, однако она увеличивает шансы на то, что с помощью этого метода поиска вы найдете цель быстрее или найдете более оптимальное решение, а может быть, одновременно выполнятся оба эти условия. Короче говоря, при- менение эвристики увеличивает шансы скорейшего достижения успеха. Вы, возможно, думаете, что эвристическую информацию можно легко ввести в программы, предназначенные для специального применения, но общие эвристиче- ские методы поиска создать невозможно. Это не так. Чаще всего эвристические мето- ды поиска строятся на основе поиска максимума или минимума некоторого парамет- ра решаемой задачи. И действительно, два эвристических подхода, с которыми мы по- знакомимся, используют противоположные эвристики и генерируют разные результаты. В основе этих двух методов поиска лежат процедуры поиска в глубину. В Поиск методом наискорейшего подъема В задаче организации полета из Нью-Йорка в Лос-Анджелес могут быть два огра- ничивающих параметра, которые пассажиру хотелось бы свести к минимуму. Первый из них — это количество авиарейсов, необходимых, чтобы добраться до Лос- Анджелеса. А второй — это длина маршрута. Помните, что самый короткий мар- шрут— это не обязательно тот, который имеет минимальное количество авиарейсов1. В алгоритме поиска, ищущем в качестве первого решения маршрут с минимальным количеством авиарейсов1 2, применяется следующая эвристика. Чем длиннее авиарейс, 1 Действительно, пересадок может быть мало (например, всего лишь одна), но иногда лучше совершить несколько коротких перелетов, чем два длинных. — Прим. ред. 2 Т.е. маршрут с минимальным количеством пересадок. — Прим. ред. 548 Часть IV. Алгоритмы и приложения
то тем больше вероятность того, что путешественник окажется ближе к месту назна- чения. Поэтому количество авиарейсов1 сводится к минимуму. На языке искусственного интеллекта это называется наискорейшим подъемом. Алго- ритм наискорейшего подъема в качестве следующей выбирает вершину, которая, как ему кажется, ближе всего находится к цели (то есть дальше всего от текущей верши- ны). Своим названием алгоритм обязан вот чему. Представьте себе, что турист- пешеход на полпути к вершине заблудился в темноте. И хотя вокруг темно, но турист, зная, что его лагерь находится на вершине, может, следуя алгоритму наискорейшего подъема, найти свой лагерь. Просто он должен помнить, что каждый шаг наверх — это шаг в правильном направлении. Применительно к базе данных авиарейсов, в программе, генерирующей маршруты, эвристику наискорейшего подъема можно использовать следующим образом. Среди авиарейсов, исходящих из текущей вершины, выбирайте самый дальний — в надежде, что он доставит ближе всего к месту назначения. Вот как для этого нужно модифици- ровать подпрограмму f ind (): /* Зная пункт отправления (параметр from), найти пункт прибытия для самого дальнего авиарейса (параметр anywhere). */ int find(char *from, char *anywhere) { int pos, dist; pos=dist = 0; find__pos = 0; while (find_pos < f__pos) { if (! strcmp (flight [find__pos] . from, from) && ! flight [ find__pos] . skip) { if (flight [find__pos] .distance>dist) { pos = find_pos; dist = flight [find__pos] .distance; } } f ind__pos++; } if(pos) { strcpy(anywhere, flight[pos].to); flight[pos].skip = 1; return flight[pos].distance; } return 0; } Теперь подпрограмма find () проводит поиск по всей базе данных, отыскивая са- мый дальний авиарейс из пункта вылета. Вот вся программа, в которой используется алгоритм наискорейшего подъема: /* Наискорейший подъем */ #include <stdio.h> #include <string.h> #define MAX 100 /* структура базы данных авиарейсов */ struct FL { 1 Т.е. количество проходимых ребер графа. — Прим. ред. Глава 25. Решение задач с помощью искусственного интеллекта 549
char from[20]; char to[20]; int distance; char skip; /* используется поиском с возвратом */ }; struct FL flight[MAX]; /* массив структур БД */ int f_pos =0; /* количество записей в БД авиарейсов */ int find_pos =0; /* индекс для поиска в БД авиарейсов */ int tos =0; /* вершина стека */ struct stack { char from [20]; char to[20]; int dist; } ; struct stack bt_stack[MAX]; /* стек, используемый для поиска с возвратом ★/ void setup(void), route(char *to); void assert_flight(char *from, char *to, int dist); void push(char *from, char *to, int dist); void pop(char *from, char *to, int *dist); void isflight(char *from, char *to); int find(char *from, char *anywhere); int match(char *from, char *to); int main(void) { char from[20], to[20]; setup(); printf("Пункт вылета: "); gets(from); printf("Пункт прибытия: "); gets(to); isflight(from,to); route(to); return 0; } /* Инициализация базы данных авиарейсов. */ void setup(void) { assert_flight("Нью-Йорк", "Чикаго", 1000); assert_flight("Чикаго", "Денвер", 1000); assert_flight("Нью-Йорк", "Торонто", 800); assert_flight("Нью-Йорк", "Денвер", 1900); assert—flight("Торонто", "Калгари", 1500); assert—flight("Торонто", "Лос-Анджелес", 1800); assert—flight("Торонто", "Чикаго", 500); assert—flight("Денвер", "Урбана", 1000); assert—flight("Денвер", "Хьюстон", 1500); 550 Часть IV. Алгоритмы и приложения
assert_flight("Хьюстон", "Лос-Анджелес", 1500); assert_flight("Денвер", "Лос-Анджелес", 1000); } /* Записать факты в базу данных. */ void assert_flight(char *from, char *to, int dist) { if(f_pos < MAX) { strcpy(flight[f_pos].from, from); strcpy(flight[f_pos].to, to); flight[f_pos].distance = dist; flight[f_pos].skip - 0; f_pos++; } else printf("База данных авиарейсов заполнена.n"); /* Показать маршрут и общее расстояние. */ void route(char *to) { int dist, t; dist = 0; t = 0; while(t < tos) { printf("%s - ", bt_stack[t].from) ; dist += bt_stack[t].dist ; t++; } printf("%sn", to); printf("Расстояние в милях равно %d.n", dist); } /* Если между двумя городами имеется авиарейс, то возвращается расстояние между ними, а в противном случае возвращается 0. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) return flight[t].distance; return 0; /* не найден */ } /* Зная пункт отправления (параметр from) , найти пункт прибытия для самого дальнего авиарейса (параметр anywhere). */ int find(char *from, char *anywhere) { int pos, dist; pos=dist = 0; find_pos = 0; while(find_pos < f_pos) { if(!strcmp(flight[find_pos].from, from) && Глава 25. Решение задач с помощью искусственного интеллекта 551
!flight[find_pos].skip) { if(flight[find_pos].distance>dist) { pos = find_pos; dist = flight[find_pos].distance; } } find_pos++; } if(pos) { strcpy(anywhere, flight[pos].to); flight[pos].skip = 1; return flight[pos].distance; } return 0; /* Определить, имеется ли маршрут из города, на который указывает параметр from в город, на который указывает параметр to. */ void isflight(char *from, char *to) { int d, dist; char anywhere[20]; if(d=match(from, to)) { /* это цель */ push(from, to, d); return; } /* найти любой авиарейс */ if(dist=find(from, anywhere)) { push(from, to, dist); isflight(anywhere, to); } else if(tos > 0) { pop(from, to, &dist); isflight(from, to); } /* Подпрограммы обращения к стеку */ void push(char *from, char *to, int dist) { if(tos < MAX) { strcpy(bt_stack[tos].from, from); strcpy(bt_stack[tos].to, to); bt_stack[tos].dist = dist; tos++; } else printf (’’Стек заполнен. n”) ; void pop(char *from, char *to, int *dist) { if(tos > 0) { tos—; strcpy(from, bt_stack[tos].from); strcpy(to, bt_stack[tos].to); 552 Часть IV. Алгоритмы и приложения
*dist = bt_stack[tos].dist; I } I else printf("Стек пуст.п"); I } В результате выполнения программы получается такое решение: I Нью-Йорк - Денвер - Лос-Анджелес Расстояние в милях равно 2900. Оно довольно-таки хорошее! В полученном маршруте количество пересадок мини- мально (только одна), и он достаточно близок к самому короткому маршруту. Более того, программа приближается к решению, не тратя времени и усилий на обширные возвраты. Однако если бы отсутствовал авиарейс между Денвером и Лос-Анджелесом, то ре- шение не было бы таким хорошим. Это был бы маршрут Нью-Йорк — Денвер — Хьюстон — Лос-Анджелес общей протяженностью 4900 миль! При получении реше- ния пришлось бы делать восхождение на ложный максимум. Как можно легко уви- деть, перелет в Хьюстон не приблизит нас к цели, то есть к Лос-Анджелесу. На рис. 25.7 показано как первое решение, так и путь к ложному максимуму. Лос-Анджелес Хьюстон Урбана Ложный максимум Рис. 25.7. Пути к решению и на ложный максимум, найденные с помощью наискорей- шего подъема Глава 25. Решение задач с помощью искусственного интеллекта 553
Анализ наискорейшего подъема Наискорейший подъем дает во многих случаях достаточно хорошие решения, по- тому что обычно перед тем как решение будет найдено, надо посещать сравнительно мало вершин. Однако у этого метода могут быть такие три недостатка. Во-первых, имеется проблема ложных максимумов, которой мы просто по счастливой случайно- сти смогли избежать во втором решении нашего примера. Именно для того чтобы из- бежать ложных максимумов, при поиске решения приходится широко использовать возвраты. Вторая проблема связана с “плато” — ситуациями, когда все следующие шаги выглядят одинаково хорошими (или плохими). В таком случае наискорейший подъем будет не лучше, чем поиск в глубину. И последней проблемой является “горный хребет”. В таком случае наискорейший подъем проходит плохо, так как алго- ритм при возвратах заставляет несколько раз пересекать “горный хребет”. Но несмотря на возможные проблемы, наискорейший подъем, как правило, быстрее любого неэвристического метода приводит к решению, которое близко к оптимальному. Я Поиск с использованием частичного пути минимальной стоимости Противоположностью наискорейшему подъему является поиск с использованием частичного пути минимальной стоимости. Эта стратегия похожа на то, как если бы вы стояли на середине улицы, ведущей на большую гору, а на ногах у вас были бы наде- ты роликовые коньки. Вы бы тогда явно почувствовали, что двигаться вниз намного легче, чем вверх! Другими словами, поиск с использованием частичного пути мини- мальной стоимости выбирает путь наименьшего сопротивления. Если поиск с использованием частичного пути минимальной стоимости применять к задаче выбора маршрутов полетов, то это означает, что авиарейсы всегда выбирают- ся самые короткие — в надеже, что и найденный маршрут окажется самым коротким. В отличие от наискорейшего подъема, который стремится уменьшить количество авиарейсов, поиск с использованием частичного пути минимальной стоимости сводит к минимуму общую длину маршрута. Чтобы использовать поиск с использованием частичного пути минимальной стои- мости, сначала, как обычно, нужно переписать функцию find (). Ниже показан ее новый код. /* Найти самый близкий город и поместить его в ’’anywhere". */ int find(char *from, char *anywhere) { int pos, dist; pos = 0; dist = 32000; /* больше длины самого длинного авиарейса */ find_pos = 0; while(find_pos < f_pos) { if (! strcmp (flight [ find_pos ]. from, from) && !flight[find_pos].skip) { if(flight[find_pos].distance<dist) { pos = find_pos; dist = flight[find_pos].distance; } } find_pos++; 554 Часть IV. Алгоритмы и приложения
} if(pos) { strcpy(anywhere, flight[pos].to); flight[pos].skip = 1; return flight[pos].distance; } return 0; } С помощью этой версии find () получается такое решение: Нью-Йорк - Торонто - Лос-Анджелес Расстояние в милях равно 2600. Как видите, в данном случае этот метод поиска позволяет найти самый короткий маршрут. Цепочка, ведущая к цели (т.е. маршрут, причем самый короткий), показана на рис. 25.8. Лос-Анджелес Рис. 25.8. Эта цепочка, ведущая к решению (т.е. путь, притом наикратчайший), была найдена методом поиска с использованием частичного пути минимальной стоимости Анализ поиска с использованием частичного пути минимальной стоимости Поиск с использованием частичного пути минимальной стоимости и наискорей- ший подъем имеют одни и те же достоинства и недостатки, но только с точностью до наоборот: то, что является достоинством одного метода, является недостатком другого, Глава 25. Решение задач с помощью искусственного интеллекта 555
и наоборот. При поиске с использованием частичного пути минимальной стоимости могут появиться ложные, обманчивые “овраги, долины, низины и пропасти”, но в целом этот метод работает достаточно хорошо. Однако не надо думать, что если поиск с использованием частичного пути минимальной стоимости работал в нашей задаче лучше, чем наискорейший подъем, то он вообще работает лучше1. Т.е. можно сказать, что в среднем он работает лучше, чем поиск вслепую. Н Выбор метода поиска Как вы видели, обычно эвристические методы работают в среднем лучше, чем ме- тоды поиска вслепую. Однако не всегда представляется возможным использовать эв- ристический поиск. Так происходит потому, что иногда недостаточно информации для определения вероятности того, что следующая вершина ведет к цели. Поэтому правила, определяющие выбор метода поиска, делятся на две категории: одни приме- няются для задач, в которых можно использовать эвристический поиск, другие — для задач, в которых эвристический поиск применить нельзя1 2. Если для задачи не удается найти достаточно эффективную эвристику, то лучшим методом обычно является поиск в глубину. Единственным исключением может быть тот случай, когда вам известно нечто такое, что говорит в пользу поиска в ширину. Выбор между наискорейшим подъемом и поиском с использованием частичного пути минимальной стоимости на самом деле состоит в том, что вы решаете, какой именно параметр требуется минимизировать или максимизировать. Вообще-то, в среднем наискорейший подъем генерирует решение, имеющее наименьшее количест- во вершин, а поиск с использованием частичного пути минимальной стоимости нахо- дит путь наименьшей длины. Допустим, вы ищете решение, близкое к оптимальному, однако по каким-либо причинам нельзя использовать исчерпывающий поиск. Тогда можно воспользоваться следующим эффективным методом: применить все четыре метода поиска, а затем вы- брать наилучшее решение. В сущности, все методы поиска работают по-разному, по- этому у одного из них результат должен все-таки быть лучше, чем у остальных. В Поиск нескольких решений Иногда бывает полезно найти несколько решений одной задачи. Но это не то же самое, что полный перебор, при котором необходимо найти все решения. Например, подумайте о проектировании дома вашей мечты. Вам, чтобы решить, какой проект будет наилучшим, может потребоваться несколько разных набросков поэтажного пла- на дома, но наброски всех возможных домов вам не нужны. В сущности, разные реше- ния помогают увидеть много разных подходов к поиску окончательного решения, а затем реализовать один из них. Генерировать разные решения можно несколькими способами, но здесь рассказы- вается только о двух из них. Первый — это удаление путей, а второй — удаление вер- шин. Как и следует из названий этих методов, чтобы при генерации разных решений 1 Действительно, зная условие задачи, в которой поиск с использованием частичного пути минимальной стоимости работает лучше, можно сформулировать двойственную задачу, в кото- рой будет лучше работать наискорейший подъем. — Прим. ред. 2 Обратите внимание, это утверждение означает, что общие эвристики могут оказаться не- эффективными. Более того, для каждой, пусть даже самой “интеллектуальной” эвристики, най- дется (непустой) класс задач, для которых “алгоритм Британского музея” (полный перебор без каких-либо правил предпочтения) будет более эффективным. — Прим. ред. 556 Часть IV. Алгоритмы и приложения
те из них, которые уже найдены не повторялись, некоторые элементы требуется уда- лять из системы. Помните, что ни один из этих методов не является попыткой найти все решения (и даже не может для этого использоваться). Поиск всех решений — это совсем другая задача, за выполнение которой обычно даже не берутся, потому что она подразумевает исчерпывающий поиск. Удаление путей При использовании метода генерации нескольких решений, который называется методом удаления путей, из базы данных удаляются все вершины текущего решения, а затем делается попытка найти следующее решение. В сущности, при удалении путей подрезаются ветви дерева. Чтобы найти несколько решений с помощью удаления путей, необходимо в про- грамме поиска в глубину изменить функцию main (): int main(void) { char from[20]f to[20]; setup(); printf("Пункт вылета: "); gets(from); printf("Пункт прибытия: "); gets (to); do { isflight(from, to); route(to); tos =0; /* сброс стека, используемого при поиске с возвратом */ } while(getchar() != ’q’); /* для выхода вводится символ ’q’ */ return 0; } У каждого авиарейса, входящего в цепочку, ведущую к решению, будет помечено его поле skip. Следовательно, такие авиарейсы больше не будут найдены функцией find(), все авиарейсы, имеющиеся в решении, будут эффективно удалены. Перед поиском следующего решения нужно только сбрасывать переменную tos, ведь имен- но это на самом деле и очищает стек, используемый при поиске с возвратом. Метод удаления путей обнаруживает следующие решения: Нью-Йорк - Чикаго - Денвер - Лос-Анджелес Расстояние в милях равно 3000. Нью-Йорк - Торонто - Лос-Анджелес Расстояние в милях равно 2600. Нью-Йорк - Денвер - Лос-Анджелес Расстояние в милях равно 2900. При поиске было найдено три наилучших решения. Однако этот результат нельзя обобщать, так как он зависит от размещения информации в базе данных и от кон- кретной ситуации. Удаление вершин Для генерации нескольких решений также применяется метод удаления вершин. Используя этот метод, из пути, представляющего собой найденное решение, удаляется последняя вершина, а затем делается повторная попытка найти решение. Для этого Глава 25. Решение задач с помощью искусственного интеллекта 557
функция main () с помощью другой функции retract () должна выталкивать из сте- ка, используемого для поиска с возвратом, последнюю вершину и удалять ее из базы данных. Кроме того, все поля skip должны сбрасываться с помощью функции clearmarkers (). Необходимо также очищать стек, используемый для поиска с воз- вратом. Ниже приведены коды функций main (), clearmarkers () H retract (): int main(void) { char from[20], to[20], cl[20], c2[20]; int d; setup(); printf("Пункт вылета: "); gets(from); printf("Пункт прибытия: "); gets(to); do { isflight(from,to); route(to); clearmarkers(); /* переустановка базы данных */ if(tos > 0) pop(cl,c2,&d) ; retract(cl,c2); /* удаление последней вершины из базы данных */ tos =0; /* сброс стека, используемого для поиска с возвратом */ } while(getchar() != ’q’); /* для выхода вводится символ ’q’ */ return 0; } /* Сбросить поле skip, т.е. заново активизировать все вершины. */ void clearmarkers() { int t; for(t=0; t < f_pos; ++t) flight[t].skip = 0; } /* Удаление записи из базы данных. */ void retract(char *from, char *to) { int t; for(t=0; t < f_pos; t++) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) { strcpy(flight[t].from,""); return; } } Как видите, запись удаляется таким образом: имя города заменяется строкой нуле- вой длины. Ниже приведен полный текст программы, в которой используется метод удаления вершин: I/* Поиск нескольких решений методом поиска в глубину; некоторые вершины удаляются */ #include <stdio.h> #include <string.h> 558 Часть IV. Алгоритмы и приложения
#define MAX 100 /* структура базы данных авиарейсов */ struct FL { char from[20]; char to[20]; int distance; char skip; /* используется для поиска с возвратом */ In- struct FL flight[MAX]; int f_pos =0; /* количество записей в БД авиарейсов */ int find__pos = 0; /* индекс для поиска в БД авиарейсов */ int tos =0; /* вершина стека */ struct stack { char from [20]; char to [20]; int dist; I ; struct stack bt_stack[MAX]; /* стек, используемый для поиска с возвратом */ void retract(char *from, char *to); void clearmarkers(void); void setup(void), route(char *to); void assert_flight(char *from, char *to, int dist); void push(char *from, char *to, int dist); void pop(char *from, char *to, int *dist); void isflight(char *from, char *to); int find(char *from, char *anywhere); int match(char *from, char *to); int main(void) { char from[20],to[20], cl[20], c2[20]; int d; setup(); printf("Пункт вылета: ") ; gets(from); printf("Пункт прибытия: "); gets(to); do { isflight(from,to); route(to); clearmarkers(); /* возврат базы данных в исходное состояние */ if(tos > 0) pop(cl,с2,&d) ; retract(cl,с2); /* удаление последней вершины из базы данных */ tos =0; /* сброс стека, используемого для поиска с возвратом */ } while(getchar() != 'q’); /* для выхода вводится символ ’q’ */ return 0; Глава 25. Решение задач с помощью искусственного интеллекта 559
/★ Инициализация базы данных авиарейсов. */ void setup(void) { assert_flight("Нью-Йорк", "Чикаго", 1000); assert_flight("Чикаго", "Денвер", 1000); assert_flight("Нью-Йорк", "Торонто", 800); assert_flight("Нью-Йорк", "Денвер", 1900); assert_flight("Торонто", "Калгари", 1500); assert_flight("Торонто", "Лос-Анджелес", 1800); assert_flight("Торонто", "Чикаго", 500); assert_flight("Денвер", "Урбана", 1000); assert_flight("Денвер", "Хьюстон", 1500); assert_flight("Хьюстон", "Лос-Анджелес", 1500); assert_flight("Денвер", "Лос-Анджелес", 1000); } /* Запомнить авиарейсы в базе данных. */ void assert_flight(char *from, char *to, int dist) { if(f_pos < MAX) { strcpy(flight[f_pos].from, from); strcpy(flight[f_pos].to, to); flight[f_pos].distance = dist; flight[f_pos].skip = 0; f_pos++; } else printf("База данных авиарейсов заполнена.n"); } /* Сбросить поле skip, то есть заново активизировать все вершины. */ void clearmarkers() { int t; for(t=0; t < f_pos; ++t) flight[t].skip = 0; } /* Удаление записи из базы данных. */ void retract(char *from, char *to) { int t; for(t=0; t < f_pos; t++) if (!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) { strcpy(flight[t].from,""); return; } /* Показать маршрут и общее расстояние. */ void route(char *to) { int dist, t; dist = 0; t = 0; while(t < tos) { 560 Часть IV. Алгоритмы и приложения
printf("%s - ", bt_stack[t].from) ; dist += bt_stack[t].dist; t++; } printf("%sn",to); printf("Расстояние в милях равно %d.n", dist); } /* Зная пункт отправления (параметр from), найти пункт прибытия (параметр anywhere). */ int find(char *from, char *anywhere) { find_pos = 0; while(find_pos < f_pos) { if(!strcmp(flight[find_pos].from, from) && !flight[find_pos].skip) { strcpy(anywhere, flight[find_pos].to); flight[find_pos].skip = 1; return flight [find__pos] .distance; } find_pos++; } return 0; } /* Если между двумя городами (параметры from и to) имеется авиарейс, то возвращается расстояние между ними, а в противном случае возвращается 0. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) return flight[t].distance; return 0; /* не найден */ } /* Определить, имеется ли маршрут между двумя городами, на которые указывают параметры from (из) и to (в). */ void isflight(char *from, char *to) { int d, dist; char anywhere[20]; if(d=match(from, to)) { push(from, to, d); /* расстояние */ return; } if(dist=find(from, anywhere)) { push(from, to, dist); isflight(anywhere, to); } else if(tos > 0) { pop(from, to, &dist); isflight(from, to); Глава 25. Решение задач с помощью искусственного интеллекта 561
} } /* Подпрограммы обращения к стеку */ void push(char *from, char *tor int dist) { if(tos < MAX) { strcpy(bt_stack[tos].from, from); strcpy(bt_stack[tos].to, to); bt_stack[tos].dist = dist; tos++; } else printf("Стек заполнен.n"); } void pop(char *from, char *to, int *dist) { if(tos >0) { tos--; strcpy(from, bt_stack[tos].from); strcpy(to, bt_stack[tos].to); *dist = bt_stack[tos].dist; } else printf("Стек пуст.п"); } С помощью этого метода получаются такие решения: Нью-Йорк - Чикаго - Денвер - Лос-Анджелес Расстояние в милях равно 3000. Нью-Йорк - Чикаго - Денвер - Хьюстон - Лос-Анджелес Расстояние в милях равно 5000. Нью-Йорк - Торонто - Лос-Анджелес Расстояние в милях равно 2600. В этом случае второе решение — это самый худший из возможных маршрутов, но оптимальное решение все равно найдено. Однако помните, что эти результаты нельзя обобщать: они зависят как от физической организации базы данных, так и от кон- кретной ситуации. И Поиск “оптимального” решения Все предыдущие методы поиска от начала и до конца предназначены для нахожде- ния решения, притом любого из них. Кроме того, те из приведенных методов, кото- рые являются эвристическими, показали, что при определенных стараниях решение можно до некоторой степени улучшить. Но не было сделано ни одной попытки убе- диться, что получено именно оптимальное решение. Но иногда бывает так, что подхо- дит только оптимальное решение. Впрочем, не забывайте, что “оптимальное реше- ние” в данном смысле просто означает наилучший маршрут, который можно найти с помощью одного из разнообразных методов, генерирующих варианты решений. И этот маршрут в действительности может и не быть наилучшим решением. (Ведь для получения действительно оптимального решения может понадобиться полный пере- бор, на который требуется недопустимо много времени.) Перед тем как закончить изучение хорошо проработанного примера с маршрутами, проанализируйте программу, которая находит оптимальный маршрут при следующем ограничении: общее расстояние должно быть минимальным. Эта программа с помо- 562 Часть IV. Алгоритмы и приложения
щью удаления путей генерирует разные решения, а с помощью поиска с использова- нием частичного пути минимальной стоимости получает путь с минимальным общим расстоянием. При нахождении самого короткого пути применяют такой принцип: из двух решений, старого и нового, сохраняется лишь то, которое короче. А когда новые решения больше не генерируются, то остается только оптимальное. Чтобы реализовать этот алгоритм, необходимо серьезно изменить функцию route () и создать еще один стек. В новом стеке будет храниться текущее решение, а в конце работы — оптимальное. Этот стек называется solution (решение), а вот и сама измененная функция route (): /* Найти кратчайшее расстояние. */ int route(void) { int dist, t; static int old_dist=32000; if(Itos) return 0; /* все сделано */ t = 0; dist = 0; while(t < tos) { dist += bt_stack[t].dist; } /* Если короче, то найти новое решение */ if(dist<old dist && dist) { t = 0; old_dist = dist; stos =0; /* удалить старый маршрут из стека solution */ while(t < tos) { spush(bt_stack[t].from, bt_stack[t].to, bt_stack[t].dist); } } return dist; } Далее приводится вся программа. Обратите внимание на изменения в функции main () и на то, что в программу введена функция spush О, которая помещает вер- шины нового решения в стек решений (solution). /* Поиск оптимального решения методом поиска частичного пути минимальной стоимости; некоторые маршруты удаляются. */ #include <stdio.h> #include <string.h> #define MAX 100 /* структура базы данных авиарейсов */ struct FL { char from[20]; char to[20]; int distance; char skip; /* используется при поиске с возвратом */ }; Глава 25. Решение задач с помощью искусственного интеллекта 563
struct FL flight[MAX]; /* массив структур БД */ int f_pos =0; /* количество записей в БД авиарейсов */ int find—pos =0; /* индекс для поиска в БД авиарейсов */ int tos =0; /* вершина стека */ int stos = 0; /* вершина стека solution */ struct stack { char from[20]; char to[20]; int dist; } ; struct stack bt_stack[MAX]; /* стек, используемый для поиска с возвратом */ struct stack solution[MAX]; /* хранит временные решения */ void setup(void); int route(void); void assert—flight(char *from, char *to, int dist); void push(char *from, char *to, int dist); void pop(char *from, char *to, int Mist); void isflight(char *from, char *to); void spush(char *from, char *to, int dist); int find(char *from, char *anywhere); int match(char *from, char *to) ; int main(void) { char from[20], to[20]; int t, d; setup(); printf("Пункт вылета: "); gets (from) ; printf("Пункт прибытия: "); gets(to); do { isflight(from, to); d = route(); tos =0; /* возврат в исходное состояние стека, используемого для поиска с возвратом */ } while(d != 0); /* пока алгоритм может найти новые решения */ t = 0; printf("Оптимальное решение:п"); while(t < stos) { printf("%s - ", solution[t].from); d += solution[t].dist; t++; } printf("%sn", to); printf("Расстояние й милях равно %d.n", d); return 0; } 564 Часть IV. Алгоритмы и приложения
/★ Инициализация базы данных авиарейсов. */ void setup(void) { assert_flight("Нью-Йорк", "Чикаго", 1000); assert__flight ("Чикаго", "Денвер", 1000); assert__flight ("Нью-Йорк", "Торонто", 800); assert__flight ("Нью-Йорк", "Денвер", 1900); assert__flight ("Торонто", "Калгари", 1500); assert__flight ("Торонто", "Лос-Анджелес", 1800); assert__flight ("Торонто", "Чикаго", 500); assert__f light ("Денвер", "Урбана", 1000); assert_flight("Денвер", "Хьюстон", 1500); assert__flight ("Хьюстон", "Лос-Анджелес", 1500); assert__f light ("Денвер", "Лос-Анджелес", 1000); } /* Записать факты в базу данных. */ void assert__flight (char *from, char *to, int dist) { if(f_pos < MAX) { strcpy (flight [f__pos] . from, from); strcpy (flight [f__pos] . to, to); flight[f—pos].distance = dist; flight[f_pos].skip = 0; f__pos++; } else printf("База данных авиарейсов заполнена.n"); } /* Найти кратчайшее расстояние. */ int route(void) { int dist, t; static int old_dist=32000; if(Itos) return 0; /* все сделано */ t = 0; dist = 0; while(t < tos) { dist += bt_stack[t].dist; } /* Если короче, то заменить новым решением */ if (dist<old__dist && dist) { t = 0; old_dist = dist; stos =0; /* удалить старый маршрут из стека solution */ while(t < tos) { spush (bt__stack [t] . from, bt__stack [t] . to, bt__stack [t] . dist) ; } } return dist; /* Если между двумя городами (параметры from и to) имеется авиарейс, Глава 25. Решение задач с помощью искусственного интеллекта 565
то возвращается расстояние между ними, в противном случае возвращается 0. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(flight[t].from, from) && !strcmp(flight[t].to, to)) return flight[t].distance; return 0; /* не найден */ } /* Зная пункт отправления (параметр from), найти пункт прибытия (параметр anywhere). */ int find(char *from, char *anywhere) { find_pos = 0; while(find_pos < f_pos) { if(!strcmp(flight[find_pos].from, from) && !flight[find_pos].skip) { strcpy(anywhere, flight[find_pos].to); flight[find_pos].skip = 1; return flight[find_pos].distance; } find_pos++; } return 0; } /* Определить, имеется ли маршрут между двумя городами, на которые указывают параметры from (из) и to (в). */ void isflight(char *from, char *to) { int d, dist; char anywhere[20]; if(d=match(from, to)) { push(from, to, d); /* расстояние */ return; } if(dist=find(from, anywhere)) { push(from, to, dist); isflight(anywhere, to); } else if(tos > 0) { pop(from, to, &dist); isflight(from, to); } } /* Подпрограммы обращения к стеку */ void push(char *from, char *to, int dist) { if(tos < MAX) { strcpy(bt_stack[tos].from, from); 566 Часть IV. Алгоритмы и приложения
strcpy(bt_stack[tos].to, to); bt_stack[tos].dist = dist; tos++; } else printf (’’Стек заполнен.n"); } void pop(char *from, char *to, int *dist) { if(tos > 0) { tos —; strcpy(from, bt__stack [tos ]. from) ; strcpy(to, bt__stack [tos] . to) ; *dist = bt__stack [tos] . dist; } else printf (’’Стек пуст.п”); } /* Стек решений (solution) */ void spush(char *from, char *to, int dist) { if(stos < MAX) { strcpy(solution[stos].from, from); strcpy(solution[stos].to, to); solution[stos].dist = dist; stos++; } else printf("Стек для кратчайших маршрутов заполнен.n"); } В последнем методе есть один недостаток: обход по всем маршрутам продолжается вплоть до их листьев. Будь этот метод более совершенным, обход по маршруту немед- ленно прекращался бы тогда, когда длина найденной части маршрута достигала бы текущего минимума (или превосходила его). С учетом этого обстоятельства данную программу можно значительно улучшить. В И снова возвращаемся к поиску потерянных ключей Возможно, вы помните, что вам все-таки удалось найти ключи от машины. (Если, конечно, не потеряли их снова.) Однако главу, посвященную решению задач, хоте- лось бы завершить демонстрацией программы, которая находит потерянные ключи от машины. Ведь подобные задачи встречаются так часто! В коде программы использу- ются те же методы, что и при поиске маршрута из одного города в другой. Теперь, когда вы уже освоили технику применения языка С для решения задач, программа представлена без последующих подробных объяснений. /* Найти ключи с помощью поиска в глубину. */ #include <stdio.h> #include <string.h> #define MAX 100 /* структура базы данных keys (ключи) */ struct FL { Глава 25. Решение задач с помощью искусственного интеллекта 567
char from[20]; char to[20]; char skip; struct FL keys[MAX]; /* массив структур БД */ int f_pos =0; /* количество комнат в доме */ int find_pos =0; /* индекс для поиска в БД keys */ int tos =0; /* вершина стека */ struct stack { char from[20]; char to[20]; } ; struct stack bt_stack[MAX]; /* стек, используемый для поиска с возвратом */ void setup(void), route(void); void assert_keys(char *from, char *to); void push(char *from, char *to); void pop(char *from, char *to); void iskeys(char *from, char *to); int find(char *from, char *anywhere); int match(char *from, char *to); int main(void) { char from[20] = "входная_дверь”; char to[20] = "ключи”; setup(); iskeys(from, to); route (); return 0; /* Инициализация базы данных. */ void setup(void) { assert_keys("входная_дверь”, "гостиная”); assert_keys (’’гостиная”, ’’ванная”) ; assert_keys (’’гостиная”, ’’холл”) ; assert_keys (’’холл”, ”спальня1”) ; assert_keys (’’холл", ”спальня2”) ; assert_keys (’’холл”, ”большая_спальня”) ; assert_keys (’’гостиная”, ’’кухня") ; assert_keys("кухня", "ключи"); } /* Запомнить факты в базе данных. */ void assert_keys(char *from, char *to) { if(f_pos < MAX) { strcpy(keys[f_pos].from, from); strcpy(keys[f_pos].to, to); keys[f_pos].skip = 0; 568 Часть IV. Алгоритмы и приложения
f_pos++; } else printf("База данных keys заполнена.n"); } /* Показать путь к ключам. */ void route(void) { int t; t = 0; while(t < tos) { printf("%s", bt_stack[t].from); t++; if(t < tos) printf(" - "); } printf("n") ; } /* Посмотреть, есть ли ребро в графе. */ int match(char *from, char *to) { register int t; for(t=f_pos-l; t > -1; t—) if(!strcmp(keys[t].from, from) && !strcmp(keys[t].to, to)) return 1; return 0; /* не найдено */ } /* Зная откуда (from), попасть куда-будь (anywhere). */ int find(char *from, char *anywhere) { find_pos = 0; while(find_pos < f_pos) { if(!strcmp(keys[find_pos].from, from) && ! keys [ find__pos ]. skip) { strcpy(anywhere, keys[find_pos].to) ; keys[find_pos].skip = 1; return 1; } find_pos++; } return 0; } /* Определить, имеется ли путь из from (из) в to (в) . */ void iskeys(char *from, char *to) { char anywhere[20]; if(match(from, to)) { push(from, to); /* расстояние?/ return; } Глава 25. Решение задач с помощью искусственного интеллекта 569
if(find(from, anywhere)) { push(from, to); iskeys(anywhere, to); } else if(tos > 0) { pop (from, to); iskeys(from, to); } /* Подпрограммы обращения к стеку */ void push(char *from, char *to) { if(tos < MAX) { strcpy(bt_stack[tos].from, from) ; strcpy(bt_stack[tos].to, to); tos++; } else printf("Стек заполнен.n"); void pop(char *from, char *to) { if(tos > 0) { tos — ; strcpy(from, bt_stack[tos].from); strcpy(to, bt_stack[tos].to); } else printf("Стек пуст.п"); 570 Часть IV. Алгоритмы и приложения
Полный справочник по ТТ Л 7 Часть V Разработка программ с помощью С В этой части книги исследуются различные аспекты процесса разработки программ, обусловленные средой программирования на С. В главе 26 демонстрируются приемы использования С при создании скелета прило- жения, предназначенного для работы в среде Windows 2000. Глава 27 освещает процесс проектирования при- кладных программ с помощью С. Глава 28 посвящена вопросам переноса, эффективности и отладки.
Полный справочник по Глава 26 Создание скелета приложения для Windows 2000
Язык С как один из основных языков программирования имеет первостепенное значение при создании программ для платформы Windows. По существу, вполне логично было бы просто поместить в этой части книги примеры программирования для Windows. Однако Windows является очень большой и сложной средой программи- рования, поэтому, к сожалению, невозможно описать в одной главе все детали и тон- кости, которые необходимо учитывать при написании Windows-приложений. Но с другой стороны, вполне возможно представить базовые элементы, которые являются общими для всех приложений. Затем эти элементы могут быть объединены в мини- мальный “скелет” (заготовку) прикладной Windows-программы, который сформирует фундамент для ваших собственных Windows-приложений. С момента своего появления операционная система Windows претерпела несколько превращений. В период подготовки данной книги текущей версией была, например, Windows 2000. Поэтому материалы данной главы максимально адаптированы именно для этой версии Windows. Тем не менее, даже если вы работаете с более свежей или, наоборот, со старой версией Windows, большая часть обсуждаемых вопросов останутся по-прежнему актуальными. На заметку Данная глава является адаптацией материала, почерпнутого из моей книги Win- dows 2000 Programming from the Ground up — Berkeley, CA: Osbome/McGraw-Hill, 2000. Для тех, кто захочет глубже изучить секреты программирования для Windows 2000, эта книга окажется исключительно полезным руководством. И Общая картина специфики программирования для Windows 2000 Краеугольным камнем операционной системы Windows 2000 (как впрочем и любой из версий Windows в целом) является идея предоставления любому человеку, имею- щему базовые навыки работы с данной системой, возможность сесть за компьютер и запустить практически любое приложение без специального предварительного обуче- ния. Чтобы реализовать этот замысел, Windows предоставляет пользователю единооб- разный интерфейс. Теоретически, если вы можете работать в какой-либо одной Win- dows-ориентированной программе, то вы сможете работать и во всех остальных. Без сомнения, в реальной жизни для максимально эффективного использования возмож- ностей наиболее полезных программ по-прежнему требуется определенная подготов- ка. Но, по крайней мере, это обучение может быть сведено до разъяснения того, что делает данная программа, а не того, как пользователь должен взаимодействовать с ней. Поэтому неудивительно, что значительная часть кода в Windows-приложениях предназначена для реализации интерфейса пользователя. Необходимо иметь в виду (и это очень важный момент), что не всякая программа, выполняемая под управлением Windows 2000, автоматически предоставляет пользова- телю интерфейс, выполненный в Windows-стиле. Операционная система Windows оп- ределяет среду, которая только поддерживает и стимулирует единообразие, но не за- ставляет делать это в “принудительном порядке”. Например, можно написать массу Windows-программ, которые не будут использовать преимущества, которые обеспечи- вают стандартные элементы интерфейса Windows. (Кстати, очень многие системные и сетевые утилиты Windows выполнены именно в виде приложений командной строки, а не в стиле Windows. Но это допустимо, так как они предназначены для высококва- лифицированных специалистов, например, системных администраторов.) Для того чтобы создать программу в стиле Windows, необходимо осознанно следовать опреде- ленным правилам. Только те программы, которые написаны с учетом использования 574 Часть V. Разработка программ с помощью С
богатых возможностей Windows, будут выглядеть и вести себя как настоящие Windows-приложения. Конечно, вы можете отвергнуть базовую философию Windows- дизайна, но для такого решения у вас должны быть очень веские причины. Потому что иначе ваши программы будут нарушать самую фундаментальную заповедь Win- dows: обеспечить единообразный и последовательный интерфейс пользователя. В об- щем случае, если вы пишите прикладные программы под Windows 2000, то они обяза- ны соответствовать нормам и подходам проектирования стандартного Windows-стиля. Давайте рассмотрим самые значительные составляющие, которые определяют сре- ду приложений для Windows 2000. Модель рабочего стола Основная идея (с некоторыми нюансами) основанного на окнах пользовательского интерфейса состоит в том, чтобы обеспечить на экране монитора эквивалент поверхно- сти рабочего стола. Так, на обычном письменном столе могут лежать стопкой несколько листков бумаги, причем часто некоторые листы торчат из стопки в разные стороны, так что под верхним листом видны фрагменты этих страниц. Эквивалентом рабочего стола в Windows 2000 является экран монитора. Эквивалентами листов бумаги или их фрагмен- тов являются окна на экране. На обычном рабочем столе эти листы бумаги вы можете передвигать с места на место. Возможно, даже ухитряетесь выдернуть из стопки необхо- димый документ и положить его поверх всех остальных, или изучаете кусочек другого листа, выглядывающего из пачки документов. Аналогичные операции позволяет выпол- нять в своих окнах и Windows 2000. Благодаря возможности выбрать какое-то окно, вы можете сделать его активным. Это означает, что оно будет помещено поверх всех ос- тальных окон, и вся информация, вводимая, например, с помощью клавиатуры будет направляться именно приложению, представленному этим окном. Окно можно увели- чить или уменьшить, а также переместить в другое место экрана. Иными словами, Win- dows позволяет использовать поверхность экрана практически таким же образом, как и поверхность своего письменного стола. Все программы, соответствующие стандартам Windows-стиля, должны позволять пользователям выполнять подобные действия. Мышь Как и все предыдущие версии Windows, операционная система Windows 2000 ис- пользует манипулятор типа мышь почти во всех операциях управления, выбора и пе- ремещения. Конечно, для этого можно использовать также и клавиатуру, но в первую очередь Windows оптимизирована для работы с мышкой. Поэтому ваши программы должны поддерживать мышь в качестве основного устройства ввода, причем везде, где это только возможно. К счастью, для основного набора обычных действий, как, на- пример, использования линейки прокрутки, выбора элемента меню и тому подобного, автоматически предусмотрено использование мыши. Пиктограммы, растровые изображения и другая графика Windows 2000 широко поддерживает, даже поощряет применение пиктограмм, рас- тровых изображений и других видов графики. Обоснованием повсеместного примене- ния таких элементов служит старая проверенная временем поговорка: Лучше один раз увидеть, чем сто раз услышать1. Пиктограмма — это маленький мнемонический зна- чок, символизирующий некоторую операцию, ресурс или программу. Растровое изо- бражение, часто называемое битмапкой (bitmap) — это графическое изображение прямоугольной формы в растровом формате; такие изображения часто используются 1 Вот ее Windows-переформулировка: Лучше один раз щелкнуть на пиктограмме, чем сто раз набрать на клавиатуре. — Прим. ред. Глава 26. Создание скелета приложения для Windows 2000 575
для того, чтобы быстро донести до пользователя некоторую информацию в визуаль- ном виде. Более того, растровые изображения могут также использоваться в качестве элементов меню. Windows 2000 поддерживает очень широкий диапазон графических возможностей, в том числе прорисовку линий, прямоугольников и окружностей. Пра- вильное применение подобных графических элементов является необходимым усло- вием успешного программирования для Windows. Меню, средства управления и диалоговые окна Windows обеспечивает несколько видов стандартных элементов, которые предназначе- ны для ввода информации пользователем. В их число входят: меню, разнообразные сред- ства управления, а также диалоговые окна. Не отвлекаясь на подробности, можно сказать, что меню отображает варианты действий, на которых пользователь может остановить свой выбор. Поскольку меню являются стандартными элементами Windows-программирования, функции встроенного в меню выбора обеспечиваются средствами самой системы Windows. Из этого следует, что вашей программе не понадобится самой нести бремя непроизводи- тельных организационных издержек, связанных с применением меню. Элемент управления представляет собой окно особого вида, которое позволяет осу- ществлять специфический способ взаимодействия с пользователем. В качестве приме- ров таких окон можно привести кнопки, полосы прокрутки, окна редактирования и флажки (check boxes). Как и в случае со средствами меню, обработка элементов управления, которые определяются самой Windows, почти полностью автоматизиро- вана. Поэтому ваша программа может использовать их без необходимости погружать- ся в рутину проработки всех деталей. Диалоговое окно — специальное окно, которое позволяет осуществлять более сложное взаимодействие с программой по сравнению с возможностями, реализуемы- ми с помощью меню. Например, ваше приложение может использовать диалоговое окно, которое будет позволять пользователям вводить имя файла. Как правило, диа- логовые окна содержат еще и элементы управления. В большинстве случаев ввод ин- формации с помощью стандартных средств, реализуемый не через меню, осуществля- ется посредством диалогового окна. Н Интерфейс прикладного программирования Win32 С точки зрения прикладного программиста, Windows 2000 (как и любая другая опера- ционная система) характеризуется в первую очередь тем, каким именно образом програм- мы взаимодействуют с ней. Все приложения “общаются” с Windows 2000 через интерфейс, базирующийся на вызовах. Базирующийся на вызовах интерфейс Windows 2000 — это весьма обширный набор системных функций, которые предоставляют доступ к функциональным возможностям операционной системы. В совокупности эти функции обозначаются терми- ном Application Programming Interface (интерфейс прикладного программирования) или со- кращенно — API. API содержит несколько сотен функций, которые могут использовать ваши прикладные программы, чтобы выполнять все необходимые операции для успеш- ного взаимодействия с операционной системой. Например, распределение памяти, вывод информации на экран, создание окна и тому подобное. Подмножество функций API под названием GDI (Graphics Device Interface — графический интерфейс устройств) является той частью Windows, которая обеспечивает поддержку графического представления незави- симо от типа устройства. Существует две основных разновидности API, получивших широкое распростране- ние: Winl6 и Win32. Winl6 — более старая 16-разрядная версия API, которая исполь- 576 Часть V. Разработка программ с помощью С
зуется операционной системой Windows 3.1. Win32 — современная 32-разрядная вер- сия, интерфейс которой применяется программами в Windows 2000. (Win32 использу- ется также системами Windows 95 и Windows 98.) В целом, Win32 охватывает множе- ство функций Win 16. На самом деле в большинстве случаев функции имеют одинако- вые названия и применяются аналогичным образом. Тем не менее, будучи одинаковыми по сути и назначению, эти API отличаются друг от друга в двух фунда- ментальных аспектах. Во-первых, Win32 поддерживает 32-разрядную прямую адреса- цию, тогда как Win 16 поддерживает только 16-разрядную сегментированную модель памяти. Это различие приводит к тому, что Win32, как правило, использует 32- разрядные значения аргументов и возвращаемых результатов в тех случаях, в которых Winl6 применяет 16-разрядные значения. Во-вторых, Win32 включает функции API, которые поддерживают основанную на потоках многозадачность, защиту и другие продвинутые функциональные возможности, недоступные в Win 16. В целом же не стоит слишком беспокоиться по поводу различий. Если вы новичок в Windows- программировании, эти различия если и затронут вас, то весьма незначительно. И то только в том случае, если вы будете переносить 16-разрядный программный код на платформу Windows 2000. Тогда вам необходимо будет просто внимательно проверить все аргументы, которые будут передаваться каждой функции API. И Компоненты окна Перед тем как перейти к конкретным аспектам программирования в Windows 2000, не- обходимо объяснить несколько важных терминов. На рис. 26.1 представлено стандартное окно и его основные элементы, которые снабжены соответствующими надписями. Рис. 26.1. Элементы стандартного окна. Глава 26. Создание скелета приложения для Windows 2000 577
Все окна имеют обрамление (рамку), которое определяет границы окна и использу- ется для изменения его размеров. В верхней части окна расположено несколько элемен- тов. В частности, в левом углу находится в виде кнопки значок системного меню (альтернативное название — значок панели заголовков). Щелчок на этой кнопке приво- дит к отображению на экране системного меню. Правее значка системного меню распо- лагается заголовок окна. В правом углу находятся кнопки свертывания, развертывания и закрытия окна. Клиентская область (часто употребляется такой термин как рабочая об- ласть) — это та часть окна, в которой происходит и отображается деятельность вашей программы. Кроме того, многие окна имеют горизонтальные и вертикальные полосы прокрутки, которые используются для прокрутки текста в пределах окна. И Взаимодействие прикладных программ с Windows При написании программ для многих операционных систем обычно вы исходили из того, что именно ваша программа инициирует взаимодействие с данной операци- онной системой. Например, в системе DOS программа сама осуществляет запросы на выполнение таких операций, как ввод и вывод информации. Другими словами, про- граммы, написанные “традиционным” способом, сами обращаются к операционной системе, а операционная система после запуска прикладную программу не вызывает. Windows же в значительной степени работает совсем наоборот. Именно Windows осу- ществляет обращение к прикладным программам. Этот процесс происходит примерно следующим образом. Программа находится в состоянии ожидания до тех пор, пока Windows не пошлет ей сообщение. Сообщение передается прикладной программе по- средством специальной функции, которая вызывается самой Windows. После того как сообщение будет принято, предполагается, что прикладная программа должна выпол- нить соответствующее действие. Хотя в ответ на принятое сообщение прикладная программа может вызвать одну или несколько функций API, тем не менее, именно Windows инициирует всю эту “бурную” деятельность. По сравнению с остальными аспектами, именно механизм взаимодействия с Windows посредством сообщений больше всего определяет общий вид (структуру) всех Windows-программ. Существует множество разнообразных типов сообщений, которые Windows 2000 может посылать вашей программе. Например, всякий раз, когда выполняется щелчок кнопкой мыши в пределах принадлежащего прикладной программе окна, ей (программе) посылается сообщение о щелчке кнопкой мыши. Другой тип сообщений посылается каждый раз, когда должно быть перерисовано окно, принадлежащее при- кладной программе. Кроме того, сообщения иного вида посылаются прикладной программе каждый раз, когда пользователь нажимает клавишу, если приложение сфо- кусировано на вводе информации. Поэтому необходимо твердо усвоить следующую аксиому: построение прикладной программы должно исходить из предпосылки, что сообщения поступают к ней практически совершенно случайным образом. Вот почему Windows-приложения представляют собой управляемые прерываниями программы. Ведь вы не можете точно предсказать, каким будет следующее сообщение. В Базовые концепции функционирования приложений для Windows 2000 Прежде чем разрабатывать скелет приложения для Windows 2000, необходимо’об- судить несколько базовых концепций, лежащих в основе всех Windows-программ. 578 Часть V. Разработка программ с помощью С
WinMain() Все Windows 2000-приложения начинают свою работу с вызова функции WinMain (). (В распоряжении Windows-программ отсутствует ее “не оконный” рабо- тающий по командам вариант — функция main ()). WinMain () обладает специальны- ми свойствами, которые отличают ее от других функций в вашем приложении. Во- первых, она должна быть скомпилирована в соответствии с winAPl-соглашением о вызовах. По умолчанию функции используют С-соглашение о вызовах, но имеется возможность компилировать функцию так, чтобы она использовала другое соглаше- ние. Например, широко распространенным вариантом является применение Pascal- соглашения о вызовах. По различным техническим причинам операционной системой Windows 2000 для вызова функции WinMain () применяется WinAPI-соглашение о вы- зовах. Результат, возвращаемый функцией WinMain (), должен иметь тип int. Процедура окна Все Windows-программы должны содержать специальную функцию, которая вызы- вается не вашей программой, а самой Windows. Эта функция обычно называется про- цедурой окна (window procedure) или функцией окна (window function). Именно посред- ством этой функции Windows 2000 взаимодействует с прикладными программами. Функция окна вызывается системой Windows 2000 в тех случаях, когда ей необходимо передать сообщение прикладной программе. Это сообщение функция окна принимает через свои параметры. Все функции окна должны быть объявлены таким образом, чтобы возвращаемое ими значение соответствовало типу lresult callback. Тип LRESULT представляет собой 32-разрядное целое число. Модель вызова функций CALLBACK используется в тех случаях, когда функцию вызывает сама Windows. В Win- dows-терминологии любая функция, вызываемая самой системой Windows, относится к классу функций обратного вызова. К тому же, кроме получения сообщений, посланных операционной системой Windows 2000, функция окна должна инициировать выполнение действий, соот- ветствующих содержащимся в сообщении указаниям. Как правило, в теле функ- ции окна содержится оператор switch, который связывает конкретное ответное действие с тем сообщением, на которое программа будет реагировать. Приклад- ной программе не обязательно реагировать на все полученные сообщения. Те со- общения, которые не несут полезной информации для прикладной программы, вы можете отослать обратно в Windows 2000 для стандартной обработки, осущест- вляемой по умолчанию. Так как Windows может генерировать сотни различных сообщений, вполне естественно, что большинство сообщений обрабатывает Win- dows, а не прикладная программа. Все сообщения представляют собой 32-разрядные целые значения. Кроме того, все сообщения сопровождаются некоторой дополнительной информацией, характерной для каждого сообщения. Классы окон Когда программа стартует под Windows 2000, первое, что ей необходимо сделать, так это определить и зарегистрировать класс окна (window class), который подразумева- ет стиль или тип этого окна. Когда прикладная программа регистрирует класс окна, она сообщает Windows сведения о форме (т. е. внешнем виде) и функции окна. Одна- ко регистрация класса окна еще не приводит к его появлению на экране. Чтобы дей- ствительно создать окно, необходимо выполнить дополнительную работу. Глава 26. Создание скелета приложения для Windows 2000 579
Цикл обработки сообщений Как уже упоминалось, Windows 2000 взаимодействует с прикладной программой по- средством отправки ей сообщений. Все Windows-програмы должны содержать цикл обра- ботки сообщений внутри функции winMain (). Этот цикл извлекает приходящее сообще- ние из очереди сообщений приложения, после чего отсылает его обратно в Windows. Опе- рационная система, в свою очередь, вызывает функцию окна вашей программы, причем с этим же сообщением в качестве параметра. Эго может показаться чересчур запутанным методом передачи сообщений, но, тем не менее, это единственный путь, которого должны придерживаться все Windows-программы. (Частично причина такого положения состоит в том, чтобы оперативно возвращать управление Windows. В этом случае планировщик зада- ний операционной системы может распределять время работы центрального процессора так, как он считает целесообразным, а не ожидать, когда закончится промежуток времени, выделенный для прикладной программы.) Типы данных Windows В функциях Windows API не очень часто используются стандартные типы данных языка программирования С, например int или char*. Вместо этого многие типы данных, применяемых в Windows, должны быть определены с помощью оператора typedef в файле WINDOWS.H и (или) в связанных с ним файлах. Этот файл постав- ляется компанией Microsoft (а также другими компаниями, выпускающими компиля- торы С для платформы Windows) и должен включаться во все Windows-программы. Вот примеры наиболее употребительных типов данных: handle, hwnd, uint, byte, WORD, DWORD, LONG, BOOL, LPSTR И LPCSTR. Тип HANDLE — ЭТО 32-разряДНОе целое число, которое используется как дескриптор. Существует множество типов дескрипто- ров, но все они имеют такой же размер, как и handle. Дескриптор является просто значением, которое используется для уникальной идентификации некоторого объекта или ресурса. Например, тип hwnd является 32-разрядным целым числом, которое ис- пользуется в качестве дескриптора окна. К тому же, имена дескрипторов всех типов начинаются с буквы Н. Тип byte является 8-разрядным целым без знака (unsigned); WORD — 16-разрядное короткое целое без знака; DWORD — 32-разрядное целое без зна- ка; uint — 32-разрядное целое без знака; long — 32-разрядное целое со знаком; BOOL — целый тип, используемый для указания величин, которые могут принимать значения, которые интерпретируются как ИСТИНА или ЛОЖЬ; LPSTR — указатель на строку, а тип lpcstr — константный (const) указатель на строку. В дополнение описанным выше базовым типам, в Windows 2000 определены еще несколько структур. Две из них, MSG и wndclassex, необходимы для создания скелета программы. Структура MSG содержит в себе сообщение Windows 2000, а WNDCLASSEX — структура, которая определяет класс окна. Позже в данной главе эти структуры будут обсуждаться более подробно. Н Скелет программы для Windows 2000 Теперь, когда представлена вся необходимая предварительная информация, можно приступить к разработке минимального приложения для Windows 2000. Как уже гово- рилось, все программы для Windows 2000 имеют некоторые общие атрибуты. Скелет программы для Windows 2000, разработанный в этой главе, имеет все необходимые функциональные свойства. В мире Windows-программирования скелеты приложений (другими словами — программы-заготовки) используются довольно часто, поскольку “входная плата” при создании Windows-программ довольно значительна. В качестве 580 Часть V. Разработка программ с помощью С
примера, сравните следующие показатели. В отличие от DOS-программ, у которых минимальный размер программы уложится всего в пять строк кода, минимальная программа для Windows составляет примерно пятьдесят строк. Минимальная программа для Windows 2000 содержит две функции: WinMain () и функцию окна. Функция WinMain () должна выполнить следующие общие действия: 1. Описать класс окна; 2. Зарегистрировать этот класс в Windows 2000; 3. Создать окно данного класса; 4. Отобразить это окно; 5. Запустить выполнение цикла обработки сообщений. Функция окна должна адекватно реагировать на все имеющие отношение к при- кладной программе сообщения. Поскольку скелетная программа кроме отображения окна на экране дисплея больше ничего не делает, единственным сообщением, на ко- торое она должна отреагировать, является сообщение о том, что пользователь прекра- тил выполнение программы. Перед тем как перейти к подробному обсуждению отдельных вопросов, рассмот- рим следующую программу, которая представляет собой минимальный скелет про- граммы для Windows 2000. Эта программа-заготовка создает стандартное окно, содер- жащее заголовок, кнопки системного меню, а также стандартные кнопки свертыва- ния, развертывания и закрытия окна. Благодаря этому окно можно будет свернуть, развернуть, перемещать по экрану, изменять его размеры и, наконец, закрыть. /* Минимальный скелет программы для Windows 2000. */ #include <windows.h> LRESULT CALLBACK WindowFunc(HWND, UINT, WPARAM, LPARAM); char szWinName[] = "MyWin"; /* имя класса окна */ int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR IpszArgs, int nWinMode) { HWND hwnd; MSG msg; WNDCLASSEX wcl; /* Определим класс окна. */ wcl.cbSize = sizeof(WNDCLASSEX); wcl.hlnstance = hThisInst; /* дескриптор данного экземпляра */ wcl.IpszClassName = szWinName; /* имя класса окна */ wcl.IpfnWndProc = WindowFunc; /* функция окна */ wcl.style =0; /* стиль по умолчанию */ wcl.hlcon = Loadicon(NULL, IDI__APPLICATION); /* большая пиктограмма */ wcl.hlconSm = NULL; /* использовать уменьшенный вариант большой пиктограммы */ wcl.hCursor = LoadCursor(NULL, IDC_ARROW); /* стиль курсора */ wcl.IpszMenuName = NULL; /* класс меню отсутствует */ wcl.cbClsExtra =0; /* дополнительная память не Глава 26. Создание скелета приложения для Windows 2000 581
требуется */ wcl.cbWndExtra = 0; /* Сделаем белым цвет фона окна. */ wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH); /* Зарегистрируем класс окна. */ if(!RegisterClassEx(&wcl)) return 0; /* Поскольку класс окна уже зарегистрирован, теперь может быть создано окно. */ hwnd = CreateWindow( szWinName, /* имя класса окна */ ’’Windows 2000 Skeleton’’, /* заголовок */ WS_OVERLAPPEDWINDOW, /* стиль окна - стандартный */ CW—USEDEFAULT, /* Координата X - пусть решает Windows */ CW—USEDEFAULT, /* Координата Y - пусть решает Windows */ CW—USEDEFAULT, /* Ширина - пусть решает Windows */ CW—USEDEFAULT, /* Высота - пусть решает Windows */ NULL, /* Дескриптор родительского окна - родительское окно отсутствует */ NULL, /* Дескриптор меню - меню отсутствует */ hThisInst, /* Дескриптор экземпляра */ NULL /* Дополнительные аргументы отсутствуют */ ) ; /* Отобразим окно. */ ShowWindow(hwnd, nWinMode); UpdateWindow(hwnd); /* Создадим цикл обработки сообщений. */ while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); /* трансляция клавиатурных сообщений */ DispatchMessage(&msg); /* возвратить управление Windows 2000 */ } return msg.wParam; } /* Эта функция вызывается Windows 2000 и пересылает сообщения из очереди сообщений. */ LRESULT CALLBACK WindowFunc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam) { switch(message) { case WM_DESTROY: /* завершить программу */ PostQuitMessage(0); break; default: /* Пусть Windows 2000 обрабатывает все сообщения, не перечисленные в предыдущем операторе switch. */ return DefWindowProc(hwnd, message, wParam, IParam); } return 0; } 582 Часть V. Разработка программ с помощью С
Давайте тщательно, пункт за пунктом, проанализируем эту программу. Во-первых, все Windows-программы должны содержать заголовочный файл WINDOWS.Н. Как уже упоминалось, этот файл (вместе с сопутствующими файлами) содержит прототи- пы функций API и всевозможные типы, макросы и описания, используемые самой Windows. Например, в файле WINDOWS. Н (или в его придаточных файлах) опреде- лены типы данных HWND и wndclassex. Функция окна, используемая данной программой, называется WindowFunc (). Она объявлена как функция обратного вызова, поскольку именно эту функцию Windows вызывает для взаимодействия с данной программой. Как уже говорилось, работа программы начинается с выполнения WinMain (). Функции WinMain () передается четыре параметра. Из них hThisInst и hPrevlnst — дескрипторы. Дескриптор hThisInst относится к текущему экземпля- ру программы. Помните, что Windows 2000 является многозадачной системой, поэто- му одновременно может выполняться более одного экземпляра вашей программы. Для Windows 2000 дескриптор hPrevlnst всегда принимает значение NULL. Параметр IpszArgs является указателем на строку, которая содержит аргументы командной строки, указанные при запуске приложения. В Windows 2000 эта строка содержит всю командную строку, в том числе и имя программы. Параметр nWinMode содержит зна- чение, которое определяет то, как будет отображаться окно в момент, когда програм- ма начнет выполняться. При выполнении данной функции в ней будут созданы три переменные. Пере- менная hwnd будет содержать дескриптор окна программы. Структурная переменная msg будет содержать сообщение окна, а структурная переменная wcl будет использо- ваться для описания класса окна. Определение класса окна В первую очередь функция WinMain () выполняет два действия: определение клас- са окна, а затем его регистрация. Класс окна описывается путем заполнения необхо- димых значений в полях, определяемых структурой wndclassex. Вот эти поля: UINT cbSize; UINT style; WNDPROC IpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hlnstance; EICON hlcon; EICON hlconSm; ECURSOR hCursor; EBRUSE hbrBackground; LPCSTR IpszMenuName; LPCSTR IpszClassName; /* размер структуры WNDCLASSEX */ /* тип окна */ /* адрес функции окна */ /* дополнительная память класса */ /* дополнительная память окна */ /* дескриптор данного экземпляра */ /* дескриптор большой пиктограммы */ /* дескриптор маленькой пиктограммы */ /* дескриптор указателя мыши */ /* цвет фона */ /* имя главного меню */ /* имя класса окна */ Как видно из приведенного листинга, cbSize задает размер структуры WNDCLASSEX. Элемент hlnstance определяет дескриптор текущего экземпляра и устанавливается в соответствии со значением дескриптора hThisInst. Имя класса окна указывается с помощью поля IpszClassName, которое в нашем случае ука- зывает на строку "MyWin”. Адрес функции окна устанавливается в IpfnWndProc. В данной программе не назначается стиль по умолчанию, не требуется никакой дополнительной информации и не определяется главное меню. Хотя большинство программ содержат главное меню, в нем нет никакой необходимости для скелета приложения. Глава 26. Создание скелета приложения для Windows 2000 583
Все Windows-приложения должны определять используемые по умолчанию формы (изображения) указателя мыши и пиктограммы приложения. Прикладная программа может определять свою собственную пользовательскую версию этих ресурсов или она может использовать один из встроенных стилей, что и создает скелет приложения. В любом случае дескрипторы этих ресурсов должны быть присвоены соответствующим элементам структуры WNDCLASSEX. Чтобы лучше понять, как это делается, начнем, пожалуй, с пиктограмм. Любое приложение для Windows 2000 имеет две связанные с ним пиктограммы: одна большая и одна маленькая. Маленькая пиктограмма используется в том случае, когда окно приложения свернуто. Эта же пиктограмма используется для отображения значка систем- ного меню программы. Большая пиктограмма отображается на экране в том случае, когда вы перемещаете или копируете свое приложение на рабочий стол Windows. Как правило, большие пиктограммы представляют собой растровые изображения размером 32x32 пиксе- ля, а маленькие — размером 16x16 пикселей. Большая пиктограмма загружается посредст- вом API-функции Loadicon (), чей прототип приведен ниже: HICON Loadicon (HINSTANCE hlnst, LPCSTR IpszName); Эта функция возвращает дескриптор пиктограммы, а в случае аварийного завер- шения — значение NULL. В приведенном примере hlnst определяет дескриптор моду- ля, который содержит пиктограмму, а ее название определяется параметром IpszName. Впрочем, чтобы воспользоваться одной из встроенных пиктограмм, необходимо ис- пользовать null для первого параметра и указать один из следующих макросов в ка- честве второго параметра: Макрос пиктограммы Картинка IDI_APPLICATION Пиктограмма по умолчанию IDI_ERROR Символ ошибки IDIJNFORMATION Символ информации IDI_QUESTION Знак вопроса IDI_WARNING Восклицательный знак 1DI_WINLOGO Логотип Windows При загрузке пиктограмм следует обратить особое внимание на два важных мо- мента. Во-первых, если ваше приложение не определяет маленькую пиктограмму, бу- дет исследован ресурсный файл большой пиктограммы. Если в нем содержится ма- ленькая пиктограмма, то именно она и будет использована. В противном случае при необходимости маленькая пиктограмма будет получена в результате уменьшения (пропорционального сжатия) большой пиктограммы. Если вы не хотите определять маленькую пиктограмму, присвойте значение NULL параметру hlconSm — именно так и поступает наша программа-заготовка. Во-вторых, функция Loadicon (), вообще го- воря, может применяться только для загрузки большой пиктограммы. Для загрузки пиктограмм произвольного размера можно воспользоваться функцией Loadimage (). Чтобы загрузить указатель мыши, используйте API-функцию Loadcursor (). Дан- ная функция имеет следующий прототип: HCURSOR LoadCursor (HINSTANSE hlnst, LPCSTR IpszName) , Эта функция возвращает дескриптор ресурса курсора или NULL в случае ава- рийного завершения. В данном примере hlnst определяет дескриптор модуля, со- держащего курсор мыши, а его имя указывается в параметре IpszName. Чтобы вос- пользоваться одним из встроенных указателей мыши, необходимо использовать null в качестве первого параметра и задать макрос одного из встроенных курсо- ров мыши в качестве второго параметра. Ниже приведено несколько встроенных курсоров: 584 Часть V. Разработка программ с помощью С
Макрос курсора мыши IDC_ARROW IDC_CROSS IDCJHAND IDCJBEAM IDC_WAIT Форма Указатель-стрелка по умолчанию Перекрестие Рука Вертикальная двутавровая балка Песочные часы В качестве цвета фона окна, созданного скелетом программы, выбран белый цвет, а дескриптор кисти (brush) получается с помощью API-функции GetStockObject (). Кисть является ресурсом, который окрашивает экран с учетом предварительно задан- ных размера, цвета и узора. Функция GetStockObject () применяется для получения дескриптора ряда стандартных объектов отображения, в том числе кистей, перьев (которые проводят линии) и шрифтов символов. Вот его прототип: HGDIOBJ GetStockObject (int object); Данная функция возвращает дескриптор объекта, определенного параметром object. В случае аварийного завершения возвращается значение NULL. (Тип HGDIOBJ отно- сится к GDI-дескрипторам). Ниже приведено несколько встроенных кистей, доступ- ных вашей программе: Имя макроса Тип фона BKACK_BRUSH Темно серый DKGRAY_BRUSH Полупрозрачный (видно сквозь окно) HOLLOW_BRUSH Черный LTGRAY.BRUSH Светло серый WHITE_BRUSH Белый Для получения кисти можно использовать эти макросы в качестве параметров функции GetStockObj ect (). После того как класс окна полностью определен, он регистрируется в Windows 2000 с помощью API-функции RegisterClassEx (), прототип которой приведен ниже: ATOM RegisterClassEx (CONST WNDCLASSEX *lpWCldSS) ; Эта функция возвращает значение, которое идентифицирует класс окна, атом яв- ляется typedef-описанием типа, которое подразумевает тип WORD. Каждый класс ок- на принимает уникальное значение. Параметр IpWClass должен содержать адрес структуры WNDCLASSEX. Создание окна После того, как класс окна определен и зарегистрирован, ваше приложение может на самом деле создать окно этого класса с помощью API-функции CreateWindow (), прототип которой выглядит следующим образом: HWND CreateWindow( LPCSTR IpszClassName, LPCSTR IpszWinName, Dword dwStyle, int X, int Y, int Width, int Height, HWDN h Pa rent, HMENU hMenu, HLNSTANCE hThisInst, LPVOID IpszAdditional, ) ; /★ название класса окна */ /* заголовок окна */ /* тип окна */ /* координаты верхней левой точки */ /* размеры окна */ /* дескриптор родительского окна */ /* дескриптор главного меню */ /* дескриптор создателя */ /* указатель на дополнительную информацию */ Глава 26. Создание скелета приложения для Windows 2000 585
Как видно из листинга скелета программы, многим параметрам функции Сге- ateWindow () значения могут присваиваться по умолчанию или же в качестве значения им можно присвоить null. В действительности, в качестве параметров X, Y, Width и Height чаще всего используется макрос cw_USEDEFAULT; в этом случае Windows 2000 вы- бирает подходящий размер и местоположение окна. Если данное окно не имеет роди- тельского окна (а именно этот случай имеет место в нашем скелете программы), то в качестве параметра hParent может быть указан null. (Для указания значения этого па- раметра можно также использовать HWND_DESKTOP.) Если окно не содержит главного меню или использует главное меню, которое определено посредством класса окна, то параметр hMenu должен иметь значение null. (Параметр hMenu имеет также и другие применения.) К тому же, если никакая дополнительная информация не требуется, что характерно для большинства случаев, то параметру IpszAdditional можно присвоить зна- чение NULL. (Тип LPVOID переопределяется оператором typedef как void*. Историче- ски сложилось так, что LPVOID обозначает длинный указатель на void.) Значения остальных четырех параметров должны быть явно установлены приклад- ной программой. Во-первых, параметр IpszClassName должен указывать на имя класса окна. (Это то имя, которое вы дали окну при его регистрации.) Заголовок окна — это последовательность символов, на которую указывают посредством IpszWinName. Это может быть и пустая строка, но, как правило, окну следует давать какой-то заголовок. Стиль (или тип) окна, созданного в действительности, определяется значением пара- метра dwStyle. Макрос WS_OVERLAPPEDWINDOW определяет стандартное окно, которое имеет системное меню, обрамление и кнопки свертывания, развертывания и закрытия окна. Хотя чаще всего используется именно такой стиль окна, вы можете построить окно, удовлетворяющее вашим собственным критериям. Для этого просто объедините с помощью оператора OR макросы различных необходимых вам стилей. Ниже приве- дены некоторые часто встречающиеся стили: Макрос стиля Функция Windows WS_OVERLAPPED Перекрывающееся окно с обрамлением WS_MAXIMIZEBOX Кнопка развертывания WS_MIN1MIZEBOX Кнопка свертывания WS_SYSMENU Системное меню WS_HSCROLL Горизонтальная полоса прокрутки WS_VSCROLL Вертикальная полоса прокрутки Параметр hThisInst игнорируется операционной системой Windows 2000, но для Windows 95/98 он должен содержать дескриптор текущего экземпляра приложения. Поэтому, чтобы обеспечить совместимость с этими средами, а заодно и предотвратить проблемы в будущем, параметру hThisInst рекомендуется присваивать значение деск- риптора текущего экземпляра, как это сделано в нашем скелете программы. Функция CreateWindow () возвращает дескриптор созданного ею окна или NULL, если окно не может быть создано. Даже после создания окна оно все еще не отображается на экране дисплея. Чтобы отобразить окно, надо вызвать API-функцию Showwindow (). Эта функция имеет сле- дующий прототип: BOOL Showwindow(HWND hwndf int nHow} ; Дескриптор отображаемого дисплеем окна указывается в параметре hwnd. А пара- метром nHow определяется режим отображения. Если окно выводится на экран в пер- вый раз, целесообразно в качестве параметра nHow указать значение параметра nWin- Mode функции WinMain (). Значение nWinMode определяет способ отображения окна сразу после запуска программы на выполнение. Последующие вызовы могут при не- обходимости отобразить окно в нужном виде или вовсе удалить его. Некоторые обще- употребительные значения параметра nHow приведены ниже: 586 Часть V. Разработка программ с помощью С
Макрос отображения Получаемый эффект SW_ HIDE Удаляет окно с экрана SW_,MINIMIZE Свертывает окно в пиктограмму SW_MAXIMIZE Развертывает окно SW_RESTORE Возвращает окну обычный размер Функция Showwindow () возвращает статус предыдущего режима отображения ок- на. Если окно выводилось на экран, то возвращается ненулевое значение. А если окно не отображалось на экране, то возвращается нуль. Хотя с формальной точки зрения для скелета программы это и не обязательно, все же в его текст включен вызов функции Updatewindow (), поскольку она необходима практически для всех Windows 2000-приложений. По существу, она указывает, что Windows 2000 должна послать вашему приложению сообщение о том, что его главное окно необходимо обновить. Цикл обработки сообщений Финальная часть функции WinMain () заготовки прикладной программы относит- ся к циклу обработки сообщений. Цикл обработки сообщений является составной ча- стью всех Windows-приложений. Его назначение — принять и обработать сообщение, посланное Windows 2000. Во время выполнения прикладной программы ей постоянно посылаются сообщения. Все эти сообщения сохраняются в очереди сообщений при- ложения и находятся там до тех пор, пока они не будут извлечены и обработаны. Вся- кий раз, когда приложение готово извлечь следующее сообщение, оно должно вызвать API-функцию GetMessage (), имеющую следующий прототип: BOOL GetMessage(LPMSG msg, HWND hwnd, UINT min, UINT max} ; Сообщение будет записано в структуру, на которую указывает параметр msg. Все сообщения Windows имеют тип структуры MSG, представленный ниже: /* Структура сообщения */ typedef stuct tagMSG { HWND hwnd; /* окно, для которого предназначено сообщение */ UINT message; /* сообщение */ WPARAM wParam; /* информация, обусловленная сообщением */ LPARAM IParam; /* дополнительная информация, обусловленная сообщением */ DWORD time; /★ время, когда было отправлено сообщение */ POINT pt; /* координаты X и Y местоположения указателя мыши */ } MSG; В структуре MSG дескриптор окна, которому предназначается сообщение, содер- жится в hwnd. Все сообщения в Windows 2000 являются 32-разрядными целыми чис- лами, а само сообщение содержится в поле message. В зависимости от конкретного сообщения обусловленная им дополнительная информация передается в wParam и IParam. Оба типа WPARAM и LPARAM являются 32-разрядными значениями. Время, когда было отправлено (зарегистрировано) сообщение, определяется в мил- лисекундах в поле time. Элемент pt содержит координаты указателя мыши в тот момент, когда было от- правлено сообщение. Координаты хранятся в структуре point, которая определяется следующим образом: I typedef struct tagPOINT { I LONG x, у; I } POINT; Глава 26. Создание скелета приложения для Windows 2000 587
Если в очереди сообщений приложения отсутствуют сообщения, то вызов функции GetMessage () приведет к передаче управления обратно Windows 2000. Параметр hwnd функции GetMessage () определяет, для какого окна будут получе- ны сообщения. Ведь часто приложение имеет несколько окон, и иногда необходимо принимать сообщения только для конкретного окна. Ну а если этому параметру при- своить значение null, то в ваше приложение будут направляться все сообщения. Оставшиеся два параметра функции GetMessage () определяют диапазон получае- мых сообщений. Обычно приложение должно принимать все сообщения. Для этого необходимо оба параметра (и min, и max) установить равными нулю; именно так и сделано в скелете программы. Функция GetMessage () возвращает нуль, когда пользователь прекращает работу программы, что приводит к завершению цикла обработки сообщений. Во всех осталь- ных случаях данная функция возвращает ненулевое значение. Если происходит ошиб- ка, эта функция возвращает -1. Ошибка может произойти только при необычных об- стоятельствах, которые не приемлемы для работы большинства программ. Внутри цикла обработки сообщений осуществляется вызов двух функций. Первая — это API-функция TranslateMessage (). Эта функция транслирует генерируемые опера- ционной системой Windows 2000 виртуальные коды клавиш в символьные сообщения. Хо- тя ее применение необязательно во всех приложениях, в большинстве из них все же ис- пользуется вызов функции TranslateMessage (), поскольку она необходима для осущест- вления полной интеграции клавиатуры в вашу прикладную программу. После того как сообщение было извлечено и преобразовано, оно отсылается обратно в Windows 2000 с помощью API-функции DispatchMessage (). Затем Windows 2000 хранит это сообщение до тех пор, пока не сможет передать его функции окна вашей программы. Как только завершается цикл обработки сообщений, выполнение функции Win- Main () заканчивается, при этом она возвращает Windows 2000 значение msg.wParam. Это значение содержит код возврата, генерируемый при завершении выполнения прикладной программы. Функция окна Второй функцией в скелете приложения является его функция окна. В нашем случае эта функция названа WindowFunc (), хотя она может носить любое понравившееся вам имя. Сообщения передаются функции окна посредством Windows 2000. Первые четыре элемента структуры MSG являются его параметрами. Из них единственным параметром, который используется скелетом программы, является собственно сообщение. Функция окна нашей программы-заготовки реагирует явным образом только на одно сообщение: wm_destroy. Такое сообщение посылается тогда, когда пользователь прекращает работу приложения. После получения такого сообщения программа должна осуществить вызов API-функции PostQuitMessage (). Аргументом этой функции является код возврата, который помещается в msg.wParam внутри функции WinMain (). Вызов PostQuitMessage () приводит к передаче приложению сообщения WM_QUIT, что заставляет функцию GetMessage () возвратить значение ЛОЖЬ (false) и, следовательно, прекратить работу вашей программы. Все остальные сообщения, принимаемые функцией WindowFunc (), передаются операционной системе Windows 2000 посредством вызова DefwindowProc () для стан- дартной обработки по умолчанию. Этот этап необходим, поскольку все сообщения так или иначе должны быть обработаны. Каждое сообщение устанавливает некоторое значение, которое должно быть воз- вращено функцией окна после обработки сообщения. Обычно после обработки боль- шей части сообщений необходимо возвращать нулевое значение. Но после обработки некоторых сообщений требуется возвратить другой код возврата. 588 Часть V. Разработка программ с помощью С
OS Файл описания больше не нужен Если вам приходилось создавать 16-разрядные программы для Windows, то вы помните, что при этом необходимо было использовать еще и файлы описаний (definition files). Что касается 16-разрядных версий Windows, все программы должны были иметь сопутствующие файлы описаний. Файл описаний — это просто текстовый файл, который задает определенную информацию и параметры, которые необходимо указывать для 16-разрядной среды. Поскольку Windows 2000 имеет 32-разрядную ар- хитектуру (а также другие усовершенствования), файлы описаний, как правило, не являются больше необходимым атрибутом для программ, работающих под управлени- ем Windows 2000. Если вы новичок в программировании для Windows и не имеете ни- какого представления о файлах описаний, то следующий материал будет вам, конечно же, полезен. Все файлы описаний имеют расширение .DEF. Например, файл описаний для на- шей программы-заготовки мог бы иметь название SKELL.DEF. Ниже приведен файл описания, который можно использовать для обеспечения нисходящей совместимости с Windows 3.1: DESCRIPTION ’Skeleton Program’ EXETYPE WINDOWS CODE PRELOAD MOVEABLE DISCARDABLE DATA PRELOAD MOVEABLE MULTIPLE HEAPSIZE 8192 STACKSIZE 8192 EXPORTS WindowFunc Приведенный файл определяет название программы и ее описание, причем оба эти параметра не являются обязательными. Здесь также формулируется утверждение, что исполняемый файл будет совместим с Windows (а не с DOS, например). Оператор CODE сообщает Windows 2000, что при запуске программы ее необходимо загрузить всю целиком (элемент PRELOAD), что код программы разрешается перемещать в па- мяти (элемент MOVEABLE) и что данный код может быть удален из памяти и загру- жен повторно, когда в этом возникнет необходимость (DISCARDABLE). К тому же, этот файл определяет то, что данные для прикладной программы должны быть загру- жены в самом начале ее выполнения, а также допускается их перемещение в другие области памяти. Здесь же устанавливается, что каждый экземпляр программы имеет свои собственные данные (MULTIPLE). Следующие строки сообщают, что программе выделяются динамически распределяемая область памяти (куча) и стек указанных размеров. И, наконец, указывается имя экспортируемой функции окна. Экспорт функции предоставляет возможность системе Windows 3.1 осуществлять ее вызов. Не забывайте, что файлы описаний редко используются в процессе программиро- вания для 32-разрядных версий Windows. Н Соглашения об именовании Прежде чем закончить данную главу, необходимо привести небольшие пояснения по поводу именования функций и переменных. Новичкам в Windows- программировании некоторые имена переменных и параметров в программе- заготовке и ее описании покажутся, вероятно, довольно необычными. Причина кро- ется в том, что подобные имена являются следствием строгого соблюдения ряда со- глашений о присвоении имен, которые были изобретены и учреждены компанией Mi- crosoft для программирования под управлением Windows. Согласно этим соглашени- Глава 26. Создание скелета приложения для Windows 2000 589
ям, название функции состоит из глагола, за которым следует существительное. При- чем первая буква и глагола, и существительного пишутся с большой буквы. Что касается имен переменных, то здесь Microsoft избрала путь применения до- вольно сложной системы указания типа данных в имени переменной. В соответствии с ней, впереди имени переменной добавляется префикс типа (который пишется строчными буквами). Непосредственно имя переменной пишется с заглавной буквы. Префиксы типов приведены в табл. 26.1. Целесообразность применения префиксов типа не является очевидной (для многих она кажется скорее даже спорной), поэтому такая модель именования не получила повсеместного признания и не стала универ- сальной. Многие Windows-программисты пользуются именно данным способом име- нования, но в то же время не меньшее количество не приемлют его. Вам, естествен- но, предоставляется свобода выбора применять любое соглашение об именовании, ко- торое придется вам по душе. Таблица 26.1. Символы префикса для переменных различных типов Префикс Тип данных b с dw f fn h I булев (1 байт) символ (1 байт) длинное целое без знака 16-разряд ное битовое поле (флаги) функция дескриптор длинное целое Ip n p pt w sz Ipsz rgb длинный указатель короткое целое указатель длинное целое, содержащее координаты экрана короткое целое без знака указатель на строку — массив, оканчивающийся нулевым символом длинный указатель на строку — массив, оканчивающийся нулевым символом длинное целое, содержащее значения RGB-цветов 590 Часть V. Разработка программ с помощью С
Глава 27 Проектирование программ с помощью С
Начнем главу со сравнений. Так, создание большой компьютерной программы и проектирование крупного здания имеют много общих черт. В английском языке даже должность разработчика программного обеспечения в наши дни часто называет- ся “architect” (“архитектор”). Без сомнения, скрытые от взора непосвященных меха- низмы, благодаря которым делается возможным как проектирование крупных зданий, так и проектирование крупных программ, в действительности подобны друг другу. И заключаются они в применении подходящих методов проектирования. В этой главе будут подробно рассмотрены некоторые методы проектирования, характерные для среды программирования на языке С; эти методы значительно облегчают процесс разработки и сопровождения программ. Данная глава в основном предназначена для новичков в программировании. Опытным профессионалам большая часть изложенного здесь материала будет, конечно, уже знакома. В Проектирование сверху вниз Без сомнения, главнейшее условие успешного создания крупных программ заключает- ся в применении надежных методов проектирования. Широкое распространение при на- писании программ получили следующие три метода: нисходящий (сверху вниз), восходя- щий (снизу вверх) и специальный (на данный конкретный случай). В случае нисходящего метода вы начинаете созидательный процесс с программы высокого уровня и спускаетесь до подпрограмм низкого уровня. Восходящий метод работает в обратном направлении: вы начинаете с отдельных специальных подпрограмм, постепенно строите на их основе более сложные конструкции и заканчиваете самым верхним уровнем программы. Специальный подход не имеет заранее установленного метода. Поскольку С является структурированным языком программирования, то лучше всего он сочетается с нисходящим методом проектирования. Подход сверху вниз по- зволяет производить ясный, легко читаемый программный код, который в дальней- шем не вызовет трудностей и при сопровождении. К тому же данный подход помога- ет прояснить и сделать понятной всю структуру программы в целом до кодирования функций более низкого уровня. Такой подход позволяет уменьшить потери времени, обусловленные неудачными или ошибочными начинаниями. Структурирование программы Как и для любой общей схемы, при применении нисходящего метода начинают с общего описания программы, а затем переходят к проработке ее конкретных элемен- тов. На практике при разработке программы лучше всего сначала точно определить, что программа будет делать на самом высоком уровне, и только после этого погру- жаться в подробности, касающиеся каждого действия. Предположим, к примеру, что необходимо написать программу, предназначенную для ведения списка рассылки. Во- первых, необходимо составить перечень действий, которые будет выполнять такая программа. Каждая строчка данного перечня должна содержать только один функ- циональный элемент. (На этом этапе под функциональным элементом понимается “черный ящик”, который выполняет только одну задачу.) Тогда такой перечень может быть представлен примерно в следующем виде: Ввести новый адрес Удалить новый адрес Печать списка Найти имя 592 Часть V. Разработка программ с помощью С
Сохранить список Загрузить список Завершить выполнение программы После того как вы определили все функциональные возможности программы, можно в общих чертах описать подробные свойства каждого функционального модуля, начиная с основного цикла. Ниже приведен один из возможных вариантов такого представления, по- зволяющий реализовать основной цикл программы работы со списком рассылки: main loop { do { вывести меню определить выбор пользователя выполнить выбранное действие } while выбор != quit } Такая разновидность алгоритмического представления (иногда называемая псевдо- кодом) может помочь внести ясность в общую структуру программы; ее необходимо выполнить до кодирования. С-подобный синтаксис был использован по той простой причине, что он вам уже знаком, но можно воспользоваться и любым другим подхо- дящим синтаксисом. Аналогичные определения необходимо дать каждому функциональному элементу. Например, вы можете описать функцию, которая осуществляет запись списка рассыл- ки в файл на диске примерно следующим образом: save to disk { open disk file while есть данные write { write данные на диск } close disk file } На этом уровне абстракции функция “записать-на-диск” обращается к новым функциональным модулям более низкого уровня. Эти модули открывают файл на диске, записывают на него данные, а затем закрывают дисковый файл. В дальнейшем необходимо будет еще определить и каждый из этих модулей. Если при их описании будут создаваться новые функциональные элементы, они также должны быть опреде- лены и т.д. Этот процесс закончится, когда при описании больше не будет создан ни один новый функциональный элемент. Тогда остается всего лишь сесть и написать реальный С-код, который реализует эти действия. Например, модуль, который закры- вает дисковый файл, вероятно, будет транслирован в вызов функции fclose (). Обратите внимание, что при подобном определении совсем ничего не упоминается о структуре данных и переменных. Это сделано умышленно, потому что до сих пор вы хотели определить только то, что должна делать ваша программа, но не то, каким об- разом она реально будет это осуществлять. Такой описательный процесс помогает вы- брать эффективную структуру данных. (Естественно, структуру данных необходимо будет описать перед кодированием функциональных элементов.) Выбор структуры данных После определения общей структуры прикладной программы необходимо решить, какой будет структура данных. Выбор структуры данных и ее реализация имеют чрез- вычайно важное значение, поскольку она помогает определить проектные ограниче- ния вашей программы. Глава 27. Проектирование программ с помощью С 593
Список рассылки, как правило, содержит следующую информацию: имена, названия улиц, городов, штатов и почтовые индексы. При нисходящем подходе это немедленно предполагает применение определенной структуры для хранения этой информации. И тут же возникает вопрос: а как такие структуры будут храниться и обрабатываться? Для программы, работающей со списком рассылки, можно было бы использовать массив структур фиксированного размера. Но массив фиксированного размера имеет один серьезный недостаток: размер массива жестко и безапелляционно ограничивает длину списка рассылки. Более удачное решение заключается в динамическом выделении памя- ти для каждого адреса. Тогда каждый адрес будет сохраняться в динамической структуре данных определенной формы (например, в связном списке), которая может прй необхо- димости расти или уменьшаться. В таком случае список может быть огромным или очень маленьким в зависимости от конкретных обстоятельств. Хотя на данном этапе мы отдали предпочтение динамическому распределению па- мяти, а не массиву фиксированного размера, все же точная модель представления данных все еще не определена. Существует несколько возможных вариантов. Можно использовать однонаправленный связный список, двунаправленный связный список, двоичное дерево и даже метод хэширования. Каждый из этих методов имеет свои дос- тоинства и недостатки. Ограничим круг обсуждения и предположим, что конкретно нашему приложению, обрабатывающему список рассылки, предъявляется требование достичь минимального времени поиска. Исходя из этого, мы выбираем двоичное де- рево. Теперь можно определить структуру, которая содержит в списке каждое имя и адрес, как показано ниже: struct addr { char name[30]; char street[40]; char city[20]; char state[3]; char zip[11]; struct addr *left; /* указатель на левое поддерево */ struct addr *right; /* указатель на правое поддерево */ }; Теперь, после определения структуры данных, можно приступить к кодированию всей программы. Для этого надо всего лишь определить все подробности, описанные в обшей структуре созданного ранее псевдокода. Если вы последуете нисходящему ме- тоду, программы будут не только легко читаться, но и потребуют меньше времени на разработку и сопровождение. bJ “Пуленепробиваемые” функции В больших программах, особенно в тех, которые предназначены для управления устройствами, которые потенциально могут представлять угрозу для жизни, любая возможность возникновения ошибки должна быть сведена к минимуму. Маленькие программы еще могут быть верифицированы (тщательно проверены на отсутствие ошибок), но с большими программами такая работа вряд ли осуществима. (Верифицированная программа не содержит ошибок и никогда не допускает сбоев в работе, по крайней мере, теоретически.) К примеру, представьте себе программу, ко- торая управляет закрылками современного реактивного самолета. Физически невоз- можно протестировать все возможные взаимодействия сил, которые могут быть при- ложены к самолету. Это означает, что вы не можете в полной мере протестировать программу. В лучшем случае, максимум, что можно будет сказать о ней — она работа- ет корректно в таких-то условиях и при таких-то обстоятельствах. В программах по- 594 . Часть V. Разработка программ с помощью С
добного рода вы (как пассажир или программист) вряд ли захотели бы столкнуться с крушением (программы или самолета)! После программирования в течение нескольких лет вы заметите, что хотя боль- шинство отказов в работе программы происходят по причине самых разных ошибок программирования, само количество типов этих ошибок сравнительно невелико. На- пример, многие так называемые катастрофические программные сбои вызваны одной из следующих довольно частых ошибок: Какое-то условие привело к непредусмотренному выполнению бесконечного цикла; Были нарушены границы массива, что привело к повреждению примыкающего к нему кода программы или данных; Неожиданное переполнение при обработке данных определенного типа. Теоретически при наличии необходимого опыта программирования ошибок по- добного рода можно избежать в процессе тщательного продумывания и внимательной реализации проекта. (И в самом деле, профессионально написанные программы не должны содержать ошибок подобного рода.) Тем не менее, после первого этапа разработки программ часто появляются ошибки иного типа, проявляющиеся или во время заключительного процесса “тонкой на- стройки”, или на стадии сопровождения программы. Такие ошибки возникают из-за того, что одна функция по непредусмотренным причинам влияет на код или данные другой функции. Подобные ошибки обнаружить исключительно тяжело, поскольку код обеих функций может оказаться вполне корректным, а появление ошибки вызы- вает именно взаимодействие этих функций. Поэтому вполне естественно, что стрем- ление уменьшить вероятность появления катастрофических сбоев приводит к жела- нию сделать свои функции и их данные как можно более “пуленепробиваемыми”. Наилучший способ достижения этой цели состоит в том, чтобы код и данные каждой функции были скрыты как друг от друга, так и от остальной части программы. Методы сокрытия кода и данных во многом аналогичны механизму передачи сек- ретной информации только тем лицам, кому ее необходимо знать. Проще говоря, ес- ли какая-то функция не должна знать что-либо о другой функции или переменной, не предоставляйте этой функции возможности доступа к ним. Для этого необходимо придерживаться следующих четырех правил-принципов: 1. Каждый функциональный элемент должен иметь только одну точку входа и одну точку выхода; 2. Везде, где только возможно, не используйте глобальных переменных, а явно передавайте информацию функциям (например, посредством параметров); 3. В случаях, когда в нескольких зависимых функциях используются глобальные переменные, необходимо размещать как эти переменные, так и функции в обособленном файле. К тому же, в таких случаях глобальные переменные должны быть объявлены как static; 4. Каждая функция должна сообщать вызвавшей ее программе об успешном или аварийном завершении намеченной ей операции. То есть вызывающая программа должна распознавать признаки успешного или сбойного завершения функции. Правило 1 устанавливает, что каждая выполняемая функция имеет только одну точку входа и одну точку выхода. Это означает, что хотя в функциональный элемент может входить несколько функций, остальная часть программы взаимодействует толь- ко с одной из них. Обратимся к программе, обрабатывающей список рассылки; эта программа обсуждалась в предыдущих разделах. В ней предусмотрено выполнение се- ми функций. Можно поместить каждую функцию, вызываемую из соответствующего функционального поля, в свой собственный файл и компилировать их все раздельно друг от друга. Если все будет сделано надлежащим образом, то единственной точкой Глава 27. Проектирование программ с помощью С 595
входа и выхода каждого функционального элемента будет его функция наиболее вы- сокого уровня. А применительно к программе, обрабатывающей список рассылки, это означает, что такие высокоуровневые функции будут вызываться только функцией main (), тем самым будет предотвращено случайное разрушение одного функциональ- ного элемента другим. Эту ситуацию поясняет рис.27.1. Наилучший способ уменьшения вероятности появления побочных эффектов со- стоит в том, чтобы всегда явно передавать конкретной функции всю необходимую ей информацию. Правда, такое решение в некоторых случаях может ухудшить параметры производительности. Тем не менее, во всех случаях старайтесь избегать применения глобальных данных. Это уже правило 2, и если вы когда-нибудь писали крупные про- граммы с помощью стандартной версии BASIC (в которой все переменные глобаль- ные), то вы, наверное, уже понимаете важность соблюдения этого принципа. Правило 3 устанавливает, что в тех случаях, когда все же необходимо применить гло- бальные данные, то сами они и те функции, которые обращаются к ним, должны быть размещены в одном файле и компилироваться отдельно от остальной части приложения. Главный принцип здесь состоит в том, чтобы объявлять глобальные данные как static, тогда они будут доступны из других файлов. К тому же, функции, осуществляющие доступ к данным типа static, могут быть сами объявлены как static, что предохранит их от вызова функциями, которые не были объявлены в том же самом файле. Правило 4, попросту говоря, гарантирует, что программы получают “вторую по- пытку”, так как программа, вызвавшая определенную функцию, может приемлемым образом реагировать на ситуацию, в которой возникла ошибка. Например, допустим, что при выполнении функции, осуществляющей управление закрылками самолета, непредвиденно происходит выход за диапазон представимых значений. Но вы ведь не хотите, чтобы произошел отказ всей программы (а вместе с ней и авария самолета). Скорее вы предпочтете, чтобы программа узнала, что при выполнении данной функ- ции произошел отказ. Поскольку выход за границы диапазона может оказаться вре- менной ситуацией для программы, обрабатывающей данные в режиме реального вре- мени, то программа могла бы отреагировать на такую ошибку просто путем ожидания (простоя) в течение нескольких тактовых циклов, а затем попробовать повторно вы- полнить свою работу. Рис. 27.1. Каждый функциональный модуль имеет только одну точку входа Имейте в виду, что неукоснительное соблюдение этих правил может оказаться не- возможным в любой ситуации, но необходимо придерживаться этих принципов везде, где это только возможно. Подобный подход преследует цель максимизировать в соз- 596 Часть V. Разработка программ с помощью С
даваемой программе вероятность восстановления после сбойной ситуации, т.е. чтобы программа работала так, как если бы состояние ошибки не возникало. Читателям, проявляющим повышенный интерес к концепциям построения “пуленепробиваемых" функций, обязательно надо детально исследовать и по- экспериментировать с C++; он обеспечивает значительно более сильный ме- ханизм защиты, называемый инкапсуляцией, который еще больше уменьшает вероятность повреждения одной функции другой. Использование программы МАКЕ При создании больших программ разработчиков подстерегает ошибка иного рода, которая проявляется в основном на стадии разработки, но может завести такой проект почти в глухой тупик. Такая ошибка возникает в тех случаях, когда при компиляции и компоновке программы один или несколько файлов ресурсов оказываются устаревшими по сравнению с соответствующими объектными фай- лами. Если такое случится, то полученная в результате исполняемая программа не будет функционировать так, как предусмотрено в последней реализации исход- ного кода. Наверно каждый, кто когда-либо участвовал в создании или сопровож- дении большого программного проекта, сталкивался с подобной проблемой. Что- бы помочь избежать обескураживающих ошибок подобного типа, большинство компиляторов С имеют в своем составе утилиту под названием МАКЕ, которая помогает синхронизировать файлы ресурсов и объектные файлы. (Точное назва- ние МАКЕ-утилиты, соответствующей вашему компилятору, может немного от- личаться от МАКЕ, поэтому для большей уверенности проверьте себя по доку- ментации, прилагаемой к компилятору.) Программа МАКЕ автоматизирует процесс перекомпиляции крупных про- грамм, скомпонованных из нескольких файлов. Очень часто в процессе разработ- ки программы во многие файлы вносится масса незначительных изменений. По- сле этого программа повторно компилируется и тестируется. К сожалению, до- вольно легко забыть, какие именно файлы нуждаются в перекомпиляции. В такой ситуации вы можете или перекомпилировать все файлы и потерять массу време- ни, или случайно пропустить файл, который обязательно необходимо перекомпи- лировать. А это, в свою очередь, может повлечь за собой необходимость в допол- нительной, иногда многочасовой, отладке. Программа МАКЕ решает эту пробле- му посредством автоматической перекомпиляции только тех файлов, которые претерпели изменения. Примеры, приведенные в этом разделе, совместимы с программами МАКЕ, по- ставляемыми вместе с Microsoft C/C++. На сегодняшний день, Microsoft-версия про- граммы МАКЕ носит название NMAKE. Эти примеры будут также работать со мно- гими другими широко распространенными МАКЕ-утилитами, а общие концепции, описанные здесь, применимы для всех МАКЕ-программ. На заметку В последние годы программы МАКЕ стали очень изощренными. Примеры, при- веденные здесь, иллюстрируют только основные возможности МАКЕ. Стоит подробнее изучить утилиту МАКЕ, поддерживаемую вашим компилятором. Она может содержать такие функциональные особенности, которые окажут- ся исключительно полезными в вашей среде разработки. В своей работе утилита МАКЕ руководствуется сборочным файлом проекта или так называемым make-файлом, который содержит перечень выходных файлов (target files), зависимых файлов (dependent files) и команд. Для генерации выход- ного файла необходимо наличие файлов, от которых он зависит. Например, от Т.С Глава 27. Проектирование программ с помощью С 597
зависит файл T.OBJ, поскольку Т.С необходим для создания T.OBJ. В процессе работы утилиты МАКЕ производится сравнение даты выходного файла с датой файла, от которого он зависит. (В данном случае под термином “дата” подразуме- вается и календарная дата, и время.) Если выходной файл старше, т.е. имеет бо- лее позднюю дату создания, чем файл, от которого он зависит (или если выход- ной файл вообще отсутствует), выполняется указанная последовательность ко- манд. Если эта последовательность команд использует выходные файлы, при построении которых используются файлы, от которых они зависят, то при необ- ходимости модифицируются также и эти используемые файлы 1. Когда процесс МАКЕ завершится, все выходные файлы будут обновлены. Следовательно, в пра- вильно построенном сборочном файле проекта все исходные файлы, которые тре- буют компиляции, автоматически компилируются и компонуются, образуя новый исполняемый модуль. Таким образом, данная утилита следит, чтобы все измене- ния в исходных файлах были отражены в соответствующих им объектных файлах. В общем виде make-файл выглядит следующим образом: target_filel: dependent_file list command_sequenсe target_file2: dependent_file list command_sequence target_file3: dependent_file list command_sequence target_fileN: dependent_file list command_sequence Имя выходного файла должно начинаться в крайней левой позиции; за ним долж- но следовать двоеточие и список файлов, от которых он зависит. Последовательность команд, соответствующая каждому выходному файлу, должна предваряться как мини- мум одним пробелом или одним знаком табуляции. Перед комментариями должен стоять знак #, а сами комментарии могут следовать за списком зависимых файлов и/или последовательностью команд. Кроме того, их можно написать в отдельных строках. Спецификации выходных файлов должны отделяться друг от друга по край- ней мере одной пустой строкой. Самое главное при работе с make-файлом —учитывать следующую особенность: выполнение make-файла заканчивается сразу после того, как удастся обработать пер- вую же цепочку зависимостей. Это означает, что необходимо разрабатывать свои make-файлы таким образом, чтобы зависимости составляли иерархическую структуру. Запомните, что ни одна зависимость не будет считаться успешно обработанной до тех пор, пока все подчиненные ей зависимости (т.е. зависимости более низкого уровня) не будут разрешены. Чтобы лучше понять, как работает утилита МАКЕ, давайте рассмотрим очень про- стую программу. Она состоит из четырех файлов под названием TEST.H, TEST.С, TEST2.C и TEST3.C. Рис. 27.2 иллюстрирует данную ситуацию. (Чтобы лучше понять, о чем идет речь, введите каждую часть программы в указанные файлы.) 1 Все происходит как при вычислении сложной функции: сначала вычисляются аргументы, от которых она зависит, а если они, в свою очередь, являются сложными функциями, то при необходимости сначала вычисляются их аргументы и т.д. — Прим. ред. 598 Часть V. Разработка программ с помощью С
TEST.H: extern int count; TEST.C: #include <stdio.h> void test2(void), test3(void); int count = 0; int main(void) { printf (’’count = %dn", count); test2 ( ); printf("count = %dn", count); test3 ( ); printf("count = %dn", count); return 0; } TEST2.C: #include <stdio.h> #include "test.h" void test2(void) { count = 30; } TEST3.C: #include <stdio.h> #include "test.h" void test3(void) { count = -100; } Puc. 27.2. Простая программа, состоящая из четырех файлов Если в своей работе вы используете Visual C++, следующий make-файл перекомпили- рует данную программу после того, как вы внесете в них какие-нибудь изменения: test.exe: test.h test.obj test2.obj test3.obj cl test.obj test2.obj test3.obj test.obj: test.c test.h cl — c test.c test2.obj: test2.c test.h cl —c test2.c Глава 27. Проектирование программ с помощью С 599
Itest3.obj: test3.c test.h I cl -c test3.c По умолчанию программа MAKE выполняет директивы, содержащиеся в файле под названием MAKEFILE. Однако, как правило, разработчики предпочитают приме- нять другие имена для своих сборочных файлов проекта. Задать другое имя make- файла можно с помощью опции -f в командной строке. Например, если упоминав- шийся ранее make-файл называется TEST, то, чтобы с помощью программы NMAKE фирмы Microsoft скомпилировать необходимые модули и создать исполняемый мо- дуль, в командной строке следует набрать нечто подобное следующей строке: | шпаке -f test (Эта команда подходит для программы NMAKE фирмы Microsoft. Если вы поль- зуетесь другой утилитой МАКЕ, возможно, придется использовать другое имя опции.) Очень большое значение в сборочном файле проекта имеет очередность специфи- каций, поскольку, как уже упоминалось ранее, МАКЕ прекращает выполнение со- держащихся в файле директив сразу после того, как она полностью обработает первую зависимость1. Например, допустим, что ранее упоминавшийся make-файл был изме- нен, так что он стал выглядеть следующим образом: # Это неправильный make-файл. test.obj: test.с test.h cl -с test.с test2.obj: test2.c test.h cl —c test2.c test3.obj: test3.c test.h cl -c test3.c test.exe: test.h test.obj test2.obj test3.obj cl test.obj test2.obj test3.obj Теперь работа будет выполнена неправильно, если файл TEST.H (или любой дру- гой исходный файл) будет изменен. Это происходит потому, что последняя директива (которая создает новый TEST.EXE) больше не будет выполняться. Использование макросов в МАКЕ МАКЕ позволяет определять макросы в make-файле. Имена этих макросов явля- ются просто метками-заполнителями информации, которая в действительности будет определена или в командной строке, или в макроопределении из make-файла. Общая форма определения макроса следующая: имя_макроса ^определение Если в макроопределении используется символ пробела, то такое определение сле- дует заключить в двойные кавычки. После определения макроса его можно использовать в make-файле следующим образом: $(имя_макроса) 1 Т.е. как только построит первый зависимый файл. — Прим. ред. 600 Часть V. Разработка программ с помощью С
Вместо каждого вхождения такого оператора подставляется его макроопределение. Например, в следующем make-файле макрос LIBFIL позволяет указать редактору свя- зей библиотеку: I LIBFIL = grapics.lib prog.exe: prog.obj prog2.obj prog3.obj cl prog.obj prog2.obj prog3.obj $(LIBFIL) Многие МАКЕ-программы имеют дополнительные функциональные возможности, поэтому очень важно внимательно познакомиться с документацией, поставляемой вместе с компилятором. Применение интегрированной среды разработки Большинство современных компиляторов поставляются в двух различных видах. Первый вид представляет собой автономный функционально-законченный компиля- тор, работающий в режиме командной строки. При работе с таким компилятором вначале программист с помощью отдельного редактора создает исходный текст про- грамм. Затем он компилирует свою программу, и, наконец, выполняет ее. Все эти действия выполняются как отдельные команды, вводимые программистом в команд- ной строке. Любая отладка или контроль исходных файлов (например, с помощью утилиты МАКЕ) также выполняются обособленно. Компилятор, работающий в режи- ме командной строки, является традиционной формой поставки компиляторов. Второй вид компиляторов входит в состав интегрированной среды разработки (IDE - integrated development environment), как например, интегрированная среда раз- работки Visual C++. Выполненный в таком виде компилятор интегрирован вместе с редактором, отладчиком и диспетчером (или менеджером) проектов (который заменя- ет самостоятельную утилиту МАКЕ), а также системой поддержки исполнения про- грамм. С помощью интегрированной среды разработки программист может редакти- ровать, компилировать и прогонять программы, не покидая интегрированной среды разработки. Когда впервые была выпущена интегрированная среда разработки, она представляла собой этакого неуклюжего и громоздкого монстра, работать с которым было весьма утомительно. Тем не менее, сегодня интегрированные среды разработки, выпускаемые основными производителями компиляторов, могут предложить про- граммистам очень широкие возможности. Если вы не поленитесь заняться установкой параметров интегрированной среды разработки, чтобы они оптимально соответствова- ли вашим потребностям, то обнаружите, что применение именно такой интегриро- ванной среды значительно упрощает процесс разработки. Естественно, применяете ли вы интегрированную среду разработки или традици- онно набираете все команды в командной строке — это дело вашего личного вкуса. Если вам нравится набирать все команды в командной строке — это тоже будет пра- вильно. К тому же, одно из достоинств этого традиционного подхода заключается в том, что вы лично сами можете выбирать для работы любое инструментальное средст- во, а не довольствоваться тем, что предложит вам интегрированная среда разработки. Глава 27. Проектирование программ с помощью С 601
Полный справочник по Глава 28 Производительность, переносимость и отладка
Умение писать программы, которые эффективно используют ресурсы системы, лег- ко переносятся в другую среду и в которых отсутствуют ошибки, — вот отличи- тельный признак профессионального программиста. И кстати, это именно те вопро- сы, при решении которых информатика, как научная дисциплина, превращается в “искусство программирования”. В этой главе мы рассмотрим некоторые методы, ко- торые помогают наделить программы столь желанными свойствами. И Эффективность В данном разделе рассматривается несколько методов, которые помогут повысить эф- фективность разрабатываемых программ. В программировании под термином “эффективность”, как правило, подразумеваться скорость выполнения программы, а также показатели использования ресурсов системы, или и то, и другое вместе. Понятие ресурсов системы охватывает такие параметры, как объем памяти, дискового пространства, процес- сорное время и тому подобное — в основном все, что можно распределять и расходовать. Ответ на вопрос, является ли некоторая программа эффективной или нет, иногда носит субъективный характер, ведь суждения об эффективности могут меняться в зависимости от конкретной ситуации. Например, методы программирования, которые использовались при создании программы, ориентированной на работу с пользователем (к примеру, текстового процессора), могут совершенно не подходить для фрагмента системного кода, например, сетевого маршрутизатора. Эффективность часто предполагает компромиссы. Например, стремление заставить программу выполняться быстрее часто приводит к увеличению ее размеров. Такая взаимо- связь особенно сильно проявляется в тех случаях, когда используется линейный код с це- лью исключения накладных расходов на вызов функций. С другой стороны, желание уменьшить размер программы за счет замены линейного кода вызовами функций иногда приводит к замедлению выполнения программы. Аналогичная ситуация складывается и в отношении эффективного использования дискового пространства. Достижение этой цели часто подразумевает более компактное представление данных, которое может замедлить доступ к данным, из-за накладных расходов, вызванных дополнительной обработкой про- цессором таких данных. Такие и подобные им компромиссы эффективности могут вызвать чувство полного разочарования и неудовлетворенности, особенно среди неспециалистов и конечных пользователей, которые не могут понять, почему одно влияет на другое. К сча- стью, существует несколько методов, которые могут одновременно и ускорить выполнение программы, и уменьшить ее размер. Язык программирования С позволяет эффективно оптимизировать быстродействие или размер программы. В следующих разделах рассказано о нескольких технических приемах оптимизации, уже получивших весьма широкое распространение, хотя творческий, ини- циативный программист, без сомнения, обязательно найдет новые решения. Операции увеличения и уменьшения Уже стало традицией при обсуждении эффективности использования языка С поч- ти всегда начинать с операторов увеличения (инкремента) и уменьшения (декремента). В некоторых ситуациях применение этих операторов помогает компиля- тору сгенерировать более эффективный объектный код. Рассмотрим, например, сле- дующую последовательность операторов: /* первый метод */ а = а + Ь ; b = b + 1 ; /* второй метод */ а = а + Ь++ ; 604 Часть V. Разработка программ с помощью С
В обоих вариантах приведенного примера программы переменной а присваивается значение суммы а+Ь, а затем значение Ь увеличивается на единицу. Однако зачастую вто- рой метод приводит к более эффективной программе, поскольку компилятор имеет воз- можность избежать выполнения дополнительных (а значит, избыточных) инструкций за- грузки и записи в память при обращении к переменной Ь. Другими словами, Ь не надо будет загружать в регистр процессора дважды — в первый раз при суммировании с пере- менной а, а второй раз при ее инкрементировании. Хотя некоторые компиляторы будут автоматически оптимизировать оба варианта, и сгенерируют одинаковый объектный код, это не может считаться само собой разумеющимся для всех остальных случаев. В общем случае, аккуратное использование операторов ++ и — может увеличить скорость выполне- ния программы, и в то же время уменьшить ее размер. Поэтому, старайтесь находить именно такие решения, в которых используются эти операторы. Применение регистровых переменных Один из наиболее эффективных методов повышения скорости выполнения про- граммного кода состоит в применении к переменным спецификатора класса памяти register. Хотя применение регистровых переменных эффективно и в других случа- ях, они исключительно хорошо подходят для управления циклом. Ведь регистровые переменные хранятся так, что для обращения к ним требуется минимальное время. Целочисленные переменные, к которым применен спецификатор класса памяти reg- ister, хранятся, как правило, в регистре центрального процессора. Это имеет огром- ное значение, поскольку скорость выполнения критически важных циклов програм- мы, часто определяет итоговую скорость ее выполнения. Например: I for(i=0; i<MAX; i++) { I /* что-нибудь выполнить */ | } В этом фрагменте осуществляется многократная проверка и установка нового зна- чения переменной i. В частности, всякий раз, когда выполняется цикл, значение i проверяется, и если оно не достигло конечного значения, то инкрементируется. По- скольку этот процесс повторяется много раз, время обращения к i оказывает сущест- венное влияние на скорость выполнения всего цикла. Однако не только управляющие переменные циклов, но любые другие перемен- ные, используемые внутри тела цикла, могут заслуживать модификации с помощью спецификатора класса памяти register. Например: I for(i=0; i<MAX; i++) { I sum = a + b; I /*...*/ I } В данном примере при каждом повторном выполнении тела цикла осуществляется об- ращение к переменным sum, а и b. К тому же, при каждой итерации цикла переменной sum присваивается новое значение. Таким образом, время, требуемое для обращения к этим переменным, также влияет на общую производительность приведенного цикла. Спецификатор класса памяти register можно применить к любому количеству переменных. Однако ограничения, обусловленные архитектурой центрального про- цессора, приведут к тому, что в пределах одной функции большинство компиляторов смогут оптимизировать время доступа всего лишь для нескольких переменных. В об- щем случае всегда можно рассчитывать на то, что в любой момент времени в регист- рах центрального процессора обязательно найдется место, по крайней мере, для двух целочисленных переменных. Дополнительно можно использовать и другие виды бы- строй памяти, например кэш-память, но возможности ее применения также ограни- Глава 28. Производительность, переносимость и отладка 605
чены. Поскольку.применить оптимизацию к каждой переменной, модифицированной с помощью спецификатора класса памяти register, скорее всего, будет невозможно, язык С позволяет компилятору игнорировать спецификатор класса памяти register и просто разрешает использовать переменную обычным способом. К тому же это пра- вило позволяет код, созданный для одной среды, скомпилировать для другой среды, в которой емкость запоминающего устройства с быстрой выборкой меньше. Поскольку объем запоминающего устройства с быстрой выборкой всегда ограничен, лучше тща- тельно отбирать только те переменные, реализация быстрого доступа к которым в максимальной степени оптимизирует программу. Указатели вместо индексации массива В некоторых случаях индексацию массива целесообразно заменить арифметиче- скими операциями над указателями (например, приращением). Такая замена может привести к уменьшению размера кода и повышению его быстродействия. Например, рассмотрим следующие два фрагмента кода, которые выполняют одну и ту же работу: Индексация массива Приращение указателя р=аггау; f or (; ;) { for(;;) { a=array[t++]; a=*(p++); } } Преимущество метода указателей состоит в следующем. Вначале указателю р при- сваивается адрес переменной array, тогда для обработки очередного элемента масси- ва необходимо выполнять всего лишь операцию приращения указателя при каждой последующей итерации цикла. Как бы то ни было, при индексации массива всегда нужно вычислять индекс элемента массива, используя значение t, а это — более сложная и трудоемкая задача. Будьте внимательны. В случаях, когда индекс массива вычисляется с помощью сложной формулы, или когда действия над указателями “затеняют” смысл програм- мы, необходимо использовать индексацию массива. Как правило, лучше слегка сни- зить производительность работы программы, чем пожертвовать ее четкостью и ясно- стью. К тому же, разница в производительности между индексацией массива и вычис- лением указателя может оказаться незначительной в случае использования оптимизирующих компиляторов или в случае, если программа предназначена для ра- боты в различных средах или с различными процессорами. Применение функций Помните в любой ситуации, что применение автономных функций вместе с ло- кальными переменными помогает формировать фундамент для структурированного программирования. Функции являются строительным блоками в С-программах и од- ними из самых сильных сторон и главных достоинств С. И только исключительно с этой позиции (и никак иначе!) необходимо рассматривать дальнейший материал этого раздела. Сделав это строгое предупреждение, можно обратить внимание на некоторые, часто используемые при оптимизации программ, особенности С-функций и их разно- видностей, связанные со скоростью и размером программного кода. При компиляции функции для хранения передаваемых функции параметров (если они, конечно, имеются), а также любых локальных переменных, используемых этой функцией, компилятор С использует стек. А когда происходит вызов функции, то в 606 Часть V. Разработка программ с помощью С
стек помещается еще и адрес возврата в вызывающую программу. (Это позволяет про- должить выполнение подпрограммы с того места, из которого была вызвана функция. Точнее, с команды, следующей за вызовом функции.) Когда функция возвращает управление, этот адрес и все локальные переменные и параметры будут удалены из стека. Процесс заталкивания (записи в стек) этой информации обычно называют вы- зывающей последовательностью (calling sequence), а процесс выталкивания данных из стека называется возвращающей последовательностью (returning sequence)1. Эти последо- вательности выполняются в течение определенного промежутка времени, иногда до- вольно значительного. Чтобы лучше понять, каким образом вызов функции может замедлить программу, рассмотрим приведенные ниже два фрагмента программного кода: Вариант 1 Вариант 2 for(x=l; х<100; ++х) { for(x=l; х<100; ++х) { t=compute(х); t=fabs(sin(х)/100/3.1416) ; } } double compute(int q) { return fabs(sin (q)/100/3.1416); } Хотя в каждом из циклов осуществляется одна и та же операция, Вариант 2 вы- полняется быстрее, потому что благодаря применению линейного кода были исклю- чены накладные расходы на выполнение вызывающей и возвращающей последова- тельностей. (Другими словами, код, реализующий функцию compute (), просто дуб- лируется внутри цикла, а не вызывается.) Чтобы лучше понять, в чем состоят непроизводительные издержки, связанные с вызовом функций, давайте рассмотрим ассемблерный код инструкций, которые необ- ходимы для вызова и возврата из функции. Как вам, наверное, известно, многие С- компиляторы имеют специальную опцию для создания файла с ассемблерным кодом; при использовании этой опции создается ассемблерный, а не объектный код. Приме- нение этой опции позволяет исследовать код, созданный компилятором. В последую- щем примере мы исследуем файлом с ассемблерным кодом, созданным с помощью Visual C++ при включенной опции -Fa. Мы внимательно рассмотрим этот файл, что- бы воочию убедиться в том, какой код генерируется для вызывающих и возвращаю- щих последовательностей. Возьмем следующую программу: int max(int a, int b); int main(void) { int x; x=max(10, 20) ; return 0; int max(int a, int b) { return a>b ? a : b; 1 Имеются в виду, конечно, последовательности команд. — Прим. ред. Глава 28. Производительность, переносимость и отладка 607
Для нее получится следующий ассемблерный код. Вызывающие и возвращающие последовательности отмечены в листинге комментариями, начинающимися со знака звездочка (*); эти комментарии добавлены автором книги. Как вы можете убедиться, вызывающие и возвращающие последовательности занимают по объему довольно зна- чительную часть программного кода. TITLE .386P test.c include listing . inc if @Version gt 510 .model FLAT else _TEXT SEGMENT PARA USE32 PUBLIC ’CODE’ _TEXT ENDS _DATA SEGMENT DWORD USE32 PUBLIC ’DATA’ _DATA ENDS CONST SEGMENT DWORD USE32 PUBLIC ’CONST’ CONST ENDS _BSS SEGMENT DWORD USE32 PUBLIC ’BSS’ _BSS ENDS _TLS SEGMENT DWORD USE32 PUBLIC ’TLS’ _TLS ENDS FLAT GROUP _ DATA, CONST, _BSS ASSUME CS: FLAT, DS: FLAT, SS: FLAT endif PUBLIC _max PUBLIC _main _TEXT SEGMENT _x$ = -4 _main PROC NEAR ; File ex2. c ; Line 4 push ebp mov ebp, esp push ecx ; Line 7 * * * * ********* ***************************************** ; Это : начало вызывающей последовательности. . * * * * ********* ***************************************** push 20 ; 00000014H push 10 ; OOOOOOOaH call _max . * * * * ********* **************************************** r • * * * * ★★★★★★★★★ **************************************** ; Следующая строка относится к возвращающей последовательности. • ★ ★ ★ ★ ********* **************************************** add esp, 8 mov DWORD PTR _x$[ebp], eax ; Line 9 xor eax, eax ; Line 10 mov esp, ebp POP ebp ret 0 _main ENDP а$ = 8 • _b$ = 12 608 Часть V. Разработка программ с помощью С
__max PROC NI l AR ; Line 13 • ★★★★★★★★★★★★j t******************************* ********* ; Дополнительный фрагмент вызывающей последовательности t******************************* ********* push mov push ebp ebp, esp ecx k******************************* ; Line 14 mov cmp jle mov mov jmp $L48: mov mov $L49: eax, DWORD PTR _a$[ebp] eax, DWORD PTR _b$[ebp] SHORT $L48 ecx, DWORD PTR _a$[ebp] DWORD PTR -4+[ebp], ecx SHORT $L49 edx, DWORD PTR _b$[ebp] DWORD PTR -4+[ebp], edx ; Возвращающая последовательность. ********* mov ; Line 15 mov Pop ret _max ENDP TEXT ENDS eax, DWORD PTR -4+[ebp] esp, ebp ebp 0 END Реально получаемый код зависит от особенностей реализации конкретного компи- лятора и типа используемого процессора, но в основном он будет похож на приведен- ный выше листинг. Однако из приведенных рассуждений совсем не следует, что с целью сокращения времени выполнения в программах нужно использовать всего лишь несколько, но за- то громадных функций. Ведь это далеко не самый лучший стиль программирования. Во-первых, в абсолютном большинстве случаев незначительный выигрыш во времени, получаемый за счет отказа от вызова функций, не имеет столь большого значения по сравнению с весьма ощутимой потерей структурированности. К тому же, только этой проблемой дело не ограничивается. Замена функций, которые используются несколь- кими подпрограммами, линейным кодом приводит к непомерному “разрастанию” программы, поскольку одинаковый код повторяется несколько раз. Не забывайте, что подпрограммы были созданы отчасти для того, чтобы более эффективно использовать память. Фактически именно поэтому считается, что уменьшение времени выполнения программы ведет к увеличению ее размеров. И наоборот, уменьшение размера про- граммы обычно приводит к замедлению скорости ее выполнения. Чтобы применить линейный код как метод ускорения программы, в компиляторе, со- вместимом с С99, предусмотрено зарезервированное слово inline для создания встраи- ваемых функций (при компиляции вызов такой функции заменяется ее кодом). Используя это слово, вам не придется вручную каждый раз дублировать исходный код каждой функ- ции. Если же ваш компилятор со стандартом С99, то чтобы получить аналогичный резуль- тат, следует использовать макросы с параметрами в тех местах, где имеется такая возмож- ность. Естественно, макросы с параметрами не предоставляют той гибкости, которой обла- дают функции, в описании которых применяется слово inline. Глава 28. Производительность, переносимость и отладка 609
Н Перенос программ Очень часто (и это стало обыденной практикой) программы, написанные для од- ной машины, необходимо перенести на другой компьютер, оснащенный другим про- цессором или другой операционной системой, а зачастую и тем и другим одновре- менно. Этот процесс носит название перенос, или перенесение (porting), и в одних слу- чаях он может оказаться очень простым, а в других — предельно трудным. Это зависит от того, каким образом была первоначально написана программа. Поэтому, программа, которая легко поддается переносу, называется переносимой, мобильной, или машинонезависимой1 (portable). Если программа не относится к разряду переносимых, как правило, это объясняется тем, что она содержит большое количество элементов, зависящих от типа машины, — то есть, она имеет фрагменты кода, которые будут вы- полняться только в одной определенной операционной системе или на одном кон- кретном процессоре. Язык С позволяет создавать переносимый код, но для достиже- ния этой цели необходимо проявлять особую тщательность и внимание к деталям. В данном разделе рассматриваются несколько конкретных проблем, возникающих при создании машинонезависимых программ и предлагается ряд решений таких проблем. Использование #define Возможно, самый простой и эффективный способ сделать программы переноси- мыми состоит в том, чтобы представить каждое зависящее от типа системы или про- цессора “магическое число” макросом ttdefine. Магическими эти числа были назва- ны потому, что они представляют собой такие параметры системы, как размер буфе- ра, используемого для обращения к диску, специальные команды управления экраном и клавиатурой, а также информацию о распределении памяти, иными словами, все то, что может измениться при переносе программы. Такие # de fine-определения не только делают все магические числа очевидными для программиста, выполняющего перенос, но к тому же упрощают выполнение всей работы. Ведь поскольку их значе- ния необходимо будет изменить только однажды и в одном месте, следовательно, не придется “перетряхивать” всю программу. Например, рассмотрим оператор fread(), который по своей природе является не- переносимым: | fread(buf, 128, 1, fp); В данном случае проблема заключается в том, что размер буфера (число 128) является жестко запрограммированным параметром функции fread(). Это зна- чение может вполне подходить для одной операционной системе, но окажется меньше оптимальной величины для другой. А вот более удачный способ кодиро- вания этой же функции: |#define BUF_SIZE 128 fread(buf, BUF_SIZE, 1, fp) ; В последнем варианте при переносе на другую систему понадобится всего лишь изменить определение #define — и все ссылки на BUF_SIZE будут исправлены авто- матически. Это не только облегчает процесс внесения изменений, но и вдобавок убе- регает от массы ошибок при редактировании. Помните, что в реальной программе может присутствовать бездна ссылок на BUF_SIZE, поэтому переносимость програм- мы возрастает многократно. 1 Термины портабильность и портабильный к настоящему времени несколько утратили свою первоначальную популярность. — Прим. ред. 610 Часть V. Разработка программ с помощью С
Зависимость от операционной системы Код практически всех коммерческих программ адаптирован для конкретной опера- ционной системы, под управлением которой предполагается их работа. К примеру, программа, написанная для Windows 2000, может использовать многопотоковую много- задачность, тогда как программа, написанная для 16-разрядной Windows 3.1, не может. Дело в том, что определенная привязка к особенностям конкретной операционной сис- темы совершенно необходима, чтобы получить по-настоящему хорошие, быстродейст- вующие и по-коммерчески жизнеспособные программы. Однако с другой стороны, за- висимость от операционной системы усложняет процесс переноса программ. Хотя и не существует жестких правил, следуя которым можно было бы минимизи- ровать зависимость разрабатываемых программ от типа операционной системы, по- звольте предложить маленький совет. Отделяйте в разрабатываемой программе те час- ти, которые относятся непосредственно к приложению от тех фрагментов, которые осуществляют взаимодействие с операционной системой. Тогда при переносе про- граммы в новую среду потребуется изменить только модули интерфейса. Различия в размерах данных Если вы хотите написать переносимый код, никогда не следует полагаться на ожи- даемые размеры данных. Например, надо всегда учитывать отличия между 16-разрядной и 32-разрядной средами. Размер слова в 16-разрядном процессоре равен 16 битам, а в 32-разрядном процессоре — 32 битам. Поскольку размер слова часто совпадает с разме- ром данных типа int, то код, созданный в предположении, что переменные типа int являются, к примеру, 16-разрядными, не будет корректно работать после переноса его в 32-разрядную среду. Чтобы избежать жесткой привязки к размеру, там, где программе понадобятся сведения о количестве байтов, составляющих какую-нибудь величину, обя- зательно используйте оператор sizeof. Например, следующее выражение заносит зна- чение типа int в дисковый файл и будет работать в любой среде: | fwrite(&i, sizeof(int), 1, stream); И Отладка Перефразировав Томаса Эдисона, можно утверждать, что программирование на 10 процентов состоит из вдохновения и на 90 процентов из отладки. Все действительно квалифицированные программисты являются хорошими отладчиками. Чтобы нау- читься предотвращать множество ошибок, целесообразно рассмотреть некоторые до- вольно распространенные действия, которые могут привести к их появлению. Ошибки очередности вычисления В большинстве С-программ применяются операторы инкрементирования и декре- ментирования, а порядок следования этих операторов, как вы помните, имеет боль- шое значение в зависимости от того, предшествуют они или следуют за переменной. Рассмотрим следующий случай: |у=10; у=10; х=у++; х=++у; Приведенные две последовательности не эквивалентны. Та, что слева, присваивает пе- ременной х значение 10, а затем инкрементирует у. В другой же последовательности (справа) у сначала инкрементируется, и в результате этого становится равным 11, и только Глава 28. Производительность, переносимость и отладка 611
затем значение 11 присваивается переменной х. Таким образом, в первом случае х равно 10, а во втором случае х — 11. В общем случае в сложных выражениях префиксная опера- ция инкрементирования (или декрементирования) осуществляется перед вычислением значения операнда, используемого в последующих действиях. Постфиксный инкремент (или декремент) выполняется в сложных выражениях после того, как значение операнда вычислено. Если забыть об этих правилах, проблем не миновать. Путь, обычно ведущий к возникновению ошибки очередности вычисления, заклю- чается в изменении имеющейся последовательности операторов. Например, при оп- тимизации фрагмента кода вы могли бы изменить следующую последовательность: I /* первоначальный код */ I х = а + Ь; I а = а + 1; и представить ее в таком виде: I/* "усовершенствованный" код - ошибка! ★/ х = ++а + Ь; Проблема заключается в том, что эти два фрагмента кода не дают одинаковый ре- зультат. Причина состоит в том, что второй способ инкрементирует переменную а до того, как она суммируется с Ь. А такое действие в первоначальном варианте не было предусмотрено! Подобные ошибки относятся к разряду трудно обнаруживаемых. Могут быть ключи- подсказки, например циклы, выполняющиеся неправильно, или процедуры, которые не работают из-за таких ошибок. Если у вас возникает сомнение в правильности оператора, перекодируйте его таким образом, чтобы быть уверенным в нем на все 100 процентов. Проблемы с указателями Очень распространенной ошибкой в С-программах является неправильное применение указателей. Проблемы с указателями условно можно разделить на две основные категории: неправильное представление об использовании косвенной адресации и об операциях над указателями вообще, а также случайное (точнее непредумышленное) применение недейст- вительных или неинициализированных указателей. Решить первую проблему несложно: просто разберитесь окончательно и до конца в том, что означают операторы * и &! Спра- виться со второй категорией проблем с указателями несколько сложнее. Ниже приведена программа, иллюстрирующая оба типа ошибок, связанных с ука- зателями: /* Эта программа содержит ошибку. */ #include <stdlib.h> #include <stdio.h> int main(void) { char *p; *p = (char *) malloc(100); /★ эта строка содержит ошибку */ gets(р); printf(р); return 0; } При запуске такой программы скорее всего произойдет ее сбой. Объясняется это тем, что значение адреса, возвращаемого функцией malloc О, не было присвоено указателю р, а было размещено в ячейку памяти, на которую указывает р, адрес кото- 612 Часть V. Разработка программ с помощью С
рой в данном случае неизвестен (и в общем случае, непредсказуем). Данный тип ошибки представляет пример фундаментального непонимания выполнения операто- ров над указателями (а именно выполнения операции *). Как правило, такая ошибка в программах на С допускается начинающими программистами, но иногда эта неле- пая оплошность встречается и у опытных профессионалов! Чтобы исправить эту про- грамму, необходимо заменить строку с ошибкой следующей корректной строкой: |р = (char *) malloc(100); /* эта строка правильная */ Кроме того, данная программа содержит еще одну, причем более коварную ошиб- ку. В ней отсутствует динамическая проверка значения адреса, возвращаемого функ- цией malloc (). Помните, если память будет исчерпана, malloc () возвратит значение NULL, а тогда указатель использовать нельзя. Использование NULL в качестве указателя объекта недопустимо и практически всегда ведет к аварийному завершению програм- мы. Вот исправленный вариант данной программы, в который включена проверка до- пустимости указателя: /* Теперь эта программа написана корректно. */ #include <stdio.h> #include <stdlib.h> int main(void) { char *p; p = (char *) malloc(lOO); /* эта строка не содержит ошибок */ if(!р) { printf("Out of memory - Нет памяти.n"); exit (1); } gets(p); printf(p); return 0; } Следующая часто встречающаяся ошибка заключается в том, что программист за- бывает инициализировать указатель перед использованием. Обратимся к следующему фрагменту программы: Iint *х; *х=100; Выполнение такого кода обязательно приведет к проблемам, поскольку указатель х не был инициализирован, а значит, едва ли можно ожидать, что он указывает туда, куда нужно. Фактически вы не знаете, куда указывает х. Присвоение какого-либо значения этой неизвестной области памяти может разрушить что-то, имеющее огром- ное значение, например другой фрагмент программы или данные. Самая большая неприятность с “дикими” (т.е. непредсказуемыми) указателями состоит в том, что их невероятно тяжело обнаружить. Если вы присваиваете значение посредством указателя, который не содержит действительный адрес, ваша программа в одних случаях может функционировать вполне корректно, а в других — завершаться аварийным отказом. Чем меньше размер программы, тем выше вероятность, что она будет работать правильно, даже с “блуждающим” указателем. Это объясняется тем, что в таком случае программой используется очень маленький объем памяти, поэтому довольно велики шансы того, что Глава 28. Производительность, переносимость и отладка 613
указатель-нарушитель указывает на неиспользуемую область памяти. Но по мере увеличе- ния объема программы подобные сбои будут происходить все чаще и чаще. Но вы скорее попытаетесь объяснить их последними внесенными в программу дополнениями или изме- нениями, и вряд ли свяжете с ошибками в использовании указателей. Следовательно, вы будете искать ошибки совершено не в том месте. Подсказкой для распознавания проблем с указателем является то, что такие ошибки часто проявляются нерегулярно и зачастую странными образом. Один раз программа рабо- тает вполне корректно, а другой раз — неправильно. Иногда некоторые переменные со- держат “мусор”, хотя на то нет каких бы то ни было видимых причин. Когда возникают подобные проблемы, проверьте все указатели. Собственно говоря, вы всегда должны про- верять все указатели, как только начнут проявляться любые ошибки1. Возможно, утешением, станет то, что хотя указатели могут доставить множество хлопот, тем не менее, они являются одним из наиболее мощных средств языка С и стоят преодоления любой проблемы, которую они могут вам преподнести. Просто по- старайтесь с самого начала изучить их правильное применение. Интерпретация синтаксических ошибок Время от времени вы будете сталкиваться с синтаксическими ошибками, сообще- ния о которых покажутся вам абсурдными и бессмысленными. То ли сообщение об ошибке зашифровано, то ли ошибка, описание которой приводится в сообщении, во- обще не похожа на ошибку. Тем не менее, в большинстве случаев в вопросах обнару- жения ошибок компилятор оказывается прав. Просто в подобных случаях сам текст сообщения об ошибке чуть-чуть не дотягивает до совершенства. При поиске причин необычных синтаксических ошибок, как правило, необходимо при чтении программы немного возвратиться назад. Поэтому, если вы столкнулись с сообщением об ошибке, которое, судя по всему, не имеет смысла, попробуйте поискать синтаксическую ошибку одной двумя строками выше по тексту вашей программы. С одной из особенно сногсшибательных ошибок можно познакомиться ближе, ес- ли вы попытаетесь скомпилировать следующий код: char *myfunc(void); int main(void) { /* ... */ } int myfunc(void) /* сообщение об ошибке указывает сюда */ { /*...*/ } 1 Вот еще несколько универсальных советов, как избавиться от проблем: “Всегда все тщательно проверяйте!”, “Никогда не делайте ошибок!”, “Всегда все кодируйте правильно!” и т.д. Когда я слышу подобные советы, я вспоминаю, как всем известная Алиса реагировала на подобные высказывания: “Но ведь я не могла!”, на что Шалтай-Болтай ей отвечал: “Я и не говорю, что ты могла; я говорю, что ты должна была!” Честно говоря, позиция Алисы мне очень и очень близка, ведь недаром же из- вестный философ Хинтикка доказал, что аморально требовать от человека то, чего он не может вы- полнить. Если теперь предположить (вопреки тому, что заказчики считают, что программисты обяза- ны быть роботами), что все программисты — люди, то мы можем придти к заключению, что не во всех программах всегда проверяется применение всех указателей. (С точки зрения заказчиков это, ко- нечно, недопустимо.) А потому совет, даваемый автором, хотя и смахивает на один из универсальных, вполне заслуживает того, чтобы прислушаться к нему. Статистика же подтверждает, что это — самый лучший совет из всех, которые можно дать при отладке программ с указателями! Так что ничего не поделаешь — не ленитесь! — Прим. ред. 614 Часть V. Разработка программ с помощью С
Ваш компилятор выдаст сообщение об ошибке вместе с таким вот разъяснением: IType mismatch in redeclaration of myfunc(void) (Несоответствие типов при повторном объявлении myfunc(void)) Это сообщение относится к строке листинга программы, которая помечена ком- ментарием о наличии ошибки. Как такое возможно? Ведь в этой строке нет двух функций myfunc (). А разгадка состоит в том, что прототип в верхней строке про- граммы показывает, что myfunc () возвращает значение типа указатель на символ. Это ведет к тому, что в таблице идентификаторов компилятор заполняет строку, со- держащую эту информацию. Когда затем в программе компилятор встречает функцию myfunc (), то теперь тип результата указывается как int. Следовательно, вы “повторно объявили”, другими словами “переопределили” функцию. Другая синтаксическая ошибка, которую трудно сразу правильно истолковать, ге- нерируется при попытке скомпилировать следующий код: /* В тексте данной программы имеется синтаксическая ошибка */ #include <stdio.h> void fund (void) ; int main(void) { fund () ; return 0; } void fund (void) ; { printf("Это в funcl.n"); } Здесь ошибка состоит в наличии точки с запятой после определения функции fund (). Компилятор будет рассматривать это как выражение, находящееся за преде- лами какой бы то ни было функции, что является ошибкой. Однако различные ком- пиляторы по-разному сообщают об этой ошибке. Некоторые компиляторы выводят в сообщении об ошибке такой текст: bad declaration syntax (неправильный син- таксис объявления), и в то же время указывают на первую открытую скобку после функции fund (). Поскольку вы привыкли в конце выражений ставить точку с запя- той, подобную ошибку очень трудно заметить. Ошибки, вызванные “потерей” единицы Как известно, в С нумерация индексов любого массива начинаются с нуля. Тем не менее, даже опытные профессионалы в пылу творческого вдохновения, бывало, забы- вали это общеизвестное правило! Рассмотрим следующую программу, которая, как предполагается, должна инициализировать массив из ста целых чисел: /* Эта программа работать не будет. */ int main(void) { int x, num[100]; for(x=l; x <= 100; ++x) num[x] = x; return 0; } Глава 28. Производительность, переносимость и отладка 615
Цикл for в этой программе выполнен неправильно по двум причинам. Во-первых, он не инициализирует num [ 0 ], первый элемент массива num. Во-вторых, о’н пытается проинициализировать элемент массива с номером на единицу больше, чем у послед- него элемента массива, поскольку num [99] как раз и является последним элементом массива, а параметр цикла достигает значения 100. Правильно было бы записать эту программу следующим образом: /* Здесь все правильно. */ int main(void) { int x, num[100]; for(x=0; x < 100; ++x) num[x] = x; return 0; } Помните, в массиве из 100 элементов элементы пронумерованы числами от 0 до 99. Ошибки из-за нарушения границ И в среде прогона программ, написанных на языке С, и во многих стандартных биб- лиотечных функциях почти не имеется (а иногда они вообще отсутствуют) средств дина- мической проверки принадлежности к диапазону (т.е. средств контроля границ). Напри- мер, если в программе произойдет выход за границы массива, то такая ошибка может ос- таться незамеченной. Рассмотрим следующую программу, которая должна считывать строку символов из буфера клавиатуры и отображать ее на экране монитора: #include <stdio.h> int main(void) { int varl; char s [10]; int var2; varl = 10; var2 = 10; gets (s) ; printf("%s %d %d”, s, varl, var2); return 0; } В этом фрагменте нет очевидных ошибок кодирования. Тем не менее, вызов функции gets () с параметром s может косвенно привести к ошибке. В данной программе пере- менная s объявлена как массив символов (строка длиной в 10 знаков). Но что произойдет, если пользователь введет больше десяти знаков? Это приведет к выходу за границы масси- ва s, и значение переменной varl или var2, а возможно, и их обеих будет перезаписано. Следовательно, varl и (или) var2 не будут содержать правильных значений. Это вызвано тем, что для хранения локальных переменных все С-компиляторы применяют стек. Пере- менные varl, var2, а также s могут располагаться в памяти так, как показано на рис. 28.1. (Ваш компилятор С может поменять порядок следования переменных varl, var2 и s.) Предположим, что порядок распределения ячеек памяти совпадает с изображен- ным на рис. 28.1. Тогда, если произойдет выход за границы массива s, то дополни- тельные (лишние) символы будут помещены в область, в которой должна находиться переменная var2. Это практически уничтожит информацию, ранее записанную там. 616 Часть V. Разработка программ с помощью С
Поэтому на экран будет выведено не число 10 в качестве значения обеих целых пере- менных, а в качестве значения переменной, поврежденной в результате выхода за границы массива з, будет отображено что-нибудь другое. А вы можете искать ошибку совсем в другом месте. Младшие адреса памяти — 2 байта — 10 байтов 2 байта Старшие адреса • памяти • Рис. 28.1. Размещение в памяти переменных varl, var2 и s (исходя из предположения, что на целое число выделяется 2 байта) В рассмотренной программе потенциальная ошибка из-за выхода за границы мо- жет быть исключена за счет применения функции fgets () вместо gets (). Функция fgets () предоставляет возможность устанавливать максимальное количество считы- ваемых символов. Единственная проблема состоит в том, что fgets () считывает и сохраняет еще и символ разделителя строк, поэтому в большинстве приложений его необходимо будет удалять. Пропуск прототипов функций В современной среде программирования отсутствие даже одного прототипа функ- ции является непростительным упущением, “отступничеством” от мудрых принципов и здравого смысла. Чтобы понять почему именно, рассмотрим следующую программу, которая выполняет умножение двух чисел с плавающей запятой: /* Эта программа содержит ошибку. */ #include <stdio.h> int main(void) { float x, y; Глава 28. Производительность, переносимость и отладка 617
scanf(&x, &y); printf("%f"r mul(x, y)); return 0; } double mul(float a, float b) { return a*b; } В данном случае, поскольку прототип функции mul () отсутствует, при компиля- ции функции main () предполагается, что в результате выполнения mul () будет воз- вращена целочисленная величина. Но в действительности mul () возвращает число с плавающей запятой. Допустим, что для целого числа выделяется 4 байта, а для чисел двойной точности (double) — 8 байтов. Это значит, что в действительности только четыре байта из восьми, необходимых для двойной точности, будут использованы в операторе printf () внутри функции main(). Это приведет к неправильному ответу, выводимому на экран дисплея. Чтобы исправить эту программу, достаточно создать прототип функции mul (). Корректный вариант программы будет выглядеть следующим образом: /* Это правильная программа. */ tfinclude <stdio.h> double mul(float a, float b); int main(void) { float x, y; scanf("%f%f", &x, &y) ; printf(”%f”, mul(x, y)); return 0; } double mul(float a, float b) { return a*b; } В данном случае прототип указывает, что при компиляции функции main () необ- ходимо учитывать, что функция mul () возвращает значение с удвоенной точностью. Ошибки при задании аргументов Тип любого формального параметра, должен соответствовать типу фактического параметра. Хотя благодаря прототипам функций компиляторы могут обнаруживать многие несоответствия типов аргументов (параметров), они не могут обнаружить все. Более того, когда функция имеет переменное количество параметров, компилятор не может обнаружить несоответствие их типов. Например, рассмотрим функцию scanf (), которая принимает большое количество разнообразных аргументов. Не за- бывайте, что scanf () ожидает принять адреса своих аргументов, а не их значения. И никакая сила не сделает за вас правильную подстановку. Например, следующая по- следовательность операторов 618 Часть V. Разработка программ с помощью С
I int x; scanf("%d", x); содержит ошибку, поскольку передается значение переменной х, а не ее адрес. Тем не менее, вызов этой функции scanf () будет скомпилирован без сообщения об ошибке, и лишь во время выполнения этого оператора выявится ошибка. Правильный вариант вызова функции scanf () приведен ниже: | scanf("%d", &х); Переполнение стека Все компиляторы С используют стек для хранения локальных переменных, адресов возврата и передаваемых функциям параметров. Однако стек не безграничен, и, в конце концов, может быть исчерпан. Тогда попытка записи очередного элемента в него приведет к переполнению стека. Когда такое происходит, программа или полно- стью “умирает”, или продолжает выполняться в ненормальном причудливом стиле. Самое неприятное в переполнении стека заключается в том, что оно в большинстве случаев происходит безо всякого предупреждения и оказывает на программу столь серьезное воздействие, что определить, что именно было сделано неправильно, иногда бывает невероятно трудно. Единственной приемлемой подсказкой может служить то, что в некоторых случаях переполнение стека вызвано выходом из-под контроля ре- курсивных функций. Если в вашей программе используются рекурсивные функции и вы столкнулись с необъяснимыми сбоями в ее работе, проверьте условия завершения в рекурсивных функциях. И еще одно замечание. Некоторые компиляторы позволяют увеличить объем памяти, резервируемой под стек. Если ваша программа во всем остальном не имеет ошибок, но быстро исчерпывает стековое пространство (возможно из-за глубокой степени вложенно- сти или рекурсивности функций), необходимо просто увеличить размер стека. Применение отладчика Многие компиляторы поставляются вместе с отладчиком, который представляет собой программу, помогающую отладить разрабатываемый код. В общем случае от- ладчики позволяют шаг за шагом исполнять код разрабатываемой программы, уста- навливать точки останова и контролировать содержимое различных переменных. Со- временные отладчики, например такие, как поставляемые в составе пакета Visual C++, являются действительно замечательными инструментальными средствами, кото- рые могут оказать существенную помощь в обнаружении ошибок в разрабатываемом коде. Хороший отладчик стоит дополнительного времени и усилий, которые необхо- димы на его изучение, чтобы в дальнейшем эффективно его применять. Как бы то ни было, хороший программист никогда не откажется от работы с отладчиком для реали- зации надежного проекта и выполнения тонких работ. Теория отладки в общих чертах Каждый разработчик имеет свой собственный подход в программировании и от- ладке. Тем не менее, длительный опыт показывает, что существуют технические приемы, которые значительно лучше, чем остальные. В отношении отладки считается, что наиболее эффективным методом в плане времени и стоимости является инкре- ментное (нарастающее) тестирование, даже если может показаться, что этот подход может замедлить на первых порах процесс разработки. Инкрементное тестирование является технологическим приемом, гарантирующим, что вы всегда будете иметь ра- ботоспособную программу. В чем же его суть? Уже на самых ранних стадиях процесса Глава 28. Производительность, переносимость и отладка 619
разработки функциональный блок. Функциональный блок — это просто фрагмент рабо- тающего кода. По мере добавления нового кода к этому блоку, он тестируется и отла- живается. Таким способом программист может обнаруживать ошибки без особого труда, поскольку, вероятнее всего, ошибки будут присутствовать в более новом коде (добавке) или возникать из-за плохого взаимодействия с функциональным блоком. Время отладки прямо пропорционально общему количеству строк кода, в котором программист ищет ошибки. Благодаря инкрементному тестированию количество строк кода, в котором необходимо искать ошибки, ограничено как правило, количест- вом вновь добавленных строк. Другими словами, ошибка, скорее всего, содержится в строках, которые не входят в состав функционального блока. Эта ситуация проиллю- стрирована на рис. 28.2. Любому программисту хочется минимизировать объем отла- живаемого фрагмента программы. Метод инкрементного тестирования позволяет не тестировать те участки, где эта работа уже была проведена. Таким образом, можно уменьшить область, в которой вероятнее всего прячется ошибка. Крупные проекты часто можно разбить на несколько модулей, слабо взаимодейст- вующих между собой. В таких случаях можно выделить несколько функциональных блоков, что позволит вести параллельную разработку проекта. Инкрементное тестирование — просто технологический прием, благодаря которо- му который всегда можно иметь работоспособный код. Поэтому всякий раз, когда по- является возможность выполнить кусочек разрабатываемой программы, вы должны запустить его на выполнение и тщательно протестировать его. По мере добавления к программе новых фрагментов продолжайте тестировать их, а также их интерфейс с уже проверенным функциональным кодом. Этот способ позволяет разрабатывать программу так, что большинство ошибок будет сконцентрировано в небольшой облас- ти кода. Конечно, вы никогда не должны упускать из виду то, что ошибка могла быть пропущена и в функциональном блоке, но все же данный метод тестирования умень- шает вероятность такого случая. Вероятнее всего, ошибка, если она есть, находится здесь Функциональный блок Добавленный код Рис. 28.2. При регулярном использо- вании метода инкрементного тес- тирования ошибки, если они есть, вероятнее всего находятся в добав- ленном коде 620 Часть V. Разработка программ с помощью С
Полный справочник по Часть VI Интерпретатор языка С В заключительной части проектируется интерпретатор языка С, благодаря чему сразу достигаются две важные цели. Во-первых, разработка этого проекта позволяет продемонстрировать несколько аспектов языка С, важ- ных с точки зрения проектирования больших систем. Во-вторых, разработка такого проекта позволяет уви- деть природу языка С как бы изнутри и узнать, почему язык имеет именно такую структуру.
Полный справочник по Глава 29 Интерпретатор языка С
Работа с интерпретаторами увлекает! А для программиста, пишущего программы на С, нет работы более увлекательной, творческой и интересной, чем работа с ин- терпретатором С! Завершающая глава книги посвящена теме, представляющей интерес для всех программистов, работающих с языком С, и кроме того, она иллюстрирует основные средства языка путем создания интерпретатора языка Little С. Не отрицая всей значимости и актуальности компиляторов, приходится признать, что их создание — очень трудный и длительный процесс. Фактически, одно только создание библиотеки рабочих программ компилятора уже представляет собой чрезвы- чайно сложную задачу. В то же время создание интерпретатора языка — сравнительно более легкая задача, “выполнимая” в рамках одной главы. Нужно отметить, что легче понять принципы работы хорошо сконструированного интерпретатора, чем аналогич- ного компилятора. Помимо простоты разработки, интерпретатор языка содержит не- что такое, чего нет в компиляторе, — движущий механизм, фактически выполняющий программу. Необходимо помнить, что компилятор только транслирует исходный текст программы, то есть придает программе тот вид, в котором она выполняется компью- тером, а интерпретатор выполняет программу. Именно это различие делает интерпре- таторы более интересной темой для рассмотрения. Большинство программистов, пишущих программы на С, оценили не только мощ- ность и гибкость этого языка, но и его необычную формальную красоту, сделавшую его особенно привлекательным для специалистов. Благодаря его логичности и чистоте о языке С часто говорят как об элегантном языке. К настоящему времени об использовании языка С и принципах программирова- ния на нем написано довольно много, однако исследования работы языка “изнутри” встречаются не часто. Поэтому лучшим способом завершения этой книги будет созда- ние программы на С, интерпретирующей подмножество этого же языка. В данной главе рассматривается разработка интерпретатора, способного выполнять программы, написанные на подмножестве языка С. Этот интерпретатор не только вполне работоспособен, он также написан таким образом, что его легко расширять, добавляя новые средства, отсутствующие даже в стандарте С. Читатель, который не знает, как выполняется программа на языке С, будет приятно удивлен прямолинейно- стью ее выполнения. Язык С — один из самых теоретически последовательных язы- ков программирования. Ознакомившись с этой главой, читатель не только получит интерпретатор С, пригодный для использования и расширения, но и существенно улучшит свое понимание структуры языка. К тому же, сама работа с этим интерпрета- тором — весьма интересное и увлекательное занятие. Представленный в этой главе интерпретатор С имеет довольно длинный исход- ный текст программы, но это не будет проблемой. Прочтя изложенный материал, читатель без труда поймет текст программы и принцип ее работы. На заметку И Практическое значение интерпретаторов Интерпретатор С весьма интересен как объект исследований и экспериментов, кроме того, интерпретаторы вообще имеют немалое практическое значение. Программы на С обычно компилируются. Главная причина этого в том, что язык С широко используется для создания коммерческого программного продукта. Для этой цели скомпилированная программа считается более предпочтительной потому, что компиляция позволяет сохранить конфиденциальность исходного текста програм- мы, предотвратить изменение этого текста пользователем, эффективно использовать ресурсы компьютера. Кроме названных, существует немало и других причин. Видимо, 624 Часть VI. Интерпретатор языка С
компиляторы всегда будут доминировать при разработке программного продукта на основе С. Тем не менее, программа на любом языке может быть как скомпилирована, так и интерпретирована. В последние годы на рынке программных продуктов появи- лось даже несколько новых интерпретаторов С. Можно назвать две традиционные причины того, что интерпретаторы продолжают использоваться: их легко сделать интерактивными, а также они очень облегчают от- ладку программы. Однако в последние годы разработчики компиляторов обычно соз- дают интегрированные среды разработки (Integrated Development Environments, IDEs), в которых предусмотрены средства для интерактивной работы и отладки не хуже, чем имеющиеся у интерпретаторов. Поэтому обе вышеназванные причины применения интерпретаторов сейчас уже не актуальны. Однако интерпретаторы продолжают ис- пользоваться. Например, большинство программ, написанных на языках запросов к базам данных, сейчас интерпретируются, а не компилируются. Многие языки управ- ления промышленными роботами также интерпретируются. В последние годы проявилось еще одно преимущество интерпретаторов: повышен- ная переносимость на различные инструментальные комплексы. Характерный пример этого — язык Java. С самого начала Java разрабатывался как язык, предназначенный для интерпретации. Сделано было это специально для того, чтобы программы, напи- санные на нем, можно было выполнять на любом компьютере и в любой среде, со- держащей интерпретатор Java. Такое свойство языка является чрезвычайно ценным, если программа предназначается для работы в распределенных сетевых системах на- подобие Internet. Создание Java и широкое распространение Internet вызвали новую вспышку интереса к интерпретаторам в целом. Есть еще одна причина, делающая интерпретаторы интересными для исследования: они легко поддаются модификации и расширению. Если программист хочет создать свой собственный язык, с которым можно экспериментировать, то сделать это с помо- щью интерпретатора значительно легче, чем с помощью компилятора. Интерпретаторы лучше подходят для создания макетов оболочек программирования, потому что при их использовании правила языка можно легко изменить и быстро увидеть результат. Интерпретатор сравнительно легко создать, понять, как он работает, легко моди- фицировать и, что, возможно, наиболее существенно, работать с ним увлекательно. Например, представленный в данной главе интерпретатор можно переделать таким образом, что он будет выполнять программу от конца к началу, то есть, выполнять ее, начиная с закрывающейся фигурной скобки функции main() и кончая открываю- щейся скобкой. Или можно добавить любое специальное средство языка, какое захо- чется программисту. Образно говоря, компиляторы предназначены для коммерческих разработок, а интерпретаторы — для свободной игры воображения. Данная глава на- писана именно в этом духе и автор искренне надеется, что читатель получит от нее такое же удовольствие, как и он сам при ее написании. В Определение языка Little С Количество зарезервированных слов языка С невелико, однако это богатый и мощный язык. Чтобы описать интерпретатор полного С и его реализацию, понадо- бился бы значительно больший объем, чем одна глава. Интерпретатор Little С (Малый С) предназначен для интерпретации довольно узкого подмножества С, вклю- чающего, тем не менее, многие важные средства языка. При определении конкрет- ного состава подмножества языка Little С использовались два главных критерия: Неотделимо ли данное средство от языка? Необходимо ли оно для демонстрации важных аспектов языка? Глава 29. Интерпретатор языка С 625
Например, такие средства, как рекурсивные функции, глобальные и локальные переменные удовлетворяют обоим критериям. Интерпретатор Little С поддержи- вает все три вида циклов (наличие всех их, конечно, не обязательно в соответст- вии с первым критерием, но необходимо в соответствии со вторым критерием). Однако оператор switch не включен в интерпретатор, потому что он не является обязательным (он красив, но не необходим) и не иллюстрирует ничего такого, что нельзя было бы проиллюстрировать с помощью оператора if (который включен в интерпретатор). Реализация оператора switch оставлена читателю в качестве са- мостоятельного упражнения. Исходя из этих соображений, в интерпретатор Little С включены следующие сред- ства языка: Параметризованные функции с локальными переменными Рекурсия Оператор if Циклы do-while, while и for Локальные и глобальные переменные типов int и char Параметры функций типов int и char Целые и символьные константы Строковые константы (ограниченная реализация) Оператор return (как со значением, так и без него) Ограниченный набор стандартных библиотечных функций Операторы +, *, /, %, <, >, <=, >=, ==, !=, унарный -, унарный + Функции, возвращающие целое значение Комментарии вида /*. . . */ Хоть этот набор и кажется небольшим, однако для его реализации требуется до- вольно объемный исходный текст программы. Одна из причин этого заключается в том, что при выполнении программы непосредственной работе интерпретатора пред- шествует значительная подготовительная работа программы, что обусловлено структу- рированностью языка. Ограничения языка Little С Исходный текст программы интерпретатора Little С довольно длинный, фактиче- ски, длиннее, чем следовало бы помещать в книгу. С целью упрощения этого текста в грамматику Little С введены некоторые ограничения. Первое ограничение заключает- ся в том, что телом операторов if, while, do и for может быть только блок, заклю- ченный в фигурные скобки. Если телом является единственный оператор, он также должен быть заключен в фигурные скобки. Например, интерпретатор Little С не смо- жет правильно обработать следующий фрагмент программы: for(a=0; а < 10; а=а+1) for(b=0; Ь < 10; Ь=Ь+1) for(c=0; с < 10; с=с+1) puts("привет”); if(...) if(...) х = 10; Этот фрагмент должен быть написан так: 626 Часть VI. Интерпретатор языка С
for(a=0; a < 10; a=a+l) { * for(b=0; b < 10; b=b+l) { for(c=0; c < 10; c=c+l) { puts("привет"); } } } if(...) { if (. . .) { x = 10; } } Благодаря этому ограничению интерпретатору легче найти конец участка програм- мы, составляющего тело одного из операторов управления программой. К тому же, поскольку чаще всего операторы управления программой обрабатывают именно блок, это ограничение не выглядит слишком обременительным. При желании читатель мо- жет самостоятельно устранить это ограничение. Другое ограничение заключается в том, что не поддерживаются прототипы функций. Предполагается, что все функции возвращают тип int, разрешен возвращаемый тип char, но он преобразуется в int. Проверка правильности типа параметра не выполняется. Все локальные переменные должны быть объявлены в самом начале функции, сра- зу после открывающейся фигурной скобки. Локальные переменные не могут быть объявлены внутри какого-либо блока. Поэтому следующая функция в языке Little С является неправильной: int myfunc() { int i; /* это допустимо */ if(l) { int i; /* в языке Little С это не допустимо */ } } Здесь объявление переменной i внутри блока if для интерпретатора Little С явля- ется недопустимым. Требование объявления локальных переменных только в начале функции немного упрощает реализацию интерпретатора. Для читателя не составит большого труда устранить это ограничение. И, наконец, последнее ограничение: определение каждой функции должно начи- наться с зарезервированного слова char или int. Следовательно, интерпретатор Lit- tle С не поддерживает традиционное правило “int по умолчанию”. Таким образом, следующее объявление является правильным: Iint main() { /*...*/ } однако следующее объявление в языке Little С неправильное: Imain () { /* ... */ } Отказ от правила “int по умолчанию” приближает Little С к языкам С99 и C++. Глава 29. Интерпретатор языка С 627
IB Интерпретация структурированного языка Язык С структурирован. Это значит, что в нем определены отдельные подпро- граммы с локальными переменными. В языке С также поддерживается рекурсия. Ин- тересен тот факт, что для структурированного языка иногда легче написать компиля- тор, чем интерпретатор. Например, когда компилятор создает код вызова функции, он попросту заталкивает аргументы функции в системный стек и применяет к функции команду процессора CALL. При возврате функция записывает возвращаемое значение в регистр процессора, очищает стек и выполняет команду процессора RET. В то же время, если вызов функции выполняет интерпретатор, он должен на какое-то время “приостановиться”, запомнить текущее состояние, найти функцию, выполнить ее, со- хранить возвращаемое значение, возвратиться в исходную точку программы и восста- новить состояние, существовавшее до вызова функции. Пример выполнения этих действий будет приведен далее при рассмотрении интерпретатора. В сущности, ин- терпретатор должен эмулировать (выполнить другими средствами) команды процессо- ра CALL и RET. Поддержку рекурсии также значительно легче обеспечить в компиля- торе, чем в интерпретаторе. В моей книге The Art of С (Berkeley, СА: Osbome/McGraw-Hill, 1991), вышедшей несколько лет назад, рассматривалась разработка интерпретатора языка small BASIC. В книге утверждается, что интерпретировать старую версию языка BASIC значительно легче, чем язык С, потому что BASIC изначально был предназначен для интерпрета- ции. Он хорошо приспособлен к интерпретации благодаря своей неструктурированно- сти. В нем все переменные являются глобальными и нет отдельных подпрограмм. Я по-прежнему придерживаюсь этого мнения, однако, если для интерпретатора создать средства поддержки функций, локальных переменных и рекурсии, то интерпретиро- вать язык С станет легче, чем BASIC. Так получилось потому, что в языках типа BASIC на теоретическом уровне довольно много исключений из правил. Например, в нем знак равенства в операторе присваивания означает присваивание, а в операторе сравнения — равенство. Язык С почти полностью лишен подобных несуразностей. На заметку Разработанная автором реализация интерпретатора языка small BASIC со- держит немало полезного для читателей, интересующихся интерпретатора- ми. Последняя версия интерпретатора small BASIC приведена в The C/C++ An- notated Archives (Berkley, CA: Osbome/McGraw-Hill, 1999). H Неформальная теория языка С Перед тем как приступить к разработке интерпретатора языка С, необходимо уяс- нить структуру языка С. Формальное определение языка С (например, в стандарте ANSI/ISO) очень длинное, к тому же в нем довольно много зашифрованных для не- искушенного читателя положений. Однако совершенно формальное определение язы- ка С для разработки интерпретатора не понадобится, потому что этот язык является довольно прямолинейным. Полное формальное определение языка С необходимо для создания коммерческого компилятора, а для Little С оно не является необходимым. (Фактически, в одной главе невозможно изложить формальный синтаксис, опреде- ляющий С; это заняло бы целую книгу.) Эта глава предназначена для широкого круга читателей. Она не была задумана как формальное введение в теорию структурированных языков в целом и языка С в част- ности. Поэтому здесь некоторые концепции изложены упрощенно и разработка ин- терпретатора подмножества С ведется так, что от читателя не потребуется формальная подготовка по теории языков (структурной лингвистике). 628 Часть VI. Интерпретатор языка С
Несмотря на это для реализации и понимания интерпретатора Little С некоторые све- дения об определении языка все же необходимы. Материал, изложенный далее, является вполне достаточным для наших целей. Желающим ознакомиться с более формализован- ным изложением материала следует обратиться к стандарту ANSI/ISO языка С. Все программы на С представляют собой набор из одной или более функций плюс глобальные переменные (если они есть). Функция состоит из спецификатора типа функ- ции, имени функции, списка параметров и блока операторов, ассоциированного с функ- цией. Блок начинается скобкой {, за которой следует последовательность из одного или нескольких операторов, и заканчивается скобкой }. Оператор языка С либо начинается с одного из зарезервированных слов, например if, либо является выражением. (Что пред- ставляет собой выражение, будет рассмотрено в следующем разделе.) Изложенные выше порождающие правила могут быть сведены в следующую таблицу: программа -> набор функций плюс глобальные переменные функция -> спецификатор списоктгараметров блок_операторов бл©кооператоров -> { последовательность_рператоров } -> зарезервированное слово, выражение или блок операторов оператор Выполнение любой программы на С начинается вызовом функции main () и кон- чается последней закрывающейся скобкой } или первым оператором return, встре- тившимся в main (), если до этого не встретились exit () или abort (). Любая другая функция программы должна быть непосредственно или косвенно вызвана функцией main(). Таким образом, выполнение программы начинается с началом выполнения main () и кончается выходом из нее. Интерпретатор Little С именно так и работает. Выражения языка С В языке С роль выражений несколько шире, чем в других языках программирова- ния. В общем случае в программе на С оператор может начинаться с зарезервирован- ного слова языка С, например, while или switch, а может и не начинаться с него. Для удобства дальнейшего изложения все операторы, начинающиеся с зарезервиро- ванного слова языка С, будем называть операторами с зарезервированным словом. Все остальные операторы (не начинающиеся с зарезервированного слова) будем называть операторами-выражениями. Таким образом, все следующие операторы языка С явля- ются операторами-выражениями: I count = 100; sample = i / 22 * (с-10); printf("Это выражение"); /* Строка 1 */ /* Строка 2 */ /* Строка 3 */ Рассмотрим каждый из этих операторов-выражений подробно. В языке С знак ра- венства является оператором присваивания1. Здесь оператор присваивания работает не так, как, например, в BASIC. В языке BASIC значение, вычисленное в правой части знака равенства, присваивается переменной в левой части, однако, и это весьма суще- ственно, это значение не является значением оператора присваивания. В то же время в языке С знак равенства является оператором присваивания и значение результата оператора присваивания равно значению, полученному в правой части. Следователь- но, в языке С оператор присваивания фактически является выражением присваивания. Оператор присваивания имеет значение, поэтому он является выражением. Именно по этой причине правильными являются, например, следующие выражения: |а = b = с = 100; printf("%d"r а=4+5); 1 Сам знак равенства является, конечно, знаком операции присваивания. — Прим. ред. Глава 29. Интерпретатор языка С 629
Эти выражения в языке С допустимы, потому что присваивание является оператором, имеющим значение, как и любая другая операция. Продолжим рассмотрение предыдущего примера. Строка 2 содержит более слож- ное присваивание. В строке 3 вызывается функция printf (), выводящая на экран строку. В языке С все функции базовых типов, отличных от void, возвращают значе- ние независимо от того, определен тип явно или нет. Следовательно, вызов функции, возвращающей значение, является выражением, имеющим значение, опять же неза- висимо от того, присваивается оно чему-либо или нет. Вызов функции, не возвра- щающей значения (определенной со спецификатором void), также является выраже- нием, однако его результат имеет тип void. Определение значения выражения Перед тем как приступить к разработке программы, способной правильно вычислить значение выражения, нужно дать более формальное определение выражения. Фактически в каждом языке программирования выражения определяются рекурсивно с помощью по- рождающих правил, или продукций. Интерпретатор Little С поддерживает следующие опе- рации: +, -, *, /, %, =, операторы сравнения (<, ==, > и так далее) и скобки. В языке Little С выражения определяются с помощью следующих порождающих правил: выражение [присваивание] [значение_переменной] присваивание -> именующее__выражение ~ значение__переменной именующее__выражение -> переменная значение_переменной часть [оператор_сравнения часть] * часть -> терм [+терм] [-терм] терм множитель [^множитель] [/множитель] [%множитель] ; множитель [+ или -] атом атом -> переменная, константа, функция, или (выражение) Здесь термин оператор-Сравнения может обозначать любой из операторов сравне- ния. Термины именующее_выражение и значение_переменной означают объекты в левой и правой частях оператора присваивания. Старшинство оператора определяется поро- ждающим правилом. Чем выше старшинство оператора, тем ниже в списке операто- ров он расположен. Рассмотрим применение порождающих правил на примере вычисления выражения | count =10-5* 3; Сначала применяется правило 1, разделяющее выражение на три части: count 10-5*3 т т t именующее_выражение присваивание значение_переменной Поскольку значение нетерминала значение_переменной не содержит операторов сравнения, то оно может быть сгенерировано в результате применения порождающего правила для нетерминала терм 10 - 5*3 Т f t терм . минус у терм Несомненно, второй терм составлен из двух множителей: 5 и 3. Эти два множителя являются константами, они порождаются с помощью порождающих правил более низкого уровня. 630 Часть VI. Интерпретатор языка С
Теперь, чтобы вычислить значение выражения, будем двигаться, следуя порождающим правилам, в обратном направлении. Сначала выполняется умножение 5*3, что дает 15. По- том это значение вычитается из 10, получается -5. И, наконец, последний шаг — присваи- вание этого значения переменной count, оно же является значением всего выражения. При создании интерпретатора Little С в первую очередь нужно построить алгорит- мический эквивалент рассмотренной только что процедуры вычисления выражения. В Синтаксический анализатор выражений Часть программы, выполняющая чтение и анализ выражения, называется синтак- сическим анализатором выражений. Это — наиболее важная подсистема интерпретато- ра Little С. Так как согласно стандарту множество выражений в языке С значительно шире, чем во многих других языках программирования, то синтаксический анализа- тор выражений составляет значительную часть программного кода синтаксического анализатора программ. Для построения синтаксического анализатора выражений языка С можно применить несколько различных методов. Во многих коммерческих компиляторах используются синтаксические анализаторы, управляемые таблицей; такие синтаксические анализаторы создаются специальными генераторами программ синтаксического анализа. Синтакси- ческие анализаторы, управляемые таблицей, в общем случае обладают большим быстро- действием, чем другие синтаксические анализаторы, однако процесс их создания очень трудоемкий. В рассматриваемом здесь интерпретаторе Little С используется рекурсивный нисходящий синтаксический анализатор1, который представляет собой реализацию в язы- ке С производящих правил, приведенных в предыдущем разделе. Рекурсивный нисходящий синтаксический анализатор представляет собой набор взаимно рекурсивных функций, обрабатывающих выражение. Если синтаксический анализатор работает в компиляторе, то он генерирует объектный код, соответствую- щий исходному тексту программы. В интерпретаторе целью синтаксического анализа- тора является вычисление значения заданного выражения. В этом разделе рассматри- вается разработка синтаксического анализатора выражений языка Little С. Основы теории синтаксического анализа выражений рассмотрены в главе 24. Синтаксический анализатор, разрабатываемый в этой главе, строится на ос- нове простого расширения этой теории. Синтаксический разбор исходного текста программы Специальная функция, читающая исходный текст программы и возвращающая очеред- ную логическую единицу, является фундаментальной частью каждого интерпретатора и компилятора. Исторически сложившееся название такой логической единицы — лексема. Во всех языках программирования (в том числе и в языке С) программа рассматривается как последовательность лексем. Другими словами, лексема — это неделимая единица программы. Например, оператор равенства == является лексемой. Эти два знака равенства нельзя разделить, не изменив кардинальным образом их значение. Аналогично, if — так- же лексема. Ни “i”, ни “f ” сами по себе не имеют в программе на С никакого значения. В языке С каждая лексема принадлежит одной из следующих категорий: зарезервированные слова идентификаторы константы строки операторы знаки пунктуации На заметку 1 Так называется программа, выполняющая синтаксический анализ методом рекурсивного спуска. — Прим. ред. Глава 29. Интерпретатор языка С 631
Зарезервированные слова — это лексемы, составляющие язык С; к ним относится, например, while. Идентификаторы — это имена переменных, функций и типов, оп- ределенных пользователем (в Little С не реализованы). Знаки пунктуации — это неко- торые символы, такие как точка с запятой, запятая, различные скобки и т.п. В зави- симости от контекста, некоторые из этих символов, могут быть операторами. Приве- дем пример разбиения на лексемы при разборе оператора слева направо. Оператор |for(x=0; х<10; х=х+1) printf(”anno %d”z х); состоит из следующих лексем: Лексема Категория for зарезервированное слово ( знак пунктуации X идентификатор = оператор 0 константа знак пунктуации X идентификатор < оператор 10 константа знак пунктуации X идентификатор = оператор X идентификатор + оператор 1 константа ) знак пунктуации printf идентификатор ( знак пунктуации "алло %d” строка знак пунктуации X идентификатор ) знак пунктуации > знак пунктуации Однако для упрощения интерпретатора Little С в нем определяются следующие ка- тегории лексем: Тип лексемы Включает delimiter (разделитель) знаки пунктуации и операторы keyword (зарезервированное слово) зарезервированные слова string (строка) строки, заключенные в двойные кавычки identifier (идентификатор) имена переменных и функций number (число) числовая константа block (блок) { или } Функция get_token () выделяет лексемы из исходного текста программы Little С и возвращает их в качестве своего значения: /* Считывание лексемы из входного потока. */ int get—token(void) { register char *temp; token_type = 0; tok = 0; 632 Часть VI. Интерпретатор языка С
temp = token; *temp = ’’; /* пропуск пробелов, символов табуляции и пустой строки */ while(iswhite(*prog) && *prog) ++prog; if(*prog == ’r’) { ++prog; ++prog; /* пропуск пробела */ while(iswhite(*prog) && *prog) ++prog; } if(*prog == ’’) { /* конец файла */ *token = ' 0 ’; tok = FINISHED; return (token_type = DELIMITER); if(strchr("{}", *prog)) { /* ограничители блока */ *temp = *prog; temp++; *temp = ’’; prog++; return (token_type = BLOCK); } /* поиск комментариев */ if(*prog == ’/’) if(*(prog+l) == •*’) { /* это комментарий */ prog += 2; do { /* конец комментария */ while(*prog != ’*’) prog++; prog++; } while (*prog != '/’); prog++; } if(strchr(”!<>=”t *prog)) { /* возможно, это оператор сравнения */ switch(*prog) { case : if(*(prog+l) == '=') { prog++; prog++; * temp = EQ; temp++; *temp = EQ; temp++; * temp = ’’; } break; case if(*(prog+l) “ '=') { prog++; prog++; * temp = NE; temp++; *temp - NE; temp++; * temp = '’; } break; case •<•: if(*(prog+l) ==’=’) { prog++; prog++; * temp = LE; temp++; *temp = LE; Глава 29. Интерпретатор языка С 633
else { prog++; *temp = LT; } temp++; *temp = ’’; break; case if(*(prog+l) == '=') { prog++; prog++; *temp = GE; temp++; *temp = GE; } else { prog++; *temp = GT; } temp++; *temp = ’’; break; } if(*token) return(token_type = DELIMITER); } if(strchr(”+-*л/%=;(),’”, *prog)){ /* разделитель ★/ *temp = *prog; prog++; /★ продвижение на следующую позицию ★/ temp++; *temp = '’; return (token_type = DELIMITER) ; } if (*prog== ’ ’’ ’) { /* строка в кавычках */ prog++; while(*prog != *prog != 'r') *temp++ = *prog++; if(*prog == 'r') sntx_err(SYNTAX); prog++; *temp = ’'; return (token__type = STRING) ; if(isdigit(*prog)) { /* число */ while(!isdelim(*prog)) *temp++ = *prog++; *temp = ’’; return (token_type = NUMBER); if(isalpha(*prog)) { /* переменная или оператор */ while(!isdelim(*prog)) *temp++ = *prog++; token_type = TEMP; *temp = ’’; /* узнать, является эта строка оператором или переменной */ if(token_type==TEMP) { tok = look__up (token) ; /* преобразовать во внутреннее представление */ if(tok) token_type = KEYWORD; 634 Часть VI. Интерпретатор языка С
)/★ это зарезервированное слово */ else token__type = IDENTIFIER; return token__type; } В функции get_token () используются следующие глобальные данные и перечис- лимые типы: extern char *prog; /* текущий адрес в исходном тексте программы */ extern char *р_buf; /* указатель на начало буфера программы */ extern char token[80]; /* строковое представление лексемы */ extern char token—type; /* содержит тип лексемы */ extern char tok; /* внутреннее представление лексемы */ enum tok-types {DELIMITER, IDENTIFIER, NUMBER, KEYWORD, TEMP, STRING, BLOCK}; enum double—ops {LT=1, LE, GT, GE, EQ, NE}; /*Эти константы используются для вызова функции sntx_err() в случае синтаксической ошибки. При необходимости список констант можно расширить. ВНИМАНИЕ: константа SYNTAX используется тогда, когда интерпретатор не может квалифицировать ошибку. */ enum erгоГ—msg {SYNTAX, UNBAL-PARENS, NO-EXP, EQUALS—EXPECTED, NOT-VAR, PARAM-ERR, SEMI-EXPECTED, UNBAL-BRACES, FUNC_UNDEF, TYPE-EXPECTED, NEST-FUNC, RET-NOCALL, PAREN—EXPECTED, WHILE-EXPECTED, QUOTE-EXPECTED, NOT_TEMP, TOO-MANY-LVARS, DIV_BY_ZERO}; Указатель prog указывает на текущую позицию в исходном тексте интерпретируе- мой программы. Указатель p_buf интерпретатором не изменяется; он всегда указыва- ет на начало интерпретируемой программы. Функция get-token () начинает работу с удаления пробелов и символов перевода строки. Так как никакая лексема языка С (кроме строковой константы) не содержит пробелов, их нужно пропустить. Функция get-token () пропускает также комментарии (в Little С допускаются только коммен- тарии вида /*...*/)• После этого строка, представляющая каждую лексему, помещается в token и ее тип (определенный в перечислении tok-types) записывается в to- ken-type. Если лексема представляет собой зарезервированное слово, то его внут- реннее представление присваивается tok с помощью функции look up () (приведена в полном листинге синтаксического анализатора). Необходимость внутреннего пред- ставления зарезервированных слов будет обоснована позже. Функция get_token() преобразует двухсимвольные операторы сравнения в соответствующие значения пере- числимого типа. Технически в этом нет крайней необходимости, однако это упрощает реализацию интерпретатора. И, наконец, если синтаксический анализатор находит синтаксическую ошибку, то он вызывает функцию sntX-err() со значением пере- числимого типа, соответствующим типу найденной ошибки. Функция sntx_err () вызывается также другими процедурами интерпретатора каждый раз, когда встречает- ся ошибка. Листинг функции sntx_err () имеет такой вид: Глава 29. Интерпретатор языка С 635
/★ Вывод сообщения об ошибке. */ void sntx_err(int error) { char *p, *temp; int linecount = 0; register int i; static char *e[]= { "синтаксическая ошибка", "несбалансированные скобки", "выражение отсутствует", "ожидается знак равенства", "не переменная", "ошибка в параметре", "ожидается точка с запятой", "несбалансированные фигурные скобки", "функция не определена", "ожидается спецификатор типа", "слишком много вложенных вызовов функций", "оператор return вне функции", "ожидаются скобки", "ожидается while", "ожидается закрывающаяся кавычка", "не строка", "слишком много локальных переменных", "деление на нуль" }; printf("n%s", е[error]); р = p_buf; while(р != prog) { /* поиск номера строки с ошибкой */ р++; if(*р == •г•) { linecount++; } } printf(" в строке %dn", linecount); temp = р; for(i=0; i < 20 && р > p_buf && *p != ’n’; i++, p—); for(i=0; i < 30 && p <= temp; i++, p++) printf("%c", *p); longjmp (e__buf, 1); /* возврат в безопасную точку */ } Обратите внимание, sntx_err () выводит на экран номер строки, в которой обнару- жена ошибка (или номер следующей строки) и саму строку. Заканчивается sntx_err() вызовом longjmp (). Синтаксическая ошибка часто встречается внутри глубоко вложенных или рекурсивных процедур, поэтому лучшим способом реакции на ошибку является пере- ход в какое-либо безопасное место. Как альтернативный подход, можно было бы устано- вить глобальный флажок ошибки и просмотреть его значение во всех точках каждой про- цедуры, однако это существенно усложнило бы программу интерпретатора. Рекурсивный нисходящий синтаксический анализатор Little С Ниже приведен полный текст рекурсивного нисходящего синтаксического анали- затора Little С вместе со всеми его функциями, глобальными данными и типами дан- ных. Текст программы синтаксического анализатора находится в одном файле под 636 Часть VI. Интерпретатор языка С
именем PARSER.C. (Из-за большого объема всей программы интерпретатора Little С она содержится в трех отдельных файлах.) /* Рекурсивный нисходящий синтаксический анализатор целочисленных выражений, содержащих переменные и вызовы функций. */ #include <setjmp.h> #include <math.h> #include <ctype.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #define NUM_FUNC 100 #define NUM_GLOBAL_VARS 100 #define NUM_LOCAL_VARS 200 #define ID_LEN 31 #define FUNC_CALLS 31 #define PROG_SIZE 10000 #define FOR NEST 31 enum tok_types {DELIMITER, IDENTIFIER, NUMBER, KEYWORD, TEMP, STRING, BLOCK}; enum tokens {ARG, CHAR, INT, IF, ELSE, FOR, DO, WHILE, SWITCH, RETURN, EOL, FINISHED, END}; enum double_ops {LT=1, LE, GT, GE, EQ, NE}; /* Эти константы используются для вызова функции sntx_err() в случае синтаксической ошибки. При необходимости список констант можно расширить. ВНИМАНИЕ: константа SYNTAX используется тогда, когда интерпретатор не может квалифицировать ошибку. */ enum error_msg {SYNTAX, UNBAL_PARENS, NO_EXP, EQUALS_EXPECTED, NOT_VAR, PARAM_ERR, SEMI_EXPECTED, UNBAL_BRACES, FUNC_UNDEF, TYPE_EXPECTED, NEST_FUNC, RET_NOCALL, PAREN_EXPECTED, WHILE_EXPECTED, QUOTE_EXPECTED, NOT_TEMP, TOO_MANY_LVARS, DIV_BY_ZERO}; extern char *prog; /* текущее положение в исходном тексте программы */ extern char *p_buf; /* указатель на начало буфера программы */ extern jmp_buf e_buf; /* содержит данные для longjmpO */ /* Массив этой структуры содержит информацию о глобальных переменных */ extern struct var_type { char var_name[32]; int v_type; int value; } global_vars[NUM_GLOBAL_VARS]; /* Это стек вызова функций. */ extern struct func_type { char funcjiame[32]; Глава 29. Интерпретатор языка С 637
int ret_type; char *loc; /* Адрес входа функции в файле */ } func_stack[NUM_FUNC]; /* Таблица зарезервированных слов */ extern struct commands { char command[20]; char tok; } tablet]; /* Здесь функции ’’стандартной библиотеки” объявлены таким образом, что их можно поместить во внутреннюю таблицу функции */ int call_getche(void), call_putch(void); int call_puts(void) , print(void), getnum(void); struct intern_func_type { char *f_name; /* имя функции */ int (*p)(); /* указатель на функцию */ } intern_func[] = { ’’getche”, call_getche, ’’putch", call_putch, ’’puts”, call_puts, ’’print", print, "getnum", getnum, 0 /* этот список заканчивается нулем */ }; extern char token[80]; /* строковое представление лексемы */ extern char token_type; /* содержит тип лексемы */ extern char tok; /* внутреннее представление лексемы */ extern int ret_value; /* возвращаемое значение функции */ void eval_expO(int *value); void eval_exp(int *value); void eval_expl(int *value); void eval_exp2(int *value); void eval_exp3(int *value); void eval_exp4(int *value); void eval_exp5(int *value); void atom(int *value); void sntx_err(int error), putback(void); void assign_var(char *var_name, int value); int isdelim(char c), look_up(char *s), iswhite(char c); int find_var(char *s), get_token(void); int internal_func(char *s); int is_var(char *s) ; char *find_func(char *name); void call(void); /* Точка входа в синтаксический анализатор выражений. */ void eval_exp(int *value) { get_token(); if(!*token) { sntx_err(NO_EXP); 638 Часть VI. Интерпретатор языка С
return; } if(*token ==’;’) { *value =0; /* пустое выражение */ return; } eval_exp0(value); putback(); /* возврат последней лексемы во входной поток */ } /* Обработка выражения в присваивании */ void eval_exp0(int *value) { char temp[ID—LEN]; /* содержит имя переменной, которой присваивается значение */ register int temp_tok; if(token_type == IDENTIFIER) { if(is_var(token)) { /* если это переменная, посмотреть, присваивается ли ей значение */ strcpy(temp, token); temp_tok = token_type; get_token(); if(*token == ’=’) { /* это присваивание */ get_token(); eval_exp0(value); /* вычислить присваиваемое значение */ assign_var(temp, *value); /* присвоить значение */ return; } else { /* не присваивание */ putback(); /* восстановление лексемы */ strcpy(token, temp); token_type = temp_tok; } } } eval_expl(value); } /* Обработка операций сравнения. */ void eval_expl(int *value) { int partial_value; register char op; char relops[7] = { LT, LE, GT, GE, EQ, NE, 0 }; eval_exp2(value); op = *token; if(strchr(relops, op)) { get_token(); eval_exp2(&partial_value) ; switch(op) { /* вычисление результата операции сравнения */ case LT: * value = *value < partial_value; break; Глава 29. Интерпретатор языка С 639
case LE: * value = *value <= partial_value; break; case GT: * value = *value > partial_value; break; case GE: * value = *value >= partial_value; break; case EQ: * value = *value == partial_value; break; case NE: * value = *value != partial_value; break; } } } /* Суммирование или вычитание двух термов. */ void eval_exp2(int *value) { register char op; int partial_value; eval_exp3(value); while((op = *token) == ’+’ || op == ’-’) { get_token(); eval_exp3(&partial_value); switch(op) { /* суммирование или вычитание */ case •-•: * value = *value - partial_value; break; case ’+’: * value = *value + partial_value; break; } } } /* Умножение или деление двух множителей. */ void eval_exp3(int *value) { register char op; int partial_value, t; eval_exp4(value); while((op = *token) == || op == ’/’ II op == '%') { get_token() ; eval_exp4(&partial_value); switch(op) { /* умножение, деление или деление целых */ case •*•: * value = *value * partial_value; break; case ’/’: i f(partial_value == 0) sntx_err(DIV_BY_ZERO); * value = (*value) / partial_value; 640 Часть VI. Интерпретатор языка С
break; case •%•: t - (*value) / partial—value; ♦ value - *value-(t * partial—value); break; } } } /* Унарный + или - */ void eval_exp4(int *value) { register char op; op “ •’; if(*token e= •+• || *token -- •-•) { op - *token; get_token(); } eval_exp5(value); if(op) if(op =« •-•) *value - -(*value); /* Обработка выражения в скобках. */ void eval_exp5(int *value) { if((*token =='(')) { get_token(); eval_expO(value); /* вычисление подвыражения */ if(*token != ')') sntx_err(PAREN_EXPECTED); get_token(); } else atom (value) ; /* Получение значения числа, переменной или функции. */ void atom(int *value) { int i; switch(token_type) { case IDENTIFIER: i = internal—func(token); if(i!= -1) { /* вызов функции из стандартной библиотеки */ ♦value = (*intern__func [i] .р) () ; } else if(find—func(token)) { /* вызов функции, определенной пользователем */ call(); ♦value = ret_value; } else *value = find_var(token); /* получение значения переменной */ get—token(); return; Глава 29. Интерпретатор языка С 641
case NUMBER: /* числовая константа */ *value = atoi(token); get_token(); return; case DELIMITER: /* это символьная константа? */ if(*token «« ’ • ’) { *value = *prog; prog++; if(*prog!=fV') sntx_err(QUOTE_EXPECTED); prog++; get—token(); return ; } if(*token==’)’) return; /* обработка пустого выражения */ else sntX—err(SYNTAX); /* синтаксическая ошибка */ default: sntX—err(SYNTAX); /* синтаксическая ошибка */ } } /* Вывод сообщения об ошибке. */ void sntX—err(int error) { char *p, *temp; int linecount = 0; register int i; static char *e[]= { "синтаксическая ошибка", "несбалансированные скобки", "выражение отсутствует", "ожидается знак равенства", "не переменная", "ошибка в параметре", "ожидается точка с запятой", "несбалансированные фигурные скобки", "функция не определена", "ожидается спецификатор типа", "слишком много вложенных вызовов функций", "возврат без вызова", "ожидаются скобки", "ожидается while", "ожидается закрывающаяся кавычка", "не строка", "слишком много локальных переменных", "деление на нуль" }; printf("n%s", е[error]); р = Р—buf; while(р != prog) { /* поиск номера строки с ошибкой */ р++; if(*р == 'г') { linecount++; } } printf(" в строке %dn", linecount); 642 Часть VI. Интерпретатор языка С
temp = p; for(i=0; i < 20 && p > p_buf && *p != ’n’; i++, p—); for(i=0; i < 30 && p <= temp; i++, p++) printf("%c"t *p); longjmp(e_buf, 1); /* возврат в’ безопасную точку */ } /* Считывание лексемы из входного потока. */ int get__token (void) { register char *temp; token_type = 0; tok = 0; temp = token; ♦temp = ’’; /* пропуск пробелов, символов табуляции и пустой строки */ while(iswhite(*prog) && *prog) ++prog; if(*prog == ’r’) { ++prog; ++prog; /* пропуск пробела */ while(iswhite(*prog) && *prog) ++prog; } if(*prog == ’’) { /* конец файла */ ♦token = ’’; tok = FINISHED; return (token_type = DELIMITER); if(strchr(”{}”, *prog)) { /* ограничители блока */ ♦temp = *prog; temp++; ♦temp = ’’; prog++; return (token_type = BLOCK); /* поиск комментариев */ if(*prog == ’/’) if(*(prog+l) == ’*’) { /* это комментарий */ prog += 2; do { /* найти конец комментария */ while(*prog != ’♦’) prog++; prog++; } while (*prog != ’/’) ; prog++; } if(strchr("!<>=", *prog)) { /* возможно, это оператор сравнения */ switch(*prog) { case : if(*(prog+l) == ’=’) { Глава 29. Интерпретатор языка С 543
prog++; prog++; * temp = EQ; temp++; *temp = EQ; temp++; * temp = ' ’; } break; case if(*(prog+l) == ’=’) { prog++; prog++; * temp = NE; temp++; *temp - NE; temp++; * temp = ''; } break; case if(*(prog+l) == ’=’) { prog++; prog++; * temp = LE; temp++; *temp = LE; } else { prog++; * temp = LT; } temp++; *temp = ’’; break; case if(*(prog+l) == ’=’) { prog++; prog++; *temp = GE; temp++; *temp = GE; } else { prog++; *temp = GT; } temp++; *temp = ’’; break; } if(*token) return(token_type = DELIMITER); if (strchr , 1 ", *prog) ) { /* разделитель */ * temp = *prog; prog++; /* продвижение на следующую позицию */ temp++; * temp = 1•; return (token_type = DELIMITER); } if(*prog==l"1) { /* строка в кавычках */ prog++; while(*prog != 1"'&& *prog != 1 r’) *temp++ = *prog++; if(*prog == ’r’) sntx_err(SYNTAX); prog++; *temp = ’’; return (token_type = STRING); } if(isdigit(*prog)) { /* число */ while(!isdelim(*prog)) *temp++- *prog++; 644 Часть VI. Интерпретатор языка С
* temp = ’’; return (token_type e NUMBER); } if(isalpha(*prog)) { /* переменная или оператор */ while(•isdelim(*prog)) *temp++ e *prog++; token_type e TEMP; } *temp = ••; /* эта строка является оператором или переменной? */ if(token_type==TEMP) { tok e look__up (token) ; /* преобразовать во внутреннее представление */ if(tok) token_type - KEYWORD; /* это зарезервированное слово */ else token_type = IDENTIFIER; } return token_type; /* Возврат лексемы во входной поток. */ void putback(void) { char *t; t = token; for(; *t; t++) prog—; } /* Поиск внутреннего представления лексемы в таблице лексем. */ int look_up(char *s) { register int i; char *p; /* преобразование в нижний регистр */ р = s; while(*р) { *р = tolower(*p); р++; } /* Есть ли лексема в таблице? */ for(i=0; *table[i].command; i++) { if(!strcmp(table[i].command, s)) return table[i].tok; } return 0; /* незнакомый оператор */ } /* Возвращает индекс функции во внутренней библиотеке, или -1, если не найдена. */ int internal_func(char *s) { int i; for(ie0; intern_func[i].f_name[0]; i++) { Глава 29. Интерпретатор языка С 645
if(!strcmp(intern_func[i].f_name, s)) return i; } return -1; } /* Возвращает true (ИСТИНА), если с - разделитель. */ int isdelim(char с) { if(strchr(" !;,+-<>’/*%А=О ”, с) I I с == 9 || с == ’г’ || с == 0) return 1; return 0; /* Возвращает 1, если с - пробел или табуляция. */ int iswhite(char с) { if (с == ’ || с == • t •) return 1; else return 0; Функции, начинающиеся c eval_exp, и функция atom() реализуют порождающие правила для выражений в Little С. Для их проверки (и в качестве упражнения) реко- мендуется мысленно выполнить действия синтаксического анализатора для какого- либо простого выражения. Функция atom () находит значение целой константы или переменной, функции или символьной константы. В тексте программы могут присутствовать функции двух видов: определенные пользователем и библиотечные. Если встретилась пользователь- ская функция, ее текст обрабатывается интерпретатором до получения возвращаемого значения и выхода из функции. (Вызов функции рассматривается в следующем разде- ле.) Если встретилась библиотечная функция, то сначала ищется ее адрес с помощью функции internal_func (), а затем устанавливается доступ к ней с помощью ее ин- терфейсной функции. Библиотечные функции и адреса их интерфейсных функций содержатся в массиве intern_func, приведенном ниже: /* Здесь функции "стандартной библиотеки" объявлены таким образом, что их можно поместить во внутреннюю таблицу функции. */ int call_getche(void), call_putch(void); int call_puts(void), print(void), getnum(void); struct intern_func_type { char *f_name; /* имя функции */ int (*p)(); /* указатель на функцию */ } intern_func[] = { "getche", call_getche, "putch", call_putch, "puts", call_puts, "print", print, "getnum", getnum, "", 0 /* этот список заканчивается нулем */ }; Таким образом, в интерпретаторе Little С предусмотрено только несколько функ- ций стандартной библиотеки, однако расширить их список очень легко. (Тексты ин- терфейсных функций содержатся в отдельном файле, рассматриваемом далее в разделе “Библиотечные функции Little С”.) 646 Часть VI. Интерпретатор языка С
Необходимо сделать еще одно замечание о процедурах в файле синтаксического анализатора. Для правильного анализа программы на С в некоторых случаях требуется так называемый просмотр на одну лексему вперед. Например, в операторе | alpha = count(); интерпретатор сможет определить, что count является функцией, а не переменной, только если просмотрит на одну лексему вперед, то есть прочтет следующую скобку. Однако, если оператор выглядит как | alpha = count * 10; то следующую после count лексему (в данном случае *) нужно вернуть обратно во входной поток; она будет использована позднее. Поэтому в файл синтаксического анализатора выражений включена функция putback (), которая возвращает послед- нюю прочитанную лексему обратно во входной поток. В файле синтаксического анализатора выражений могут встретиться функции, в данный момент непонятные для читателя, однако в процессе изучения Little С их на- значение и принцип работы станут яснее. В Интерпретатор Little С В этом разделе рассматривается наиболее важная часть интерпретатора Little С. Перед тем как приступить к подробному чтению текста программы интерпретатора, нужно понять, как вообще работает интерпретатор. Понять программу интерпретатора в некотором смысле легче, чем программу синтаксического анализатора выражений, потому что работа по интерпретации программы на С может быть выражена следую- щим простым алгоритмом: while (есть_лексемы_во_входном_потоке) { читать следующую лексему; выполнить соответствующее действие; Этот алгоритм может показаться невероятно простым по сравнению с синтаксическим анализатором выражений, но это именно то, что делает интерпретатор. Нужно только иметь в виду следующее: шаг “выполнить соответствующее действие” может содержать чтение дополнительных лексем из входного потока. Для лучшего понимания этого алго- ритма мысленно выполним интерпретацию следующего фрагмента программы: Iint а; а = 10; if(а < 100) printf("%d", а); Согласно алгоритму, прочтем первую лексему int. Эта лексема указывает на то, что следующим действием должно быть чтение следующей лексемы для того, чтобы узнать, как называется переменная (а), которую нужно объявить и для которой нужно выделить область памяти. Следующая лексема (точка с запятой) заканчивает строку. Соответствующее действие — проигнорировать ее. Далее, начинаем следующую ите- рацию алгоритма и считываем следующую лексему, это а из второй строки. Строка не начинается с зарезервированного слова, следовательно, это выражение языка С. По- этому соответствующим действием является применение синтаксического анализатора выражений для вычисления значения выражения. Этот процесс “съедает” все лексемы во второй строке. Наконец, читаем лексему if. Она указывает на то, что начинается оператор if. Соответствующее действие — выполнить его. Аналогичный процесс вы- Глава 29. Интерпретатор языка С 647
полняется многократно, пока не будет считана последняя лексема программы. Это относится к любой программе на С. Пользуясь этим алгоритмом, приступим к созда- нию интерпретатора. Предварительный проход интерпретатора Перед тем как интерпретатор начнет выполнять программу, должны быть выпол- нены некоторые рутинные процедуры. Характерной чертой языков, предназначенных больше для интерпретации, чем для компиляции, является то, что выполнение про- граммы начинается в начале текста программы и заканчивается в его конце. Так вы- полняются программы, написанные на старых версиях языка BASIC. Это, однако, не относится к языку С (как и к любому другому структурированному языку) по трем основным причинам. Во-первых, все программы на С начинают выполняться с функции main (). Вовсе не обязательно, чтобы эта функция была первой в программе. Поэтому интерпретатор, чтобы начать выполнение с нее, должен еще до начала выполнения программы узнать, где она находится. Следовательно, должен быть реализован некоторый метод, позволяющий на- чать выполнение программы с нужной точки. (Глобальные переменные также могут предшествовать функции main (), поэтому, даже если она является первой функцией программы, все равно и в этом случае она не начинается с первой строки.) Во-вторых, все глобальные переменные должны быть известны перед началом выпол- нения main (). Операторы объявления глобальных переменных никогда не выполняются интерпретатором, потому что они находятся вне всех функций. (В языке С весь выпол- няющийся текст программы находится внутри функций, поэтому при выполнении про- граммы интерпретатор Little С никогда не выходит за пределы функций.) И наконец, в-третьих, для повышения скорости выполнения необходимо (правда, не всегда) знать, где в программе расположена каждая функция; это позволит вызы- вать ее как можно быстрее. Если это условие не будет выполнено, то при каждом вы- зове функции понадобится длительный последовательный поиск этой функции в тек- сте программы. Эти проблемы решаются с помощью предварительного прохода интерпретатора. Программа предварительного прохода (иногда ее называют препроцессором, правда это название очень неудачное из-за того, что совпадает с названием препроцессора компилятора С, хотя практически ничего общего с ним не имеет) применяется во всех коммерческих компиляторах независимо от интерпретируемого языка. Програм- ма предварительного прохода читает исходный текст программы перед ее выполнени- ем и делает все, что нужно сделать до выполнения. В интерпретаторе Little С она вы- полняет две важные задачи: во-первых, находит и запоминает положение всех пользо- вательских функций, включая main (), и во-вторых, находит все глобальные переменные и определяет область их видимости. В интерпретаторе Little С предвари- тельный проход выполняет функция prescan (): /* Поиск всех функций программы и размещение глобальных переменных. */ void prescan(void) { char *p, *tp; char temp[32]; int datatype; int brace =0; /* Если brace равно 0, то текущая позиция указателя программы вне какой-либо функции. */ р = prog; 648 Часть VI. Интерпретатор языка С
func_index = 0; do { while(brace) { /* обход кода функции */ get__token () ; if(*token == ’{’) brace++; if(*token == ’}’) brace—; } tp = prog; /* запоминание текущей позиции */ get_token(); /* тип глобальной переменной или возвращаемого значения функции */ if(tok==CHAR || tok==INT) { datatype = tok; /* запоминание типа данных */ get_token(); if(token_type == IDENTIFIER) { strcpy(temp, token); get_token(); if(*token !='(') { /* должна быть глобальная переменная */ prog = tp; /* возврат в начало объявления */ decl_global(); } else if(*token =='(’) { /* должна быть функция */ func_table[func_index].loc = prog; func_table[func_index].ret_type = datatype; strcpy(func_table[func_index].func_name, temp); func_index++; while(*prog != ’)’) prog++; prog++; /* сейчас prog указывает на открывающуюся . фигурную скобку функции */ } else putback(); } } else if(*token == ’{’) brace++; } while(tok != FINISHED); prog = p; } Функция prescanO работает следующим образом. Каждый раз, когда встречается открывающаяся фигурная скобка, переменная brace увеличивается на 1, а когда за- крывающаяся — уменьшается на 1. Следовательно, если brace больше нуля, то теку- щая лексема находится внутри функции1. Поэтому объявление переменной считается глобальным, если оно встретилось, когда brace равно нулю. Аналогично, если при brace, равном нулю, встретилось имя функции, значит, оно принадлежит определе- нию функции (в Little С нет прототипов функций). Функция decl_global () запоминает глобальные переменные в таблице global__vars: I /* Массив этих структур содержит информацию I о глобальных переменных. I */ 1 На самом деле, конечно, данный алгоритм работает правильно только при условии, что учитываются только значащие фигурные скобки, а фигурные скобки внутри строк, например, не учитываются. (Фигурные скобки внутри строк “съедаются” программой считывания лек- сем.) — Прим. ред. Глава 29. Интерпретатор языка С 649
struct var_type { cha r va r_name[ID_LEN]; int v_type; int value; } global_vars[NUM_GLOBAL_VARS]; int gvar_index; /* индекс в таблице глобальных переменных */ /* Объявление глобальной переменной. */ void decl_global(void) { int vartype; get_token(); /* определение типа */ vartype = tok; /* запоминание типа переменной */ do { /* обработка списка */ global_vars[gvar_index].v_type - vartype; global_vars[gvar_index].value =0; /* инициализация нулем */ get_token(); /* определение имени */ strcpy(global_vars[gvar_index],var_name, token); get_token(); gvar_index++; } while(*token == ','); if(*token != ’;’) sntx_err(SEMI_EXPECTED); } Переменная целого типа gvar_index содержит индекс первого свободного эле- мента массива global_vars. Адрес каждой функции, определенной пользователем, помещается в массив func_table: struct func_type { char func_name[ID_LEN]; int ret_type; char *loc; /* адрес точки входа в файле */ } func_table[NUM_FUNC]; int func_index; /* индекс в таблице функций */ Переменная func_index содержит индекс первой свободной позиции в таблице funeatable. Функция main () Главная функция интерпретатора Little С загружает исходный текст программы, инициализирует глобальные переменные, готовит интерпретатор к вызову main() и вызывает функцию call (), которая начинает выполнение программы. Работа call () будет рассмотрена далее в этой главе. int main(int argc, char *argv[]) { if(argc != 2) { printf("Применение: littlec <имя_файла>п"); exit(1); } /* выделение памяти для программы */ if((p_buf = (char *) malloc(PROG_SIZE))==NULL) { 650 Часть VI. Интерпретатор языка С
printf("Выделить память не удалось."); exit(1); } /* загрузка программы для выполнения */ if ( ’ load__program(p__buf, argv[l])) exit(l); if(setjmp(e_buf)) exit(1); /* инициализация буфера long jump */ gvar__index = 0; /* инициализация индекса глобальных переменных */ /* установка указателя программы в начало буфера программы */ prog = p_buf; prescan(); /* найти все функции и глобальные переменные в программе */ Ivartos =0; /* инициализация индекса стека локальных переменных */ functos =0; /* инициализация индекса стека вызовов (CALL) */ /* первой вызывается функция main() */ prog = find__func("main"); /* найти точку входа программы */ if(’prog) { /* функция main() неправильна или отсутствует */ printf("main() не найдена.п"); exit(1); } prog—; /* возврат к открывающейся скобке ( */ strcpy(token, "main"); call(); /* начало интерпретации main() */ return 0; } Функция interp_block () Функция interp_block () является сердцем интерпретатора. В этой функции принимается решение о том, какое действие выполнить при прочтении очередной лексемы из входного потока. Функция интерпретирует один блок программы, после чего возвращает управление. Если блок состоит из единственного оператора, этот оператор интерпретируется и функция возвращает управление вызвавшей программе. По умолчанию interp_block () интерпретирует один оператор, после чего возвраща- ет управление вызвавшей программе. Однако, если встречается открывающаяся фи- гурная скобка, то флажок block устанавливается в 1 и функция продолжает интер- претацию операторов, пока не встретит закрывающуюся фигурную скобку. Текст функции interp_block() приведен ниже: /* Интерпретация одного оператора или блока. Когда interp_block возвращает управление после первого вызова, в main() была найдена последняя закрывающаяся фигурная скобка или оператор return. */ void interp__block (void) { int value; Глава 29. Интерпретатор языка С 651
char block « 0; do { token__type « get__token () ; /* При интерпретации одного оператора управление возвращается, как только встретилась первая точка с запятой. */ /* определение типа лексемы */ if(token_type -= IDENTIFIER) { /* Это не зарезервированное слово, обрабатывается выражение. */ putback(); /* возврат лексемы во входной поток для дальнейшей обработки функцией eval__exp() */ eval__exp (&value) ; /* обработка выражения */ if(*token!=’;') sntx_err(SEMI_EXPECTED); } else if(token__type==BLOCK) { /* если ограничитель блока */ if(*token == ’{’) /* блок */ block = 1; /* интерпретация блока, а не оператора */ else return; /* это }, возврат из функции */ } else /* это зарезервированное слово */ switch(tok) { case CHAR: case INT: /* объявление локальной переменной */ putback(); decl_local(); break; case RETURN: /* возврат из функции */ func__ret () ; return; case IF: /* обработка оператора if */ exec__if () ; break; case ELSE: /* обработка оператора else */ find__eob(); /* поиск конца блока else и продолжение выполнения */ break; case WHILE: /* обработка цикла while */ exec__while () ; break; case DO: /* обработка цикла do-while */ exec__do () ; break; case FOR: /* обработка цикла for */ exec__f or () ; break; case END: exit(0); } } while (tok != FINISHED && block); } 652 Часть VI. Интерпретатор языка С
Если не считать вызовов функции exit() или подобных ей, то интерпретация программы, написанной на языке С, кончается в одном из следующих случаев: встре- тилась последняя закрывающаяся фигурная скобка функции main(), или встретился оператор return из main(). Из-за того, что при встрече последней закрывающейся фигурной скобки main () программу нужно завершить, interp_block () выполняет только один оператор или блок, а не всю программу, хоть она и состоит из блоков. Таким образом, interp_block () вызывается каждый раз, когда встречается новый блок. Это относится не только к блокам функций, но и к блокам операторов (например, if). Следовательно, в процессе выполнения программы интерпретатор Little С вызывает interp_block () рекурсивно. Функция interp block () работает следующим образом. Сначала из входного по- тока считывается очередная лексема программы. Если это точка с запятой, то выпол- няется единственный оператор и функция возвращает управление. В противном слу- чае, выполняется проверка, является ли следующая лексема идентификатором; если да, то оператор является выражением и вызывается синтаксический анализатор выра- жений. Синтаксический анализатор должен прочесть все выражение, включая первую лексему, поэтому перед его вызовом функция putback () возвращает последнюю про- читанную лексему во входной поток. После возврата управления из eval expO to- ken содержит последнюю лексему, прочитанную синтаксическим анализатором выра- жений. Если синтаксических ошибок нет, то это должна быть точка с запятой. Если token не содержит точку с запятой, то выводится сообщение об ошибке. Если очередная лексема программы является открывающейся фигурной скобкой, то переменная block устанавливается равной 1, а если закрывающейся, то in- terp_block () возвращает управление вызвавшей программе. Если лексема является зарезервированным словом, то выполняется оператор switch, который вызывает соответствующую процедуру обработки оператора. Нумерация зарезер- вированных слов в функции get_token () нужна для того, чтобы можно было применить switch, а не if, которому пришлось бы сравнивать строки, что значительно медленнее. Функции, выполняющие операторы с зарезервированным словом, будут рассмот- рены в дальнейших разделах. Ниже приведен листинг файла с текстом программы интерпретатора. Он называет- ся LITTLEC.C. /* Интерпретатор языка Little С. */ ttinclude <stdio.h> ttinclude <setjmp.h> ttinclude <math.h> ttinclude <ctype.h> ttinclude <stdlib.h> ttinclude <string.h> ♦define NUM_FUNC 100 ♦define NUM_GLOBAL_VARS 100 ♦define NUM_LOCAL_VARS 200 ♦define NUM_BLOCK 100 ♦define ID_LEN 31 ♦define FUNC_CALLS 31 ♦define NUM_PARAMS 31 ♦define PROG_SIZE 10000 ♦define LOOP NEST 31 enum tok_types {DELIMITER, IDENTIFIER, NUMBER, KEYWORD, TEMP, STRING, BLOCK}; Глава 29. Интерпретатор языка С 653
/* сюда можно добавить дополнительные лексемы зарезервированных слов */ enum tokens {ARG, CHAR, INT, IF, ELSE, FOR, DO, WHILE, SWITCH, RETURN, EOL, FINISHED, END}; /* сюда можно добавить дополнительные двухсимвольные операторы, например, -> */ enum double_ops {LT=1, LE, GT, GE, EQ, NE}; /* Это константы вызова sntx_err() при синтаксической ошибке. Их список можно расширить. Обратите внимание: SYNTAX представляет собой нераспознанную ошибку. */ enum error_msg {SYNTAX, UNBAL_PARENS, NOJEXP, EQUALS—EXPECTED, NOT_VAR, PARAM—ERR, SEMI_EXPECTED, UNBAL_BRACES, FUNCJJNDEF, TYPE_EXPECTED, NEST_FUNC, RET_NOCALL, PAREN_EXPECTED, WHILEJEXPECTED, QUOTE—EXPECTED, NOT_TEMP, TOO-MANY-LVARS, DIV_BY_ZERO}; char *prog; /* текущая цозиция в исходном тексте программы */ char *р—buf; /* указывает на начало буфера программы */ jmp—buf е_buf; /* содержит информацию для longjmpO */ /* Массив этих структур содержит информацию о глобальных переменных. */ struct var_type { char var—name[ID—LEN]; int V—type; int value; } global—vars [NUM_GLOBAL—VARS] ; struct var_type local_var_stack[NUM_LOCAL_VARS]; struct func_type { char funC—name[ID—LEN]; int ret_type; char *loc; /* адрес точки входа в файле */ } func_table[NUM—FUNC]; int call—stack[NUM—FUNC]; struct commands { /* таблица зарезервированных слов */ char command[20]; char tok; } tablet] = { /* В эту таблицу */ ”if", IF, /* команды должны быть введены на нижнем регистре. */ "else", ELSE, "for", FOR, "do", DO, "while", WHILE, "char", CHAR, "int", INT, "return", RETURN, "end", END, "", END /* конец таблицы */ 654 Часть VI. Интерпретатор языка С
}; char token[80]; char token_type, tok; int functos; /* индекс вершины стека вызова функций */ int func_index; /* индекс в таблице функций */ int gvar_index; /* индекс в таблице глобальных переменных */ int Ivartos; /* индекс в стеке локальных переменных */ int ret_value; /* возвращаемое значение функции */ void print(void)/ prescan(void); void decl__global (void) / call (void), putback (void) ; void decl_local(void)/ local_push(struct var_type i); void eval_exp(int *value)t sntx_err(int error); void exec_if(void)t find_eob(void)t exec_for(void); void get__params (void) t get_args (void) ; void exec_while (void) / func__push (int i), exec_do (void); void assign_var(char *var_name, int value); int load_program(char *p, char *fname), find_var(char *s); void interp_block (void) / func__ret (void) ; int func_pop(void), is_var(char *s), get_token(void); char *find_func(char *name) ; int main(int argcr char *argv[]) { if(argc !« 2) { printf (’’Применение: littlec <имя_файла>п”); exit(1); } /* выделение памяти для программы */ if((p_buf = (char *) malloc (PROG_S I ZE) ) “NULL) { printf (’’Выделить память не удалось.”); exit(1); } /* загрузка программы для выполнения */ if (! load__program(p_buf, argv[l])) exit(l); if(setjmp(e_buf)) exit(l); /* инициализация буфера long jump */ gvar__index «0; /* инициализация индекса глобальных переменных */ /* установка указателя программы на начало буфера программы */ prog = p__buf; prescanO; /* определение адресов всех функций и глобальных переменных программы */ Ivartos = 0; /* инициализация индекса стека локальных переменных */ functos' = 0; /* инициализация индекса стека вызовов (CALL) */ /* первой вызывается main() */ prog = find__func (’’main”) ; /* поиск точки входа программы */ if(!prog) { /* функция main() неправильна или отсутствует */ Глава 29. Интерпретатор языка С 655
printf("main() не найдена.n”); exit(1); } prog—; /* возврат к открывающейся скобке ( */ strcpy(token, ’’main”); call(); /* начало интерпретации main() */ return 0; } /* Интерпретация одного оператора или блока. Когда interp_block возвращает управление после первого вызова, в main() встретилась последняя закрывающаяся фигурная скобка или оператор return. */ void interp_block(void) { int value; char block = 0; do { token_type = get_token(); /* При интерпретации одного оператора возврат управления после первой точки с запятой. */ /* определение типа лексемы */ if(token_type == IDENTIFIER) { /* Это не зарезервированное слово, обрабатывается выражение. */ putback(); /* возврат лексемы во входной поток для дальнейшей обработки функцией eval_exp() */ eval_exp(&value); /* обработка выражения */ if(*token!=';’) sntx_err(SEMI_EXPECTED); } else if(token_type==BLOCK) ( /* если это ограничитель блока */ if(*token ~ ’{’) /* блок */ block = 1; /* интерпретация блока, а не оператора */ else return; /* это }, возврат */ } else /* зарезервированное слово */ switch(tok) { case CHAR: case INT: /* объявление локальной переменной */ putback(); decl_local(); break; case RETURN: /* возврат из вызова функции */ func_ret(); return; case IF: /* обработка оператора if */ exec_if(); break; case ELSE: /* обработка оператора else */ 856 Часть VI. Интерпретатор языка С
find_eob(); /* поиск конца блока else и продолжение выполнения */ break; case WHILE: /* обработка цикла while */ exeC—While(); break; case DO: /* обработка цикла do-while */ exec_do(); break; case FOR: /* обработка цикла for */ exec_for(); break; case END: exit(0); } } while (tok != FINISHED && block); } /* Загрузка программы. */ r int load_program(char *p, char *fname) { FILE *fp; int i=0; if((fp=fopen(fname, "rb"))==NULL) return 0; i = 0; do { *p = getc(fp); p++; i++; } while(!feof(fp) && i<PROG_SIZE); if(*(p-2) == Oxla) *(p-2) = ’’; /* программа кончается нулевым символом */ else *(р-1) = ’’; fclose(fp); return 1; /* Найти адреса всех функций в программе и запомнить глобальные переменные. */ void prescan(void) { char *p, *tp; char temp[32]; int datatype; int brace = 0; /* Если brace = 0, то текущая позиция указателя программы находится вне какой-либо функции. */ р = prog; func_index = 0; do { while(brace) { /* обход кода функции */ get_token(); if(*token == •{’) brace++; if(*token == ’}’) brace—; } Глава 29. Интерпретатор языка С 657
tp = prog; /* запоминание текущей позиции */ get_token(); /* тип глобальной переменной или возвращаемого значения функции */ if(tok==CHAR || tok==INT) { datatype = tok; /* запоминание типа данных */ get_token(); if(token_type == IDENTIFIER) { strcpy(temp, token); get_token(); if(*token !=’(’) { /* это должна быть глобальная переменная */ prog - tp; /* возврат в начало объявления */ decl_global(); } else if(*token == ’(’) { /* это должна быть функция */ func_table[func_index].loc = prog; func_table[func_index].ret_type « datatype; strcpy(func_table[func_index].func_name, temp); f unc__index++; while(*prog ’)’) prog++; prog++; /* сейчас prog указывает на открывающуюся фигурную скобку функции */ } else putback(); } } else if(*token == ’{’) brace++; } while(tok != FINISHED); prog = p; } /* Возврат адреса точки входа данной функции. Возврат NULL, если не найдена. */ char *find_func(char *name) { register int i; for(i=0; i < func__index; i++) if(1strcmp(name, funeatable[i].func_name)) return func_table[i].loc; return NULL; } /* Объявление глобальной переменной. */ void decl__global (void) { int vartype; get_token(); /* определение типа */ vartype = tok; /* запоминание типа переменной */ do { /* обработка списка */ global_vars[gvar_index].v_type = vartype; 658 Часть VI. Интерпретатор языка С
global__vars [gvar__index] .value = 0; /* инициализация нулем */ get__token () ; /* определение имени */ strcpy(global_vars[gvar_index].varjiame, token); get__token () ; gvar__index++; } while(*token ==’,’)* if(*token != sntx_err(SEMI_EXPECTED); } /* Объявление локальной переменной. */ void decl_local(void) { struct var__type i; get_token(); /* определение типа */ i.v_type = tok; i.value = 0; /* инициализация нулем */ do { /* обработка списка */ get__token () ; /* определение имени переменной */ strcpy(i.var_name, token); local_push(i); get__token () ; } while(*token == ’,’); if(*token != ’;’) sntx_err(SEMI_EXPECTED); } /* Вызов функции. */ void call(void) { char *loc, *temp; int Ivartemp; loc = find_func(token); /* найти точку входа функции */ if(loc == NULL) sntx_err(FUNC_UNDEF); /* функция не определена */ else { Ivartemp = Ivartos; /* запоминание индекса стека локальных переменных */ get__args(); /* получение аргументов функции */ temp - prog; /* запоминание адреса возврата */ func_push(Ivartemp); /* запоминание индекса стека локальных переменных */ prog = loc; /* переустановка prog в начало функции */ get_j?arams (); /* загрузка параметров функции значениями аргументов */ interp_block(); /* интерпретация функции */ prog = temp; /* восстановление prog */ Ivartos = func_pop(); /* восстановление стека локальных переменных */ } } /* Заталкивание аргументов функции в стек локальных переменных. */ void get_args(void) { Глава 29. Интерпретатор языка С 659
int value, count, temp[NUM_PARAMS]; struct var_type i; count = 0; get_token(); if(*token != ’(’) sntx_err(PAREN_EXPECTED); /* обработка списка значений */ do { eval_exp(&value); temp[count] = value; /* временное запоминание */ get_token(); count++; }while(*token == ’,’); count—; /* затолкнуть в local_yar_stack в обратном порядке */ for(; count>=0; count—) { i.value = temp[count]; i.v_type = ARG; local__push (i) ; } } /* Получение параметров функции. */ void get_params(void) { struct var_type *p; int i; i = lvartos-1; do { /* обработка списка параметров */ get_token(); p = &local_var_stack[i]; if(*token != •)• ) { if(tok ! = INT && tok != CHAR) sntx_err(TYPE_EXPECTED); p->v_type = token_type; get_token() ; /* связывание имени параметра с аргументом, уже находящимся в стеке локальных переменных */ strcpy(p->var_name, token); get_token(); } else break; } while(*token == ’,’); if(*token != ’)') sntx_err(PAREN_EXPECTED); } /* Возврат из функции. */ void func_ret(void) { int value; value = 0; /* получение возвращаемого значения, если оно есть */ 660 Часть VI. Интерпретатор языка С
eval_exp(&value); ret_value = value; } /* Затолкнуть локальную переменную. */ void local_push (struct var__type i) { if(lvartos > NUM_LOCAL_VARS) sntx_err(TOO_MANY_LVARS); 1 oca l_var__s tack [Ivartos] ® i; lvartos++; } /* Выталкивание индекса в стеке локальных переменных. */ int func_pop(void) { functos—; if(functos < 0) sntx_err(RET_NOCALL); return call_stack[functos]; } /* Запись индекса в стек локальных переменных. */ void func_push(int i) { if(functos>NUM_FUNC) sntx_err(NEST_FUNC); call_stack[functos] = i; functos+4-; } /* Присваивание переменной значения. */ void assign__var (char *var_name, int value) { register int i; /* проверка наличия локальной переменной */ for(i=lvartos-l; i >= call_stack[functos-1]; i—) { if(!strcmp(local_var_stack[i].var_name, var_name)) { local_var_stack[i].value = value; return; } } if(i < call_stack[functos-1]) /* если переменная не локальная, ищем ее в таблице глобальных переменных */ for(i=0; i < NUM_GLOBAL_VARS; i++) if(!strcmp(global_vars[i].var_name, var_name)) { global_vars[i].value = value; return; } sntx_err(NOT—VAR); /* переменная не найдена */ } /* Получение значения переменной. */ int find__var (char *s) { Глава 29. Интерпретатор языка С 661
register int i; /* проверка наличия переменной */ for (i=lvartos-l; i >= call__stack [functos-1] ; i—) if(!strcmp(1ocal_yar_stack[i].var_name, token)) return 1 ocal_var__stack[i] .value; /* в противном случае проверим, может быть это глобальная переменная */ for(i=0; i < NUM_GLOBAL_VARS; i++) if(!strcmp(global_vars[i].var_name, s)) return global_vars[i].value; sntx_err(NOT_VAR); /* переменная не найдена */ return -1; /* Если идентификатор является переменной, то возвращается 1, иначе 0. */ int is_yar(char *s) { register int i; /* это локальная переменная? */ for(i=lvartos-l; i >= call_stack[functos-1]; i—) if (! strcmp (local_var__stack [i] .var_name, token) ) return 1; /* если нет - поиск среди глобальных переменных */ for(i=0; i < NUM_GLOBAL_VARS; i++) if (! strcmp (global_vars [i] . varjiame, s) ) return 1; return 0; /* Выполнение оператора if. */ void exec__if (void) { int cond; eval_exp(ficond); /* вычисление if-выражения */ if(cond) { /* истина - интерпретация if-предложения */ interp_block(); } else { /* в противном случае пропуск if-предложения и выполнение else-предложения, если оно есть */ find_eob(); /* поиск конца блока */ get_token(); if(tok != ELSE) { putback(); /* восстановление лексемы, если else-предложение отсутствует */ return; } interp__block () ; 662 Часть VI. Интерпретатор языка С
} } /* Выполнение цикла while. */ > void exec_while(void) * { int cond; char *temp; putback(); temp = prog; /* запоминание адреса начала цикла while */ get_token(); eval_exp(&cond); /* вычисление управляющего выражения */ if(cond) interp_block(); /* если оно истинно, то выполнить интерпретацию */ else { /* в противном случае цикл пропускается */ find_eob(); return; } prog = temp; /* возврат к началу цикла */ } /* Выполнение цикла do. */ void exec_do(void) { int cond; char *temp; putback(); temp = prog; /* запоминание адреса начала цикла */ get_token(); /* найти начало цикла */ interp_block(); /* интерпретация цикла */ get_token(); if(tok != WHILE) sntx_err(WHILE_EXPECTED); eval_exp(&cond); /* проверка условия цикла */ if(cond) prog = temp; /* если условие истинно, то цикл выполняется, в противном случае происходит выход из цикла */ } /* Поиск конца блока. */ void find_eob(void) { int brace; get_token(); brace = 1; do { get_token(); if(*token == ’{’) brace++; else if(*token == •}’) brace—; } while(brace); } /* Выполнение цикла for. */ void exec_for(void) { Глава 29. Интерпретатор языка С юз
int cond; char *temp, *temp2; int brace ; get_token(); eval_exp(&cond); /* инициализирующее выражение */ if(*token != ';') sntx_err(SEMI_EXPECTED); prog++; /* пропуск ; */ temp = prog; for(;;) { eval_exp(&cond); /* проверка условия */ if(*token != ';') sntx_err(SEMI_EXPECTED); prog++; /* пропуск ; */ temp2 = prog; /* поиск начала тела цикла */ brace = 1; while(brace) { get_token(); if(*token == '(’) brace++; if(*token == ’)’) brace—; } if (cond) interp__block () ; /* если условие выполнено, то выполнить интерпретацию */ else { /* в противном случае обойти цикл */ find_eob(); return; } prog = temp2; eval_exp(&cond) ; /* выполнение инкремента */ prog = temp; /* возврат в начало цикла */ } } Обработка локальных переменных Когда интерпретатор встречает зарезервированные слова int или char, он вы- зывает функцию decl_local (), которая размещает локальные переменные. Как отмечалось ранее, при выполнении программы интерпретатор не может встретить объявление глобальной переменной, потому что выполняется только код про- граммы, записанный внутри функций. Следовательно, если встретилось объявле- ние переменной, то это локальная переменная (или параметр — этот случай будет рассмотрен в следующем разделе). В структурированных языках локальные пере- менные хранятся в стеке. Если программа компилируется, то обычно использует- ся системный стек. Однако при интерпретации стек с локальными переменными должен быть создан самим интерпретатором. В интерпретаторе Little С стек для локальных переменных хранится в массиве local_var_stack. Каждый раз, когда встречается локальная переменная, ее имя, тип и значение (первоначально равное нулю) заносятся в стек при помощи функции local_push (). Глобальная пере- менная Ivartos является указателем стека. (Соответствующей функции извлече- ния из стека нет. Вместо этого стек локальных переменных переустанавливается каждый раз при возврате управления из функции local_push (). Зачем это сде- лано, будет видно из дальнейшего изложения.) Функции decl_local() и 1о- cal_push() приведены ниже: 664 Часть VI. Интерпретатор языка С
/★ Объявление локальной переменной. */ void decl_local(void) { struct var_type i; get_token(); /* определение типа */ i.v_type = tok; i.value =0; /* инициализация нулем */ do { /* обработка списка */ get_token(); /* определение имени переменной */ strcpy(i.var_name, token); local_push(i); get_token(); } while(*token == ','); if(*token != ’;’) sntx_err(SEMI_EXPECTED); } /* Затолкнуть локальную переменную в стек. */ void local_push(struct var_type i) { if(lvartos > NUM_LOCAL_VARS) sntx_err(TOO—MANY—LVARS); local—var_stack[Ivartos] = i; lvartos++; } Функция decl—local () сначала считывает тип объявленной переменной (или пе- ременных) и инициализирует переменные нулями. Затем в цикле считывается список идентификаторов, разделенных запятыми. При каждой итерации цикла информация об очередной переменной заносится в стек локальных переменных. В конце функции decl_local () проверяется, является ли последняя лексема точкой с запятой. Вызов функций, определенных пользователем Выполнение функций, определенных пользователем, — это, наверное, самая труд- ная часть в реализации интерпретатора С. Интерпретатор должен начать чтение ис- ходного текста программы с нового места, а затем вернуться в вызывающую процеду- ру после выхода из функции. Кроме того, он должен выполнить следующие три зада- чи: передать аргументы, разместить в памяти параметры и вернуть значение функции. Все вызовы функций (кроме main()) осуществляются в синтаксическом анализа- торе выражений из функции atom() с помощью вызова функции са11(). Именно функция call () выполняет все необходимые при вызове функций действия. Текст функции call () вместе с ее вспомогательными функциями приведен ниже. Рассмот- рим эту функцию подробнее: /* Вызов функции. */ void call(void) { char *loc, *temp; int Ivartemp; loc = find—func(token); /* поиск точки входа функции */ if(loc == NULL) Глава 29. Интерпретатор языка С 665
sntx_err(FUNC_UNDEF); /* функция не определена */ else { Ivartemp = Ivartos; /* запоминание индекса стека локальных переменных */ get_args(); /* получение аргументов функции */ temp = prog; /* запоминание адреса возврата */ func_push(Ivartemp); /* запоминание индекса стека локальных переменных */ prog = loc; /* установить prog в начало функции */ get_params(); /* загрузка параметров функции значениями аргументов */ interp_block(); /* интерпретация функции */ prog = temp; /* восстановление prog */ Ivartos = func_pop(); /* восстановление стека локальных переменных */ } ' } /* Занесение, аргументов•функции в стек локальных переменных. */. void get_args(void) { int value, count, temp[NUM_PARAMS]; struct var_type i; count = 0; get_token(); if(*token != '(') sntX—err(PAREN_EXPECTED); /* обработка списка значений */ do { eval_exp(&value); temp[count] = value; 7* временное запоминание */ get_token(); count++; }while(*token == ','); count—; /* теперь записать в стек local_var_stack в обратном порядке */ for(; count>=0; count--) { i.value = temp[count]; i.v_type = ARG; < local_push(i); } /* Получение параметров функции. */ void get_params(void) { struct var__type *p; int i; i = lvartos-1; do { /* обработка списка параметров */ get_token(); p = &local_var_stack[i]; if(*token != ')’ ) { if(tok != INT && tok != CHAR) sntx_err(TYPE_EXPECTED); 666 Часть VI. Интерпретатор языка С
p->v_type = token_type; get_token(); /* связывание имени параметра с аргументом, который уже находится в стеке локальных переменных */ strcpy(p->var_name, token); get_token(); i—; } else break; } while(*token == ’,’); if(*token != ’)’) sntx_err (PAREN__EXPECTED) ; } В первую очередь с помощью вызова функции f ind_func () функция call () на- ходит адрес точки входа вызываемой функции в исходном тексте программы. Затем эта функция сохраняет текущее значение Ivartos индекса стека локальных перемен- ных в переменной Ivartemp. Потом она вызывает функцию get_args (), которая об- рабатывает все аргументы функции. Функция get_args () считывает список выраже- ний, разделенных запятыми, и заносит их в стек локальных переменных в обратном порядке. (Обратный порядок занесения переменных применяется потому, что так их легче сопоставлять с соответствующими параметрами.) Значения переменных, запи- санные в стек, не имеют имен (стек — это всего лишь массив). Имена параметров даются им функцией get_params (), которая будет рассмотрена далее. После обработки аргументов функции текущее значение указателя prog сохраня- ется в temp. Эта переменная указывает на точку возврата функции. После этого зна- чение Ivartemp заносится в стек вызова функций. Доступ к этому стеку осуществля- ется с помощью функций func_push () и func__pop (). В данный стек при каждом вызове функции записывается значение Ivartos. Значение Ivartos представляет со- бой начальную точку в стеке локальных переменных для переменных (и параметров) вызванной функции. Значение на вершине стека вызова функций используется для предотвращения доступа функции к переменным, которые в ней не объявлены. Следующие две строки функции call О устанавливают указатель программы на начало функции и затем, вызывая функцию get_params (), устанавливают соответст- вие между формальными параметрами и значениями аргументов функции, которые уже находятся в стеке локальных переменных. Фактическое выполнение функции осуществляется вызовом interp_block (). После возврата управления из in- terp_block() указатель программы prog переустанавливается; он будет указывать на точку возврата, а индекс стека локальных переменных получит значение, которое он имел до вызова функции. На этом последнем шаге из стека фактически удаляются все локальные переменные функции. Если вызванная функция содержит оператор return, то interp_block() перед возвратом в call () вызывает func_ret(), которая вычисляет возвращаемое значе- ние; код этой функции приведен ниже: /* Возврат из функции. */ void func_ret(void) { int value; value = 0; /* вычисление возвращаемого значения, если оно есть */ eval_exp(&value); ret__value = value; } Глава 29. Интерпретатор языка С 667
Глобальная целочисленная переменная ret__value содержит возвращаемое функ- цией значение. На первый взгляд может показаться странным то, что локальной пе- ременной value сначала присваивается возвращаемое значение функции, а затем это значение присваивается переменной ret_value. Причина здесь в том, что функции могут быть рекурсивными и функция eval_exp() для вычисления возвращаемого значения может вызвать ту же функцию. Присваивание значений переменным Возвратимся ненадолго к синтаксическому анализатору выражений. Когда встреча- ется оператор присваивания, то сначала вычисляется значение выражения в правой части, а затем это значение присваивается переменной в левой части путем вызова assign_var (). Однако язык С структурирован и поддерживает глобальные и локаль- ные переменные. Как же тогда в следующей программе int count; int main() { int county i; count = 100; i = f(); return 0; } int.f () { int count; count = 99; return count; } функция assign_var() знает, какой именно переменной count нужно присвоить значение? Ответ на это простой: во-первых, локальные переменные имеют приоритет над одноименными глобальными, а, во-вторых, локальные переменные недоступны за пределами своих функций. Проанализируем, как применяются эти правила для раз- решения коллизий в приведенных выше примерах операторов присваивания. Для этого рассмотрим функцию assign_var (): /* Присваивание значения переменной. */ void assign_var(char *var_name, int value) { register int i; /* сначала нужно узнать: это локальная переменная? */ for(i=lvartos-l; i >= call_stack[functos-1]; i—) { if (! strcmp (local_var_stack [i] . var__name, var_name)) { local_var_stack[i].value = value; return; } } if(i < call_stack[functos-1]) /* Если это не локальная переменная, попытаемся найти ее в таблице глобальных переменных */ 668 Часть VI. Интерпретатор языка С
for(i=0; i < NUM__GLOBAL_VARS; i++) if(!strcmp(global_vars[i].var_name, var_name)) { global—vars[i].value = value; return; } sntx_err(NOT—VAR); /* переменная не найдена */ } Как указывалось в предыдущем разделе, при каждом вызове функции индекс стека локальных переменных (Ivartos) записывается в стек вызова функции. Это значит, что любая локальная переменная (или параметр), определенные в функции, будут за- писаны в стек выше точки, на которую указывает ivartos. Следовательно, функция assign—var () просматривает local_var_stack, начиная с текущего значения на верхушке стека, причем просмотр прекращается, когда индекс достигает того значе- ния, которое было занесено при последнем вызове функции. Благодаря этому про- сматриваются только те переменные, которые являются локальными для данной функции. (Это также помогает вызывать рекурсивные функции, потому что текущее значение ivartos сохраняется при каждом вызове функции.) Таким образом, если в main () присутствует строка “count = 100; ”, то assign_var () находит локальную переменную count внутри main(). В f|) функция assign_var() находит перемен- ную count, определенную в f (), а не в main (). Если имя переменной не совпадает ни с одним из имен локальных переменных, то просматривается список глобальных переменных. Выполнение оператора if Итак, базовая структура интерпретатора Little С создана. Теперь к ней можно до- бавить некоторые управляющие операторы. Каждый раз, когда функция in- terp_block() встречает оператор с зарезервированным словом, она вызывает соот- ветствующую функцию, обрабатывающую этот оператор. Один из самых легких опе- раторов — if. Он обрабатывается функцией exec if (): /* Выполнение оператора if. */ void ехес_if(void) { int cond; eval—exp(&cond); /* вычисление выражения */ if(cond) { /* если условие выполнено, то интерпретировать if-предложение */ interp_block(); } else { /* в противном случае пропустить if-предложение и выполнить else-предложение, если оно есть */ find—eob(); /* поиск конца блока */ get—token(); if(tok != ELSE) { putback(); /* восстановление лексемы, если else-предложение отсутствует */ return; } interp—block(); } } Глава 29. Интерпретатор языка С 669
Рассмотрим эту функцию подробнее. В первую очередь она вызывает eval_exp () для вычисления значения условного выражения. Если условие (cond) выполнено (т.е. выражение имеет ненулевое значение), то функция вызывает рекурсивно in- terpjDlockO , выполняя тем самым блок if. Если.cond ложно, вызывается функция find_eob(), которая передвигает указатель программы на оператор, следующий по- сле блока if. Если там присутствует else-предложение, то оно обрабатывается функ- цией exec_if() и выполняется блок else. В противном случае выполняется сле- дующий оператор программы. Если блок else присутствует и выполняется блок if, то после его выполнения нужно каким-то образом обойти блок else. Эта задача выполняется в функции in- terp_block () путем вызова функции f ind_eob (), которая обходит блок после else. Запомните, что в синтаксически правильной программе else обрабатывается функци- ей interp_block() только в одном случае — после выполнения блока if. Если вы- полняется блок else, то оператор else обрабатывается функцией exec_if (). Обработка цикла while Интерпретировать цикл while, как и if, довольно легко. Ниже приведен текст функции exec_while (), которая интерпретирует while: /* Выполнение цикла while. */ void exec while(void) { int cond; char *temp; putback(); temp = prog; /* запомнить адрес начала цикла while */ get_token(); eval_exp(&cond); /* проверка управляющего выражения */ if(cond) interp_block(); /* если оно истинно, то интерпретировать */ else { /* в противном случае цикл пропускается */ find_eob(); return; } prog = temp; /* возврат к началу цикла */ } Функция exec_while() работает следующим образом. Сначала лексема while возвращается во входной поток, а ее адрес сохраняется в переменной temp. Этот адрес будет использован интерпретатором, чтобы возвратиться к началу цикла (т.е. начать следующую итерацию с начала цикла while). Далее лексема while считывается зано- во для того, чтобы удалить ее из входного потока. После этого вызывается функция eval_exp(), которая вычисляет значение условного выражения цикла while. Если условие выполнено (т.е. условное выражение принимает значение ИСТИНА), то ре- курсивно вызывается функция interp_block (), которая интерпретирует блок while. После возврата управления из interp_block () программный указатель prog уста- навливается на начало цикла while и управление передается функции interp_block (), в которой весь процесс повторяется. Если условное выражение оператора while принимает значение ЛОЖЬ, то происходит поиск конца блока while, а затем выход из функции exec_while (). 670 Часть VI. Интерпретатор языка С
Обработка цикла do-while Обработка цикла do-while во многом похожа на обработку цикла while. Когда функция interp_block () встречает оператор do, она вызывает функцию exec_do (), исходный текст которой приведен ниже: /* Выполнение цикла do. */ void exec_do(void) { int cond; char *temp; putback(); temp = prog; /* запомнить адрес начала цикла */ get_token(); /* прочитать начало цикла */ interp_block(); /* интерпретировать цикл */ get_token(); if(tok != WHILE) sntx_err(WHILE_EXPECTED); eval__exp(&cond) ; /* проверка условия цикла */ if(cond) prog = temp; /* если условие выполнено, то цикл выполняется; в противном случае происходит выход из цикла */ } Главное отличие цикла do-while от цикла while состоит в том, что блок do- while выполняется всегда как минимум один раз, потому что его условное выраже- ние находится после тела цикла. Поэтому exec_do сначала запоминает адрес начала цикла в переменной temp, а затем рекурсивно вызывает interp block (), которая интерпретирует блок, ассоциированный с циклом. После возврата управления из in- terp_block () идет поиск соответствующего слова while и вычисляется значение ус- ловного выражения. Если условие выполнено, то prog устанавливается так, что его значение указывает на начало цикла, в противном случае выполнение продолжается со следующего оператора. Цикл for Интерпретация цикла for — задача значительно более трудная, чем интерпретация других операторов. Частично это объясняется тем, что структура цикла for в языке С была задумана в предположении компиляции программы. Главная трудность заключа- ется в том, что условное выражение for должно проверяться в начале цикла, а часть приращения должна быть выполнена в его конце. Таким образом, эти две части цикла for в тексте исходной программы находятся рядом, а их интерпретация разделена выполнением блока цикла. Однако, после некоторой дополнительной работы, цикл for все же может быть интерпретирован. Когда interp block () встречает оператор for, вызывается функция exec for (), текст которой приведен ниже: /* Выполнение цикла for. */ void exec_for(void) { int cond; char *temp, *temp2; int brace ; get_token(); eval_exp(&cond); /* инициализирующее выражение */ Глава 29. Интерпретатор языка С 671
if(*token ! = ’sntx_err(SEMI_EXPECTED); prog++; /* пропуск ; */ temp = prog; for(;;) { eval_exp(&cond); /* проверка условия */ if(*token != sntx_err(SEMI_EXPECTED); prog++; /* пропуск ; */ temp2 = prog; /* поиск начала тела цикла */ brace = 1; while(brace) { get_token(); if(*token ’(’) brace++; if(*token == ’)’) brace—; } if(cond) interp—block(); /* если условие выполнено, то тело цикла интерпретируется */ else { /* в противном случае цикл пропускается */ find_eob(); return; } prog = temp2; eval—exp(&cond); /* выполнение инкремента */ prog = temp; /* возврат к началу цикла */ } } Сначала функция обрабатывает инициализирующее выражение цикла for. Часть инициализации for выполняется только один раз; эта часть не подвергается цикличе- ской обработке. Затем указатель программы устанавливается так, чтобы он указывал на символ, следующий сразу после той точки с запятой, которой заканчивается часть инициализации. Наконец, после этого значение указателя присваивается переменной temp. А затем организовывается цикл, в котором проверяется условная часть цикла for и переменной temp2 присваивается адрес начала части приращения. Далее про- изводится поиск начала тела цикла и его (тела) интерпретация, если условное выра- жение принимает значение ИСТИНА. (В противном случае производится поиск кон- ца тела цикла и выполнение продолжается с оператора, следующего после цикла for.) После рекурсивного вызова interp_block () выполняется часть приращения, после чего весь процесс повторяется. @ Библиотечные функции Little С Программы С, выполняемые интерпретатором Little С, никогда не компилируются и не компонуются, поэтому любая используемая в них библиотечная функция должна быть обработана непосредственно интерпретатором Little С. Лучше всего для этого создать интерфейсную функцию, вызываемую интерпретатором каждый раз при встрече библиотечной функции. Интерфейсная функция осуществляет подготовку к вызову библиотечной функции и обрабатывает возвращаемые значения. В связи с ограниченным размером интерпретатора, Little С содержит только пять “библиотечных” функций: getche О, putch(), puts(), print () и getnum(). Ко- нечно, в Стандарт С входит только функция puts О, выводящая строку на экран. Функция getche (), хотя и не предусмотрена в Стандарте, обычно включается в со- 672 Часть VI. Интерпретатор языка С
став библиотек, так как она используется при работе в интерактивной среде. Она ожидает нажатия клавиши и возвращает связанное с ней значение. Следует отметить, что эта функция предусмотрена во многих компиляторах. Функция putchO также определена во многих компиляторах, предназначенных для создания программ, рабо- тающих в интерактивной среде. Она выводит на консоль один символ — ее аргумент. Вывод не буферизован. Функции getnum () и print () созданы автором. Функция getnum () возвращает целое число, равное числу, введенному с клавиатуры. Функция print () может выводить на экран как строковый, так и целочисленный аргумент, это очень удобно. Прототипы этих библиотечных функций приведены ниже1: int getche(void); /* считывание символа с клавиатуры и возврат его значения */ int putch(char ch); /* вывод символа на экран */ int puts(char *s) ; /* вывод строки на экран */ int getnum(void); /* чтение целого числа с клавиатуры и возврат его значения */ int print(char *s); /* вывод строки на экран */ или int print(int i); /* вывод целого числа на экран */ Тексты процедур библиотеки функций Little С приведены ниже. Файл называется LCLIB.C. /****** Библиотека функций Little С ★★*****/ /* Сюда можно добавлять новые функции. */ #include <conio.h> /* если компилятор не поддерживает данный заголовочный файл, этот #include можно удалить */ #include <stdio.h> #include <stdlib.h> extern char *prog; /* указывает на текущий символ в программе */ extern char token[80]; /* содержит строковое представление лексемы */ extern char token_type; /* содержит тип лексемы */ extern char tok; /* содержит внутреннее представление лексемы */ enum tok_types {DELIMITER, IDENTIFIER, NUMBER, KEYWORD, TEMP, STRING, BLOCK}; /* Эти константы используются для вызова sntx_err() при синтаксической ошибке. Их список можно расширить. ВНИМАНИЕ: SYNTAX обозначает нераспознанную ошибку. */ enum error_msg {SYNTAX, UNBAL_PARENS, NO_EXP, EQUALS_EXPECTED, NOT—VAR, PARAM—ERR, SEMI_EXPECTED, UNBAL_BRACES, FUNC_UNDEF, TYPE_EXPECTED, NEST—FUNC, RET—NOCALL, PAREN_EXPECTED, WHILE_EXPECTED, QUOTE_EXPECTED, NOT_STRING, TOO_MANY_LVARS, DIV_BY_ZERO}; int get—token(void); void sntx_err(int error), eval_exp(int *result); 1 Язык Little С не поддерживает прототипы функций. Поэтому включать их в программу не следует. Здесь прототипы приведены в качестве справочной информации. —Прим, перев. Глава 29. Интерпретатор языка С 673
void putback(void); /* Считывание символа с консоли. Если компилятор не поддерживает _getche(), то следует использовать getchar(). */ int call_getche() { char ch; ch = __getche () ; while(*prog!=1)’) prog++; prog++; /* продвижение к концу строки */ return ch; /* Вывод символа на экран. */ int call_putch() { int value; eval_exp(&value); printf("%c", value); return value; /* Вызов функции puts(). */ int call_puts(void) { get_token(); if(*token!=’(’) sntx_err(PAREN_EXPECTED); get_token(); if(token_type!=STRING) sntx_err(QUOTE_EXPECTED); puts(token); get_token(); if(*token!=’)’) sntx_err(PAREN_EXPECTED); get_token(); if(*token!=’;’) sntx_err(SEMI_EXPECTED); putback(); return 0; /* Встроенная функция консольного вывода. */ int print(void) { int i; get_token () ; if(*token!=’(1) sntx_err(PAREN_EXPECTED); get_token(); if(token_type==STRING) { /* вывод строки */ printf("%s ”, token); } else { /* вывод числа */ putback(); eval_exp(&i); printf("%d ", i); } get_token(); 674 Часть VI. Интерпретатор языка С
if(*token!=’)’) sntx_err(PAREN_EXPECTED); get—token(); if(*token!=';') sntx_err(SEMI_EXPECTED); putback(); return 0; } /* Считывание целого числа с клавиатуры. */ int getnum(void) { char s [80]; gets (s); while(*prog != ')') prog++; prog++; /* продвижение к концу строки */ return atoi(s); } Для того чтобы добавить в библиотеку новые функции, следует сначала включить их имена и адреса интерфейсных функций в массив intern_func. После этого необ- ходимо создать соответствующие интерфейсные функции, используя приведенные выше функции как пример. И Компиляция и компоновка интерпретатора Little С Три файла, составляющие интерпретатор Little С, должны быть откомпилированы и скомпонованы совместно. Для этого можно использовать любой современный ком- пилятор С, включая Visual C++. Если используется Visual C++, то последовательность команд вызова компилятора выглядит следующим образом: Icl “С parser.с cl -с Iclib.c cl littlec.c parser.obj Iclib.obj В некоторых версиях Visual C++ интерпретатору Little С может не хватить памяти для стека. В этом случае для увеличения стека нужно использовать опцию /F. Память, выделенная опцией /F6000, в большинстве случаев будет достаточной. Однако при интерпретации программ с глубоко вложенными рекурсивными вызовами эта память может оказаться все же недостаточной. При использовании других компиляторов С нужно руководствоваться прилагае- мыми к ним инструкциями. Н Демонстрация Little С Работа интерпретатора Little С демонстрируется с помощью следующих при- меров программ. Программа №1 иллюстрирует все средства программирования, поддерживае- мые в Little С: I/* Little С. Демонстрационная программа №1. Эта программа демонстрирует работу всех средств Глава 29. Интерпретатор языка С 675
языка С, поддерживаемых интерпретатором Little С. */ int if j; /* глобальные переменные */ char ch; int main() { int if j; /* локальные переменные */ puts (’’Программа демонстрации Little С.”); print_alpha(); do { puts (’’Введите число (0r если выход): ”) ; i = getnum(); if(i < 0 ) { puts ("числа должны быть положительными, введите еще’’); } else { for(j = 0; j < i; j=j+l) { ; print(j); print("сумма равна"); print(sum(j)); puts (’”’) ; } } } while(i!=0); return 0; } /* Сумма чисел от 0 до введенного числа. */ int sum(int num) { int running_sum; running_sum = 0; while(num) { running_sum = running_sum + num; num = num - 1; } return running_sum; } /* Вывод на экран английского алфавита. */ int print_alpha() { for(ch = ’A’; ch<=’Z’; ch = ch + 1) { putch(ch); } puts ("") ; return 0; } 676 Часть VI. Интерпретатор языка С
Следующий пример демонстрирует использование вложенных циклов: /* Пример с вложенными циклами. */ int main() { int i, j, k; for(i =0; i<5; i = i + 1) { for(j =0;j<3;j=j+l) { for(k = 3; k ; k = k - 1) { print(i); print(j); print(k); puts(""); } } } puts("выполнено"); return 0; } Следующая программа демонстрирует работу оператора присваивания: /* Присваивание как операция. */ int main() { int a, b; а = b = 10; print(a); print(b); while(a=a-l) { print(a); do { print(b); } while((b=b-l) > -10); } return 0; } Следующая программа демонстрирует выполнение рекурсивных функций. В ней функция f act г () вычисляет факториал числа. /* Демонстрация рекурсивных функций. */ /* возвращает факториал числа i */ int factr(int i) { if(i<2) { return 1; } else { return i * factr(i-1); } } int main() Гдава 29. Интерпретатор языка С 677
print("Факториал от 4 равен: "); print(factr(4) ) ; return 0; } В следующей программе демонстрируются различные приемы использования ар- гументов функций: /* Использование аргументов функций. */ int fl(int a, int b) { int count; print("в функции fl"); count = a; do { print(count); } while(count=count-l); print(a); print(b); print(a*b); return a*b; int f2(int a, int x, int y) { print(a); print(x); print(x / a); print(y*x); return 0; int main() { f2(10, fl(10, 20), 99); return 0; } И, наконец, в последнем примере демонстрируется работа операторов цикла: /* Операторы цикла. */ int main() { int а; char ch; /* цикл while */ puts("Введите число: "); a = getnum(); while (a) { print(a); print(a*a); puts(”"); a = a - 1; 678 Часть VI. Интерпретатор языка С
} /* цикл do-while */ puts("Введите символ, если выход, то ’q’ "); do { ch = getche(); } while(ch !=’q’); /* цикл for */ for(a=0; a<10; a = a + 1) { print(a); } return 0; } И Усовершенствование интерпретатора Little С Рассмотренный в этой главе интерпретатор Little С разрабатывался таким образом, что- бы его принцип действия был, по возможности, очевидным. При разработке интерпрета- тора преследовалась цель сделать его максимально легким для понимания. Другой целью было сделать его легко расширяемым. Поскольку преследовались именно эти цели, интер- претатор Little С не обладает значительным быстродействием или эффективностью. Одна- ко базовая структура интерпретатора корректна, а скорость выполнения программ можно увеличить, пользуясь указаниями, приведенными в этом разделе. Фактически во всех коммерческих интерпретаторах роль программы предварительного прохода значительно шире, чем в Little С. Интерпретируемый исходный текст программы преобразуется из формы ASCII, которая удобна программисту для чтения, во внутреннюю форму. В этой внутренней форме все, кроме заключенных в двойные кавычки строк и констант, преобразуется в лексемы, состоящие из одного числа, аналогично тому, как это делает Little С для зарезервированных слов. При работе интерпретатора Little С довольно часто сравниваются строки. Например, всякий раз при поиске переменной или функции выполняется несколько сравнений строк. Процедура сравнения строк занимает много времени, что конечно же значительно снижает быстродействие программы. Но если каж- дую лексему исходной программы преобразовать в целое число, то можно использовать намного более быстродействующую операцию сравнения целых чисел. Преобразование исходного текста программы во внутреннюю форму — единственное наиболее существенное изменение, повышающее эффективность Little С. Благодаря этому преобразованию повы- шение скорости будет весьма ощутимым. Другой способ улучшения, полезный главным образом для интерпретации боль- ших программ, — создание специальных процедур для поиска переменных и функ- ций. Применяющийся метод поиска основан на последовательном просмотре имен переменных и программ. Такой просмотр занимает много времени, даже если имена переменных и программ преобразованы в целочисленные лексемы. Однако можно за- менить этот метод поиска другим, более быстрым методом, используя, например, двоичное дерево или один из методов хэширования. Как указывалось ранее, одним из ограничений Little С по сравнению с граммати- кой полного С является требование заключать объекты некоторых операторов, таких как if, в фигурные скобки независимо от того, единственный это оператор или блок операторов. Это предусмотрено с целью существенного упрощения функции find_eob(), которая ищет конец блока после выполнения одного из управляющих операторов. Функция find_eob() попросту ищет закрывающуюся скобку, соответст- вующую скобке, открывающей блок. Устранение этого ограничения будет интересным Глава 29. Интерпретатор языка С 679
упражнением для читателя. Для этого можно, например, усовершенствовать функцию find_eob() таким образом, чтобы она искала конец оператора, выражения или бло- ка. Однако следует иметь в виду, что для операторов if, while, do-while и for по- требуются различные подходы, если в них используется единственный оператор. В Расширение Little С Расширять возможности интерпретатора Little С можно в двух направлениях: до- бавлять в него новые средства языка С и дополнительные средства программирова- ния. Эти усовершенствования кратко рассматриваются в следующих разделах. Добавление новых средств в язык Little С Существует две категории операторов С, которые можно включить в Little С. В первую категорию входят дополнительные выполняемые операторы С, такие как switch, goto, break и continue. Если предыдущий материал изучен достаточно тщательно, то их добавление в Little С не составит большого труда. Во вторую категорию входит поддержка новых типов данных. В интерпретаторе Little С для этого есть некоторые “зацепки”. Например, в структуре var_type есть поле для типов переменных. Для включения дополнительных базовых типов (например, float, double или long) нужно просто увеличить размер поля до размера наибольшего элемента. Учтите, что реализация указателей не труднее, чем реализация других типов дан- ных. Однако для этого нужно будет добавить в синтаксический анализатор выражений поддержку операций для работы с указателями. После реализации операций для работы с указателями легко добавить массивы. Память для массива следует выделять динамически, используя malloc О , а указатель на массив нужно хранить в поле value структуры var_type. Более трудная задача — добавление структур и объединений. Проще всего это сделать, используя malloc () для выделения объекту памяти, причем указатель на объект нужно сохранить в поле value структуры var type. (Для обработки передачи структур и объеди- нений в качестве параметров нужно будет написать специальную программу.) Для поддержки различных типов возвращаемых функциями значений нужно использо- вать поле ret_type структуры func_type. Это поле определяет тип возвращаемых функ- цией данных. В текущей версии интерпретатора оно объявлено, но не используется. Можно также добавить в Little С поддержку комментариев вида //. Это нетрудно сделать, изменив функцию get_token (). И наконец, в интерпретатор Little С несложно добавить средства, не входящие в состав языка С. Это особенно увлекательное упражнение — заставить интерпретатор делать то, что в языке не предусмотрено. Например, можно добавить конструкцию языка Pascal REPEAT-UNTIL. Если при этом возникают трудности, как средство отлад- ки можно использовать вывод каждой лексемы в процессе ее обработки. Создание дополнительных средств программирования Кроме средств языка, в интерпретатор несложно добавить также новые средства программирования. Например, можно добавить средства трассировки, выводящие на экран в процессе выполнения программы каждую лексему отдельно. Еще можно до- бавить возможность вывода значений переменных при выполнении программы, а также, например, встроенный редактор, который позволит редактировать и выполнять программу без перехода в автономный редактор. 680 Часть VI. Интерпретатор языка С
Предметный указатель # ##, оператор, 250 #, модификатор, 208 #, оператор, 250 #define, директива препроцессора, 242 #elif, директива препроцессора, 246 #else, директива препроцессора, 246 #endif, директива препроцессора, 246 #еггог, директива препроцессора, 244 #if, директива препроцессора, 245 #ifdef, директива препроцессора, 247 tifndef, директива препроцессора, 248 #include, директива препроцессора, 245 #Ипе, директива препроцессора, 249 #pragma, директива препроцессора 250 #undef, директива препроцессора, 248 % %%, спецификатор преобразования, 202. 209 %[], спецификатор преобразования, 209 %а, спецификатор преобразования, 202; 209 %с, спецификатор преобразования, 202; 209 %d, спецификатор преобразования, 202; 209 %е, спецификатор преобразования, 202; 209 %f, спецификатор преобразования, 202; 209 %g, спецификатор преобразования, 202; 209 %i, спецификатор преобразования, 202; 209 %п, спецификатор преобразования, 202; 209 %о, спецификатор преобразования, 202; 209 %р, спецификатор преобразования, 202; 209 %s, спецификатор преобразования, 202; 209 %и, спецификатор преобразования, 202, 209 %х, спецификатор преобразования, 202; 209 * ♦, модификатор, 208 • ?, оператор, 87 2, специальная символьная константа, 62 , специальная символьная константа, 62 __DATE____, макрос, 251 __FILE____, идентификатор, 249 func , идентификатор 268 __LINE____, идентификатор, 249 __STDC____, макрос, 251 __STDC_HOSTED______, макрос, 251; 264 __STDC_IEC_559_____, макрос, 264 __STDCJEC_559_COMPLEX________, макрос, 264 __STDCJSO_10646_____, макрос, 264 __STDC_VERSION_____, макрос, 251; 264 __TIME____, макрос, 251 _Воо1, 259; 434 -Complex, 260; 426 _Ехк(), 399 _getch(), функция, 198 -getcheQ, функция, 198 -Imaginary, 260; 426 -Pragma, 263
а, режим, 219 а, специальная символьная константа, 62 а+, режим, 220 а+b, режим, 220 ab, режим, 220 abort(), 392 abs(), 393 American National Standards Institute, 32 ANSI, 32 API, 27% 576 Application Program Interface, 279 Programming Interface, 576 assert(), 393 assert.h, заголовок, 280 atexit(), 394 atof(), 394 atoi(), 395 atol(), 396 atoll(), 396 ATOM, 585 в b, специальная символьная константа, 62 BCPL, 32 Binary tree, 484 Bitmap, 575 BKACK_BRUSH, 585 bool, 26% 580 break, оператор, 102 Brian Kemighan, 32 bsearchO, 397 btowc(), 424 Bubble sort, 440 BYTE, 580 c Calling sequence, 607 calloc(), 386 Comparand, 448 complex, 260 complex.h, заголовок, 267; 280 const, ключевое слово, 262 continue, оператор, 104 CreateWindow(), 585 ctype.h, заголовок, 280 CW-USEDEFAULT, 586 defined, оператор, 249 Definition files, 589 Dennis Ritchie, 32 Dependent files, 597 DispatchMessage(), 588 div(), 398 DKGRAY.BRUSH, 585 DLL, 278 double_Complex, 260 double_Imaginary, 260 do-while, цикл, 100 DWORD, 580 Dynamic-Link Library, 278 E EOF, макрос, 219 errno. h, заголовок, 280 exit(), функция, 103; 398 EXIT_FAILURE, 398 EXIT_SUCCESS, 398 Expression parsing, 510 F f, специальная символьная константа, 62 false, 434 fclose(), функция, 218; 22% 285 fenv.h, заголовок, 267 feof() , функция, 223; 218; 286 ferror(), функция, 218; 226; 287 ffiushO, функция, 218; 287 fgetc(), функция, 218; 221; 288 fgetpos, функция, 289 fgets(), функция, 199; 218; 224; 289 fgetws(), 420 float.h, заголовок, 280 float-Complex, 260 float-imaginary, 260 fopen(), функция, 21% 219", 290 FOPEN_MAX, макрос, 219 for, оператор, 92 for, цикл, 97 fprintf(), функция, 218; 235; 291 fputc(), функция, 218; 221; 292 fputs(), функция, 218; 224; 293 fputwc(), 420 fputws(), 420 fread(), функция, 22# 293 free(), функция, 140; 387 682 Предметный указатель
freopenO, функция, 238, 294 fscanf(), функция, 218, 235; 295 fseek(), функция, 218, 234; 296 fsetpos, функция, 297 ftell(), функция, 218, 298 fwide(), 421 fwprintfO, 420 fwrite(), функция, 228, 298 fwscanf(), 420 G GDI, 576 General problem solver, 530 getc(), функция, 218 22Г, 299 getchar(), функция, 197, 300 getenv(), 399 gets(), функция, 199', 300 GetStockObject(), 585 getwc(), 420 getwchar(), 420 goto, оператор, 101 GPS, 530 Graphics Device Interface, 576 H HANDLE, 580 Hashing, 503 hh, модификатор, 207; 267 Hoare Charles Antony Richard, 448 HOLLOW_BRUSH, 585 hPrevInst, 583 hThisInst, 583 HUGEVAL, 346; 408 HUGE_VALL, 411 HWND, 580 I IDC_ARROW, 585 IDC_CROSS, 585 IDC_HAND, 585 IDCJBEAM, 585 IDC_WAIT, 585 IDE, 601 IDI__APPLICATION, 584 IDI_ERROR, 584 IDIJNFORMATION, 584 IDI_QUESTION, 584 IDL.WARNING, 584 IDI WINLOGO, 584 if, оператор, 82 if-else-if, 85 imaginary, 260 inline, ключевое слово, 168; 609 int__fast32__t, расширенный тип, 270 int_least!6_t, расширенный тип, 270 int 16—t, расширенный тип, 270 Integrated development environment, 601 International Standards Organization, 32 intmaX-t, расширенный тип, 270 isalnum, функция, 320 isalpha, функция, 321 isblank, функция, 322 iscntrl, функция, 322 isdigit, функция, 323 isgraph, функция, 324 islower, функция, 324 ISO, 32 iso646.h, заголовок, 268 isprint, функция, 325 ispunct, функция, 325 isspace, функция, 326 isupper, функция, 327 isxdigit, функция, 327 к Ken Thompson, 32 L L, модификатор, 207 labs(), 400 ldiv(), 401 limits.h, заголовок, 280 Little C, 625 11, модификатор, 207 267 llabs(), 400 lldiv(), 401 LLONG_MAX, 411 LLONG.MIN, 411 LoadCursor(), Loadlcon(), 584 Loadlmage(), 584 locate.h, заголовок, 280 LONG, 580 long double__Complex, 260 long double__Imaginary, 260 long long int, 260 long, спецификатор типа, 45 LONG_MAX, 410 LONG-MIN, 410 Предметный указатель 683
longjmpO, 402 LPCSTR, 580 LPSTR, 580 IpszArgs, 583 LPVOID, 586 LRESULT, 579 LTGRAY.BRUSH, 585 M MAKE, 597 MAKEFILE, 600 таке-файл, 597 malloc, функция, 140; 387 Martin Richards, 32 math.h, заголовок, 280 MB_CUR_MAX, 415 mblen(), функция, 403 mbrlenO, функция, 424 mbrtowcO, функция, 424 mbsinit(), функция, 424 mbsrtowcs(), функция, 424 mbstowcsO, функция, 403 mbtowc(),функция, 404 memchr, функция, 328 тетстр, функция, 329 тетсру, функция, 330 memmove, функция, 331 memset, функция 332 Microsoft C/C++,’ 597 MSG, 580 N n, специальная символьная константа, 62 NMAKE, 597 Node, 484 NULL, макрос, 219; 392 nWinMode, 583 P Pascal-соглашение о вызовах, 579 реггог, функция, 301 printf, функция, 302 putc(), функция, 218] 221 305 putchar(), функция, 7Р7; 305 puts, функция, 306 putwc(), 420 putwcharQ, 420 Q qsortO, 404 quicksort, 404 R г, режим, 219 г, специальная символьная константа, 62 r+, режим, 220 r+b, режим, 220 raise(), 405 rand(), 406 RAND_MAX, 406 rb, режим, 219 realloc(), 388 remove(), функция, 218] 227] 307 rename, функция, 307 restrict, ключевое слово, 262 return, оператор, 101 Returning sequence, 607 rewind(), функция, 27<£ 225 308 Root, 484 s scanf, функция, 208] 308 SEEK_CUR, макрос, 234 SEEK_END, макрос, 234 SEEK_SET, макрос, 234 setbuf, функция, 312 setjmpO, функция, 406 setjmp.h, заголовок, 280 setvbuf, функция, 312 Shaker sort, 443 Shell Donald Lewis , 446 short, спецификатор типа, 45 ShowWindow(), 586 SIG_ERR, 407 signal(), 407 signal.h, заголовок, 280 signed, спецификатор типа, 45 sizeof, 261 snprintf, функция, 313 Sparse array, 494 sprintf, функция, 313 srand(), 407 sscanf, функция, 314 stdarg.h, заголовок, 280 stdbool.h, заголовок, 268 STDC CX-LIMITED-RANGE, прагма, 264 684 Предметный указатель
STDC FENV_ACCESS, прагма, 264 STDC FP.CONTRACT, прагма, 263 stddef.h, заголовок, 280 stderr, поток, 237 stdin, поток, 237 stdint.h, заголовок, 268 stdio.h, заголовок, 280 stdlib.h, заголовок, 280 stdout, поток, 237 strcat, функция, 1П 332 strchr, функция, 777; 333 strcmp, функция, 777; 333 strcoll, функция, 334 strcpy, функция, 777; 335 strcspn, функция, 335 strerror, функция, 336 string.h, заголовок, 280 strlen, функция, 777; 336 strncat, функция, 336 stmcmp, функция, 337 stmcpy, функция, 338 strpbrk, функция, 339 strrchr, функция, 339 strspn, функция, 340 strstr, функция, 777; 340 strtod, функция, 408 strtof, функция, 409 strtok, функция, 341 strtol, функция, 410 strtold, функция, 411 strtoll, функция, 411 strtoul, функция, 411 strtoull, функция, 412 struct, ключевое слово, 170 strxfrm, функция, 342 Subtree, 484 SW_HIDE, 587 SW_MAXIMIZE, 587 SW_MINIMIZE, 587 SW_RESTORE, 587 switch, оператор, 89 swprintf, 420 swscanf, 420 system, 413 tmpfile, функция, 315 tmpnam, функция, 315 Token, 512 tolower, функция, 342 toupper, функция, 343 TranslateMessage(), 588 Tree traversal, 485 true, 434 typedef, ключевое слово, 170 и UINT, 580 uintmax_t, расширенный тип, 270 ULLONG_MAX, 412 ULONG_MAX, 412 ungetc, функция, 316 ungetwc(), 420 union, ключевое слово, 184 unsigned long long int, 260 unsigned, спецификатор типа, 45 V v, специальная символьная константа, 62 va__arg(), 413 va_copy(), 413 va__end(), 413 va__start(), 413 Variable-length array, 261 vfprintf, функция vfscanf, функция, 318 vfwprintf(), 421 vfwscanf(), 421 Visual C++, 601 VLA, 261 volatile, ключевое слово, 262 vprintf, функция, 317 vscanf, функция, 318 vsnprintf, функция, 317 vsprintf, функция, 317 vsscanf, функция, 318 vswprintf(), функция, 421 vswscanf(), функция, 421 T vwprintf(), функция, 421 vwscanf(), функция, 421 t, специальная символьная константа, 62 Target files, 597 Terminal node, 484 tgmath.h, заголовок, 268 time.h, заголовок, 280 w w, режим, 219 w+, режим, 220 w+b, режим, 220 Предметный указатель 685
wb, режим, 219 wcrtombO, 424 wcscat(), 422 wcschr(), 422 wcscmp(), 422 wcscollO, 422 wcscpy(), 422 wcscspn(), 422 wcsftime(), 423 wcslen(), 422 wcsncat(), 422 wcsncmp(), 422 wcsncpy(), 422 wcspbrk(), 422 wcsrchr(), 422 wcsrtombs(), 424 wcsspn(), 422 wcsstr(), 422 wcstod(), 423 wcstof(), 423 wcstok(), 422 wcstol(), 423 wcstold(), 423 wcstoll(), 423 wcstombs(), 415 wcstoul(), 423 wcstoull(), 423 wcsxfrm(), 422 wctob(), 424 wctomb(), 415 wctype.h, заголовок, 268 WEOF, 418 while, цикл, 98 WHITE_BRUSH, 585 Winl6, 576 Win32, 577 WinAPI-соглашение о вызовах, 579 Window class, 579 Window function, 579 Window procedure, 579 Windows 95, 577 Windows 98, 577 wmemchrO, 423 wmemcmpO, 423 wmemcpyO, 423 wmemmoveO, 423 wmemset(), 423 WNDCLASSEX, 58a, 583 WORD, 580 wprintf(), 421 WS_HSCROLL, 586 WS.MAXIMIZEBOX, 586 WS_MINIM1ZEBOX, 586 WS_OVERLAPPED, 586 WS_OVERLAPPEDWINDOW, 586 WS_SYSMENU, 586 WS-VSCROLL, 586 wscanf(), 421 A Абсолютный код, 277 Агрегатный тип данных, 170 Администратор оверлейной загрузки, 278 Адресная арифметика, 129 Алгоритм быстрой сортировки quicksort, 404 наискорейшего подъема, 549 пузырьковой сортировки, количество обменов, 442 Аргумент командной строки, 154 Аргументы функции main, 154 Арифметические операции %, 66 *, 66 /, 66 +, 66 ++, 66 вычитание, 66 декремент, 66 деление, 66 инкремент, 66 остаток от деления, 66 сложение, 66 увеличение, 66 уменьшение, 66 умножение, 66 унарный минус, 66 Ассемблерный код, 607 Б Бесконечный цикл, 96 Библиотека, 41 279 поддержки среды вычислений с плавающей точкой, 430 Библиотечные файлы, 279 Битмапка i, 575 Битовое поле, 170 Блок операторов, 105 управления файлом, 218 Брайан Керниган, 32 686 Предметный указатель
в Ввод адреса, 211 одиночных символов, 210 строк, 210 целых значений без знака, 210 чисел, 209 Вертикальная вертикальная табуляция, специальная символьная константа, 62 двутавровая балка, 585 полоса прокрутки, 586 Вершина (графа), 531 Вершина дерева, 484 Вложенные директивы #include, 245 операторы switch, 92 условные операторы, 84 Внешняя ссылка, 276 Возврат каретки, специальная символьная константа, 62 Возвращающая последовательность, 607 Восклицательный знак, 584 Восходящий метод, 592 Восьмеричная константа, специальная символьная константа, 62 Восьмеричные константы, 61 Время выполнения, 41 компиляции, 41 Встроенные прагмы, 263 Выбор структуры данных, 593 Вывод адреса, 204 символов, 202 чисел, 202 Вызывающая последовательность, 607 Высота, 484 г Глобальные переменные, 51 Г оризонтальная полоса прокрутки, 586 табуляция, специальная символьная константа, 62 Граничное значение для количества аргументов при вызове функции, 269 значащих символов во внешнем идентификаторе, 269 значащих символов во внутреннем идентификаторе, 268 уровней вложенности блоков, 268 уровней вложенности условных включений, 268 членов структуры или объединения, 269 Графический интерфейс устройств, 576 д Двоичное дерево, 484 Двоичный поиск, 458 поток, 217 Двойная кавычка, специальная символьная константа, 62 Двусвязный список, 476 Двухмерный массив, 112 Деннис Ритчи, 32 Дерево вырожденное, 489 сбалансированное, 489 Дескриптор, 580 кисти, 585 стандартных объектов отображения, 585 Диалоговое окно, 576 Диапазон значений типов float и double, 44 получаемых сообщений, 588 Динамически подсоединяемые библиотеки, 278 Динамическое распределение, 140 связывание, 278 Директива FENV.ACCESS, 430 Директива препроцессора, 242 #defme, 242 #elif, 246 #else, 246 #endif, 246 #еггог, 244 #if, 245 #ifdef, 247 #ifndef, 248 #include, 245 #line, 249 Предметный указатель 687
#pragma, 250 tfundef, 248 Директивы условной компиляции, 245 Дозапись, 218 потока, 228 Доступ к членам структуры, 172 3 Зависимые файлы, 597 Заголовок, 166; 279 assert.h, 280; 393 complex.h, 267; 280; 426; 433 ctype.h, 280 ermo.h, 280 fenv.h, 267; 280; 430 float.h, 280 inttypes.h, 280; 432 iso646.h, 268', 280 limits.h, 280 locate.h, 280 math.h, 280; 426, 433 setjmp.h, 280; 402, 406 signal.h, 280; 405; 407 stdarg.h, 280; 413 stdbool.h, 268; 280; 434 stddef.h, 280; 431; 432 stdint.h, 268; 280 stdio.h, 280 stdlib.h, 280 string.h, 280 tgmath.h, 268; 280; 433 time.h, 280 wchar.h, 280; 418; 421; 422, 423; 424 wctype.h, 268; 280; 418 Заголовок окна, 586 Заголовочный файл WINDOWS.Н, 583 Закрытие файла, 220 Запись символа, 221 Зарезервированные слова, 632 Знак вопроса, 584 специальная символьная константа, 62 Знаки пунктуации, 632 и Идентификатор ______FILE____, 249 __func________, 268 ___LINE____, 249 Идентификаторы, 632 Инициализация безразмерных массивов, 120 массива, 118 переменных, 60 указателя, 135 Инициализация Инкрементное тестирование, 619 Интегрированная среда разработки, 601 Visual C++, 601 Интерпретатор включение дополнительных базовых типов, 680 вложенные циклы, 677 встроенный редактор, 680 вывод значений переменных при выполнении программы, 680 вывод сообщения об ошибке, 636 вызов функций, определенных пользователем, 665 выполнение оператора if, 669 рекурсивных функций, 677 глобальная целочисленная переменная ret_value, 668 демонстрация, 675 рекурсивных функций, 677 добавление новых средств, 680 структур и объединений, 680 дополнительные средства программирования, 680 зарезервированные слова, 632 знаки пунктуации, 632 значение_переменной, 630 идентификаторы, 632 именующее_выражение, 630 компиляция и компоновка, 675 локальные переменные, 627 массив func_table, 650 global_vars, 650 массивы, 680 нисходящий синтаксический анализатор, 636 обработка локальных переменных, 664 цикла do-while, 671 цикла while, 670 объявление локальной переменной, 665 ограничения языка Little С, 626 688 Предметный указатель
оператор-Сравнения, 630 операторы цикла, 678 определение языка Little С, 625 опция /F, 675 отличие цикла do-while от цикла while, 671 память для стека, 675 переменная целого типа gvar_index, 650 повышение эффективности, 679 поддержка комментариев вида //, 680 новых типов данных, 680 поиск всех функций программы, 648 переменных и функций, 679 практическое значение, 624 предварительный проход, 648 приемы использования аргументов функций, 678 пример с вложенными циклами, 677 присваивание значений переменным, 668 как операция, 677 прототипы библиотечных функций, 673 функций, 627 размещение глобальных переменных, 648 расширение Little С, 680 реализация указателей, 680 синтаксический разбор исходного текста программы, 631 средства трассировки, 680 тексты процедур библиотеки функций Little С, 673 указатель p_buf, 635 prog, 635 усовершенствование, 679 факториал числа, 677 флажок block, 651 функция assign_var(), 668 atom (), 646 call(), 665 decl_£1оЬа1(), 649 decl_local(), 665 exec_do(), 671 exec_while(), 670 factr(), 677 find_eob(), 679 get_token(), 635 getche(), 672 getnum(), 673 interp_block(), 651 prescan(), 649 print(), 673 sntx_err(), 635 цикл for, 671 языка small BASIC, 628 Интерфейс прикладного программирования, 576 Инфиксная запись, 468 Исполняемый файл, 276 Исходный текст, 41 Исчерпывающий поиск, 534 к Квалификатор типа const, 53 volatile, 54 Квалификатор типа restrict, 257 Кен Томпсон, 32 Класс окна, 579 Ключ, 438 Ключевое слово const, 262 inline, 168 258 restrict, 262 static, 261 struct, 170 typedef, 170 union, 184 volatile, 262 Кнопка развертывания, 586 свертывания, 586 Комбинаторный взрыв, 534 Комментарии, 257; 262 Компаранд, 448 выбор, 450 Компилятор, 36 Компоновщик, 41 Компоновщик оверлеев, 277 Корень, 484 Круглые скобки, 79 Куча, 140 Предметный указатель 689
л Лексема, 512; 631 Лист, 484; 531 Логические значения, 82 Логическое значение False, 82 True, 82 ИСТИНА, 82 ЛОЖЬ, 82 Логотип Windows, 584 Локальные переменные, 47 м Макрос bool_true_false аге defined, 434 ___DATE___, 251 ___STDC___, 251 ___STDC_HOSTED____, 251; 264 ___STDCJEC.559____, 264 __STDC_IEC_559_COMPLEX , 264 ___STDC_ISO_10646 , 264 ___STDCVERSION____, 251; 264 ___TIME___, 251 _Complex_I, 426 _Imaginary_I, 426 assert(), 393 bool, 434 complex, 426 EDOM, 346 EILSEQ, 424 EOF, 219 ERANGE, 346 EXIT_FAILURE, 392 EXIT_SUCCESS, 392 false, 434 FE_ALL_EXCEPT, 430 FE_DFL_ENV, 430 FE_DIVBYZERO, 430 FEDOWNWARD, 430 FEJNEXACT, 430 FEJNVALID, 430 FE_OVERFLOW, 430 FE_TONEAREST, 430 FE_TOWARDZERO, 430 FE_UNDERFLOW, 430 FE_UPWARD, 430 FOPEN_MAX, 219 fpclassify, 346 HUGE_VAL, 346 HUGE_VALF, 346 HUGE_VALL, 346 I, 426 imaginary, 426 INFINITY, 346 int_fast32_t, 431 int!6_t, 431 isfinite, 346 isgreater, 346 isgreaterequal, 346 isinf, 346 isless, 346 islessequal, 346 islessgreater, 346 isnan, 346 isnormal, 346 isunordered, 346 LCALL, 381 LC_COLLATE, 381 LCCTYPE, 381 LC_MONETARY, 381 LC_NUMERIC, 381 LCTIME, 381 MATH_ERREXCEPT, 346 math_errhandling, 346 MATH_ERRNO, 346 MB_CUR_MAX, 392 NAN, 346 NULL, 219; 392; 418 RAND_MAX, 392 SEEK_CUR, 234 SEEK_END, 234 SEEK.SET, 234 setjmpO, 406 SIG_DFL, 407 SIGJGN, 407 SIGABRT, 406 SIGFPE, 406 SIGILL, 406 SIGINT, 406 signbit, 346 SIGSEGV, 406 SIGTERM, 406 true, 434 uint32_t, 431 va_arg, 413 va_copy(), 413 va_end, 413 va_start, 413 WCHAR_MAX, 418 WCHAR_MIN, 418 WEOF, 418 WS_OVERLAPPEDWINDOW, 586 690 Предметный указатель
Макросы вида int_fastN_t, 431 вида int_leastN_t, 431 вида intN_t, 431 вида uint_fastN_t, 431 вида uint_leastN_t, 431 вида uintN_t, 431 математические обобщенного типа, 433 Мартин Ричардс, 32 Массив, 108 двухмерный, 112 динамическое выделение памяти, 141 инициализация, 118 многомерный, 116 одномерный, 108 передача одномерного массива в функцию, 110 переменной длины, 121; 261 размер, 108 с переменными границами, 266 строк, 115 указателей, 133 Машинный код, 36 Международная организация по стандартизации, 32 Метод вставки, 439 выбора, 439 обмена, 439 поиска, 457 половинного деления, 458 рекурсивного спуска, 510 сокрытия кода и данных, 595 удаления вершин, 557 удаления путей, 557 Минимально допустимый диапазон значений char, 45 double, 45 float, 45 int, 45 long double, 45 long int, 45 long long int, 45 short int, 45 signed char, 45 signed int, 45 signed long int, 45 signed short int, 45 unsigned char, 45 unsigned int, 45 unsigned long int, 45 unsigned long long int, 45 unsigned short int, 45 Многомерный массив, 116 Многоуровневая адресация, 134 Модель вызова функций CALLBACK, 579 Модификатор #, 208 ♦, 208 hh, 207 267 L, 207 11, 207 267 минимальной ширины поля, 205 точности, 206 Модификаторы формата, 213 н Набор сканируемых символов, 211 Назначенные инициализаторы, 266 Наискорейший подъем, 549 Национальный институт стандартизации США, 32 Небуферизованная система ввода/вывода, 216 Неинициализированный указатель, 144 Неявные объявления функций, 270 Нисходящий метод, 592 синтаксический анализатор, 631 Новая строка, специальная символьная константа, 62 о Области видимости, 52 Область поиска, 531 Обмен, 440 Обратный обход, 486 слэш, специальная символьная константа, 62 Q&LQJt в глубину, 486 в обратном порядке, 486 в прямом порядке, 486 в ширину, 486 сверху, 486 симметричным способом, 486 снизу, 486 Предметный указатель 691
Обход (вершин) дерева, 485 Объединение, 170 Объектный код, 36 41 файл, 279 Объявление переменных, цикл, 97 Оверлей, 277 Оверлейная область памяти, 277 Одинарная кавычка, специальная символьная константа, 62 Одномерный массив, 108 Односвязный список, 471 Однострочные комментарии, 252 Оператор #, 250 250 ., 75 ?, 87 ->, 75 break, 102 continue, 104 defined, 249 for, 92 goto, 101 if, 82 return, 101 157 switch, 89 выбора, 89 доступа к члену структуры, 75; 172 доступа через указатель, 75 конкатенации, 250 перехода, 101 последовательного вычисления, 75 превращения в строку, 250 присваивания, 63 склеивания, 250 стрелка, 75 точка, 75; 172 Операции адресной арифметики, 130 логические, 67 поразрядные, 69 сравнения, 67 Операция &, 73 *, 73 2, 72 sizeof, 74 определения размера, 74 получения адреса, 73 приведения типов, 78 раскрытия ссылки, 73 сравнение указателей, 130 Орграф, 536 Ориентированный граф, 536 Открытие файла, 219 Отладка, 611 Очередь, 460 п Пакет переполнения, 505 Параметр dwStyle, 586 hMenu, 586 hThisInst, 586 hwnd, 588 IpszClassName, 586 nHow, 586 nWinMode, 586 Передача одномерного массива в функцию, ПО Перекрестие, 585 Перекрывающееся окно с обрамлением, 586 Переменная errno, 408 Переменное количество параметров, 166 Переменные списки аргументов, 263 Переместимый код, 277 Перенаправление ввода/вывода, 237 Перенос программ, 610 Переносимость, 33 Перечисление, 170, 189 Песочные часы, 585 Пиктограмма по умолчанию, 584 подача бумаги, специальная символьная константа, 62 Поддерево, 484 Поиск в глубину, 537 в ширину, 546 выбор метода, 556 методом наискорейшего подъема, 548 нескольких решений, 556 с использованием частичного пути минимальной стоимости, 554 Полный перебор, 546 Порождающие правила, 514 Последовательный поиск, 457 Постфиксная запись, 468 Постфиксный калькулятор, 469 Поток, 217 stderr, 237', 393 stdin, 237 692 Предметный указатель
stdout, 237 Правила продвижения целых типов, 271 Правило “неявного int”, 257 Прагма STDC CX_LIMITED_RANGE, 264 STDC FENV_ACCESS, 264 STDC FP_CONTRACT, 263 Предварительный проход, 648 Преобразование типа указателя, 128 типов, 63} 77 Префиксы типов, 590 Присваивание множественное, 64 составное, 65 указателей, 127 Пробелы, 79 Программный блок, 35 Продвижение типов, 77 Продукция, 514 Проектирование сверху вниз, 592 Пропуск лишних разделителей, 212 Просмотр на одну лексему вперед, 647 Прототип функции, 163 Процедура окна, 579 Прямой обход, 486 Псевдокод, 593 Пузырьковая сортировка, 440 р Раздельная компиляция, 276 Размерности массивов, 261 Разреженная матрица, 494 Разреженный массив, 494 Расширенные целые типы, 270 Расширенный тип int_fast32_t, 270 int_leastl6_t, 270 intl6_t, 270 intmax_t, 270 uintmax_t, 270 Редактирование связей, 277 Редактор связей, 41} 276 Режим а, 219 а+, 220 а+Ь, 220 ab, 220 г, 219 г+, 220 г+Ь, 220 rb, 219 w, 219 w+, 220 w+b, 220 wb, 219 Рекурсивное определение, 161 Рекурсивный нисходящий синтаксический анализатор, 510 Рекурсия, 161 Рука, 585 с Связанный список, 471 Сигнал, специальная символьная константа, 62 Символ информации, 584 ошибки, 584 Символьные константы, 62 Симметричный обход, 486 Синтаксический анализ методом рекурсивного спуска, 510 анализатор выражений, 631 управляемый таблицей, 631 Синтаксический разбор выражений, 510 Системная программа, 36 Системное меню, 586 Соединение, 170 Сортировка, 438 вставками, 444 количество сравнений, 445 преимущества, 445 дисковых файлов с произвольной выборкой, 454 критерии оценки алгоритма, 439 массивов, 439 методом пузырька, 440 посредством выбора, 443 пузырьком, 440 строк, 451 в лексикографическом порядке, 452 структур, 453 структур данных, 451 Шелла, 446 Составной оператор, 105 Составные литералы, 265 Специальная символьная константа ?, 62 Предметный указатель 693
\,62 а, 62 Ь, 62 f, 62 п, 62 г, 62 t, 62 v, 62 вертикальная табуляция, 62 возврат каретки, 62 восьмеричная, 62 горизонтальная табуляция, 62 двойная кавычка, 62 знак вопроса, 62 новая строка, 62 обратный слэш, 62 одинарная кавычка, 62 подача бумаги, 62 сигнал, 62 удаление предыдущего символа, 62 шестнадцатеричная, 62 Спецификатор класса памяти extern, 55 register, 59 static, 57 Спецификатор преобразования %%, 202; 209 %[], 209 %а, 202; 209 %с, 202; 209 %d, 202; 209 %ъ9 202; 209 %f, 202; 209 %g, 202; 209 %i, 202; 209 %n, 202; 209 %o, 202; 209 %p, 202; 209 %s, 202; 209 %u, 202; 209 %x, 202; 209 Спецификатор типа long, 45 short, 45 signed, 45 unsigned, 45 Список параметров переменной длины, 166 Сравнение, 440 указателей, 130 Старомодное объявление типа функции, 165 Стирание файлов, 227 Строка, 108 Строковые константы, 61 Структура, 170 imaxdiv_t, 432 вложенная, 184 Структура типа div_t, 398 ldiv_t, 401 lldiv_t, 401 т Текстовый поток, 217 Тип LPVOID, 586 LRESULT, 579 окна, 579 Тип данных, 33 _Воо1, 434 bool, 434 fenv_t, 430 fexcept_t, 430 intmaX-t, 431 intptr_t, 432 jmp-buf, 402 mbstate_t, 418; 424 size_t, 418 uintmaX-t, 431 uintptr_t, 432 va_list, 413 wchar_t, 418 wctrans_t, 418 wctype-t, 418 wint_t, 418 Типы данных Windows, 580 У Удаление вершин, 557 предыдущего символа, специальная символьная константа, 62 путей, 557 Указатель, 126 на массив, 109 на структуру, 181 на указатель, 134 на функцию, 137 операции над указателями, 129 преобразование типа, 128 текущей позиции, 217 файла, 219 694 Предметный указатель
Указатель-стрелка по умолчанию, 585 Универсальный решатель задач, 530 Упорядоченный обход, 486 Условная компиляция, 245 Условные операторы, 82 ф Файл, 217 Файлы описаний, 589 Факториал, 162 532 Формальные параметры, 50 Форматный ввод/вывод, 201 Функции Windows API, 580 ввода-вывода двухбайтовых символов, 420 для преобразования формата целочисленных значений, 432 для работы с массивами двухбайтовых символов, 423 классификации символов широкого формата, 418 обработки двухбайтовых символов, 417 преобразования строк двухбайтовых символов, 422 Функциональный блок, 620 Функция _Exit(), 399 _getch(), 198 _getche(), 198 a*b+c, 357 ab, 428 abort, 392 abs, 393 acos, 347 acosh, 348 asctime, 374 asin, 347, 348 asinh, 349 atan, 347', 349 atan2, 347', 350 atanh, 350 at exit, 394 atexit, 398, 399 atof, 394 atoi, 395 atol, 396 atoll, 396 bsearch, 397 btowc, 424 cabs, 426 cabsf, 426 cabsl, 426 cacos, 427 cacosf, 427 cacosh, 427 cacoshf, 427 cacoshl, 427 cacosl, 427 calloc, 386 carg, 427 cargf, 427 cargl, 427 easin, 427 casinf, 427 casinh, 427 casinhf, 427 casinhl, 427 casinl, 427 catan, 427 catanf, 427 catanh, 427 catanhf, 427 catanhl, 427 catanl, 427 cbrt, 351 ccos, 428 ccosf, 428 ccosh, 428 ccoshf, 428 ccoshl, 428 ccosl, 428 ceil, 347, 351 cexp, 428 cexpf, 428 cexpl, 428 cimag, 428 cimagf, 428 cimagl, 428 clock, 375 clog, 428 clogf, 428 clogl, 428 conj, 428 conjf, 428 conjl, 428 copysign, 352 cos, 347, 352 cosh, 347, 353 epow, 428 epowf, 428 cpowl, 428 cproj, 429 Предметный указатель 695
cprojf, 429 floor, 347; 356 cprojl, 429 fma, 357 creal, 429 fmax, 357 crealf, 429 fmin, 357 creall, 429 fmod, 347; 358 CreateWindow(), 586 fopen, 218, 219; 290 csin, 429 fprintf, 218, 235; 291', 420 csinf, 429 fputc, 218, 221; 292', 420 csinh, 429 fputs, 218, 224; 293; 420 csinhf, 429 fputwc, 420 csinhl, 429 fputws, 420 csinl, 429 fread, 228; 293 csqrt, 429 free, 140; 387 csqrtf, 429 freopen, 238; 294 csqrtl, 429 frexp, 347, 358 ctan, 429 fscanf, 218, 235; 295', 420 ctanf, 429 fseek, 218, 234; 296 ctanh, 429 fsetpos, 297 ctanhf, 429 ftell, 218; 298 ctanhl, 429 fwide, 421 ctanl, 429 ctime, 375 difftime, 376 div(), 398 ea* 428 earM, 355 erf, 353 erfc, 354 exit, 103; 398 exp, 347; 354 fwprintf, 420 fwrite, 228; 298 fwscanf, 420 getc, 218, 221; 299', 420 getchar, 197; 300", 420 getenv, 399 gets, 199; 300 GetStockObject(), 585 getwc, 420 getwchar, 420 exp2, 355 gmtime, 377 expml, 355 hypot, 359 fabs, 347; 355 ilogb, 359 fclose, 218, 220; 285 imaxabs, 432 fdim, 356 imaxdiv, 432 feclearexcept, 430 isalnum, 320; 418 fegetenv, 430 isalpha, 321; 418 fegetexceptflag, 430 isblank, 322; 418 fegetround, 430 iscntri, 418 feholdexcept, 431 iscntrl, 322 feof, 218; 223; 286 isdigit, 323; 418 feraiseexcept, 430 isgraph, 324; 419 ferror, 218, 226; 287 islower, 324; 419 fesetenv, 431 isprint, 325; 419 fesetexceptflag, 430 ispunct, 325; 419 fesetround, 430 isspace, 326; 419 fetestexcept, 430 isupper, 327; 419 feupdateenv, 431 iswalnum, 418 fflush, 218; 287 iswalpha, 418 fgetc, 218, 221; 288, 420 iswblank, 418 fgetpos, 289 iswcntrl, 418 fgets, 199', 218, 224; 289 iswctype, 419 fgetwc, 420 iswdigit, 418 696 Предметный указатель
iswgraph, 419 iswlower, 419 iswprint, 419 iswpunct, 419 iswspace, 419 iswupper, 419 iswxdigit, 419 isxdigit, 327; 419 labs, 400 Idexp, 347; 359 Idiv, 401 Igamma, 360 llabs, 400 lldiv, 401 llrint, 360 llround, 361 localeconv, 377 localtime, 379 log, 347; 361 loglO, 347; 362 loglp, 362 log2, 363 logb, 363 longjmp, 402; 406 Irint, 363 Iround, 364 malloc, 140; 387 mblen, 424 mblen, 403 mbrlen, 424 mbrtowc, 424 mbsinit, 424 mbsrtowcs, 424 mbstowcs, 424 mbstowcs, 403 mbtowc, 424 mbtowc, 404 memchr, 328, 423 memcmp, 329', 423 memcpy, 330; 423 memmove, 331; 423 memset, 332; 423 mktime, 380 modf, 347; 364 nan, 364 nearbyint, 365 nextafter, 365 nexttoward, 365 num ♦ 2^, 360 perror, 301 pow, 347; 366 printf, 201; 302; 421 putc, 218, 221; 305; 420 putchar, 197; 305; 420 puts, 306 putwc, 420 putwchar, 420 qsort, 404 raise, 405 rand, 392; 406 realloc, 388 remainder, 366 remove, 218, 227; 307 remquo, 367 rename, 307 rewind, 218, 225; 308 rint, 367 round, 367 scalbln, 368 scalbn, 368 scanf, 208, 308, 421 setbuf, 312 setjmp, 402 setlocale, 381 setvbuf, 312 signal, 399; 407 sin, 347; 368, 433 sinf, 433 sinh, 347; 369 sinl, 433 snprintf, 313 sprintf, 313; 420 sqrt, 347; 370 stand, 407 sscanf, 314; 420 strcat, 111; 332, 422 strchr, 111; 333; 422 strcmp, 111; 333; 422 strcoll, 334; 422 strcpy, 111; 335; 422 strcspn, 335; 422 strerror, 336 strftime, 382; 423 strftime, 381 strlen, 111; 336; 422 strncat, 336; 422 stmcmp, 337; 422 stmcpy, 338, 422 strpbrk, 339', 422 strrchr, 339; 422 strspn, 340; 422 strstr, 111; 340; 422 strtod, 408; 423 strtof, 409', 423 strtoimax, 433 strtok, 341; 422 Предметный указатель 997
strtol, 410; 423; 433 wcsspn, 422 strtold, 411; 423 wcsstr, 422 strtoll, 411; 423 wcstod, 423 strtoul, 411; 423; 433 wcstof, 423 strtoull, 412; 423 wcstoimax, 433 strtoumax, 433 wcstok, 421; 422 strxfrm, 342; 422 wcstol, 433 swprintf(), 420 wcstol, 423 system(), 413 wcstold, 423 tan, 347; 370 wcstoll, 423 tanh, 347; 371 wcstombs, 424 tgamma, 371 wcstombs, 415 time, 384 wcstoul, 433 tmpfile, 315 wcstoul, 423 tmpnam, 315 wcstoull, 423 tolower, 342; 419 wcstoumax, 433 toupper, 343; 419 wcsxfrm, 422 towctrans(), 419 wctob, 424 towlower, 419 wctomb, 424 towupper, 419 wctomb, 415 trunc, 372 wctrans, 419 ungetc, 316; 420 wctype, 419 ungetwc, 420 wctype, 419 UpdateWindow(), 587 WinMain, 579 val * FLT_RADIXcxp, 368 wmemchr, 423 vfprintf, 317; 421 wmemcmp, 423 vfscanf, 318, 421 wmemcpy, 423 vfwprintf, 421 wmemmove, 423 vfwscanf, 421 wmemset, 423 vprintf, 317; 421 wprintf, 421 vscanf, 318, 421 wscanf, 421 vsnprintf, 317 абсолютная величина, 355; 426 vsprintf, 317; 421 абсолютная величина значение, 432 vsscanf, 318, 421 абсолютное значение vswprintf, 421 целочисленного аргумента, 393 vswscanf, 421 аргумент комплексного числа, 427 vwprintf, 421 аргументы, 149 vwscanf, 421 арккосинус, 348, 427 wcrtomb, 424 арксинус, 349; 427 wcscat, 422 арктангенс, 349; 427 wcschr, 422 отношения, 350 wcscmp, 422 вещественная часть, 429 wcscoll, 422 возврат wcscpy, 422 в вызывающую программу, 157 wcscspn, 422 возврат значений, 158 wcsftime, 423 указателей, 160 wcslen, 422 времени и даты, 374 wcsncat, 422 вызов wcsncmp, 422 по значению, 149 wcsncpy, 422 по ссылке, 150 wcspbrk, 422 с помощью массива, 152 wcsrchr, 422 гамма-функция, 371 wcsrtombs, 424 698 Предметный указатель
гиперболический арккосинус, 348; 427 арксинус, 549; 427 арктангенс, 559; 427 косинус, 555; 428 синус, 369; 429 тангенс, 371; 429 Г-функция, 360 Г-функция Эйлера, 360 двоичный логарифм, 363 десятичный логарифм, 362 динамического выделения памяти, 140 длина гипотенузы, 359 дополнительный интеграл вероятности, 354 дробная часть, 364 интеграл вероятности, 354 Гаусса, 354 ошибок, 354 квадратный корень, 370; 429 из суммы квадратов, 359 комплексное сопряжение, 428 косинус, 352; 428 кубический корень, 557 логарифм по основанию 10, 362 логарифм по основанию 2, 363 максимум, 557 мантисса, 358 минимум, 557 мнимая часть, 428 модуль, 426 натуральный логарифм, 361; 362; 428 натуральный логарифм гамма- функции, 360 нормального распределения, 354 область действия, 148 обратного вызова, 579 общий вид, 148 окна, 579 округление до ближайшего целого, 36&, 361; 363; 364; 365; 367 остаток, 398; 401; 432 остаток от деления, 358; 366; 367 отбрасывание дробной части, 372 ошибок, 354 показатель, 363 порядок, 359 проекция сферу Римана, 429 прототип, 163 рекурсивная, 161 синус, 368; 429 сравнения, 397; 404 старомодное объявление, 165 степень, 366 двойки, 555 тангенс, 579; 429 типа void, 161 целая часть, 364 целый показатель степени числа 2, 358 частное, 367; 398; 401; 432 эйлеров интеграл второго рода, 360 экспонента, 354 X Хоар, 448 Хэширование, 595 ц Целочисленное расширение, 77 Цепочка хэширования, 595 Цикл do-while, 799 for, 97 while, 98 обработки сообщений, 587 объявление переменных, 97 Циклическая очередь, 464 ч Числовые выражения, 579 Чтение символа, 221 ш Шейкер-сортировка, 443 время выполнения, 443 Шелл Дональд Л., 446 шестнадцатеричная константа, специальная символьная константа, 62 Шестнадцатеричные константы, 61 э Эвристика, 557 Элемент управления, 576 Предметный указатель 699
Научно-популярное издание Герберт Шилдт Полный справочник по С, 4-е издание Литературный редактор Верстка Художественный редактор Технический редактор Корректор СТ. Татаренко О. В. Линник В.Г. Павлютин Г.Н. Горобец Л.А. Гордиенко, Т.А. Корзун, О. В. Мишу тин а Издательский дом “Вильямс”. 101509, Москва, ул. Лесная, д. 43, стр. 704. Изд. лиц. ЛР № 090230 от 23.06.99 Госкомитета РФ по печати. Подписано в печать 17.12.2001. Формат 70x100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 56,7. Уч.-изд. л. 33. Тираж 4000 экз. Заказ № 2430. Налоговая льгота - общероссийский классификатор продукции ОК 005-93, том 2: 953000 - книги, брошюры. Отпечатано с диапозитивов в ФГУП “Печатный двор” Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.
Брайан Хетч, Джеймс Ли, Джордж Курц Секреты хакеров Безопасность Linux — готовые решения В продаже Современная вычислительная техника и компьютерные сети подвергаются различным угрозам со стороны нечистоплотных пользователей. Особенно много проблем вызывает механизм защиты операционной системы Linux. Сегодня незащищенные версии Linux представляют собой одну из наиболее уязвимых для атаки целей во всем киберпространстве. Данную книгу можно назвать продолжением всемирно известного бестселлера Секреты хакеров. Безопасность сетей — готовые решения, 2-е издание, в которой все внимание сосредоточено на безопасности при работе в ОС Linux. Ее авторы уже много лет считаются ведущими и признанными специалистами по защите компьютерных систем. Это позволило им рассмотреть проблемы хакинга в Linux на новом, не имеющем аналогов уровне. Книга относится к тому редкому типу книг, которые наглядно объясняют, что именно происходит, когда злоумышленники атакуют системы Linux. Читателям продемонстрировано, чем Linux отличается от других Unix-подобных систем, раскрыты хакерские методы всех типов атак, которые используются для несанкционированного доступа к системам Linux, нарушения работы их служб и взлома компьютерных сетей. Детально изучены средства противодействия атакам хакеров и методы оперативного выявления вторжения. В этой книге нет пустых мест — после описания реальных листингов выполняемых атак предоставляются такие же реальные рецепты отражения каждой конкретной атаки. Материал книги изложен простым и доступным языком, с использованием наглядных примеров, поэтому книга будет полезна самому широкому кругу читателей — начиная от обычных пользователей домашних компьютеров и заканчивая высококвалифицированными системными администраторами крупных компаний.
Защита от хакеров: 20 сценариев взлома Плановая дата выхода 2-й кв. 2002 г. Ч^*о приводит к инциденту? Из-за чего он происходит? Что способствует ему? Как его можно избежать? Каким образом уменьшить ущерб? И, самое главное, как это случилось? Если вас интересуют ответы на такие вопросы, то это книга для вас. Здесь вы найдете истории взломов, основанные на фактах и изложенные ведущими исследователями, консультантами и судебными аналитиками, работающими в современной индустрии компьютерной безопасности. Эта книга не похожа на остальные современные книги, посвященные хакерам. Но в книге не просто пересказываются случаи взлома — здесь предоставлена их подноготная. В ходе изложения каждой истории читатель ознакомится с информацией об инциденте и узнает способы его предотвращения. Книга состоит из двух частей. В первой части приводится описание случая взлома, а также все необходимые сведения (системные журналы и т.д.), необходимые читателю для создания полной картины инцидента. Затем формулируются специфические вопросы, с помощью которых можно более детально проанализировать описанный инцидент. Во второй части каждый случай рассматривается достаточно подробно и даются ответы на поставленные вопросы.
Стьюарт Мак-Клар, Джоел Скембрей, Джордж Курц Секреты хакеров. Безопасность сетей — готовые решения, 2-е издание В продаже Эта книга появилась из-за того, что общедоступная информация вовсе не обязательно автоматически попадает в руки большинства людей. Секреты хакеров — это очень подробная информация о способах обеспечения компьютерной безопасности. В ней приведено полное описание изъянов в системах защиты: что они собой представляют, как их можно использовать и какие контрмеры необходимо предпринимать. После чтения этой книги вы будете знать о своей сети гораздо больше и, что еще важнее, сможете защитить ее гораздо лучше, чем с помощью сведений из любых других аналогичных изданий. Эта книга содержит по-настоящему бесценную информацию. Здесь приведены сведения для тех, кому необходимо знать о том, какие действия предпринимают хакеры, как функционируют используемые при этом средства и какие изъяны скрыты в системе защиты используемых сетей. Первое издание этой книги стало настоящим бестселлером среди книг на компьютерную тематику: свыше 70 тысяч экземпляров продано менее чем за год. Авторам пришлось очень быстро обновить содержимое книги, а это говорит о том, что появилось так много новой информации, что понадобилось второе издание. В книге описаны общие принципы атак хакеров и способы защиты от них. Она может быть полезна администраторам, отвечающим за защиту сетей, программистам, стремящимся обеспечить безопасность своих программ, а также всем, кто интересуется вопросами безопасности сетей. В книге не только подробно описаны возможные бреши в защите сетей, но и даны исчерпывающие рекомендации по их обнаружению и устранению. Особо следует подчеркнуть системный подход к анализу сетевого хакинга, включающий все этапы подготовки и реализации атак, начиная с предварительного сбора данных и заканчивая проникновением в систему и получением ценной информации. Во второе издание книги включено много нового материала (в том числе по Windows 2000), а также описаны новейшие средства и программы взлома, поэтом}' его можно порекомендовать даже тем читателям, которые знакомы с первым изданием. Книга рассчитана на широкий круг читателей — от новичков до профессионалов, работающих с различными операционными системами.
Джоел Скембрей, Стьюарт Мак-Клар Секреты хакеров: безопасность Windows 2000 - готовые решения Плановая дата выхода 2-й кв. 2002 г. Особенностям защиты операционной системы Windows 2000 В известном бестселлере Секреты, хакеров. Безопасность сетей - готовые решения. 2-е издание, была посвящена отдельная глава, однако вполне очевидно, что эта тема заслуживает более глубокого рассмотрения. Данную книгу можно назвать специализированным продолжением упомянутого выше всемирно известного бестселлера, где все внимание сосредоточено на безопасности работы в ОС Windows 2000. Узкая специализация позволила авторам глубже проанализировать механизмы защиты этой операционной системы и предоставить читателям большой объем полезной информации. Даже специалисты корпорации Microsoft найдут в этой книге новые полезные сведения об особенностях защиты Windows-систем. Не вызывает сомнения, что эта книга будет полезнейшим инструментом в арсенале средств каждого системного администратора, ответственного за безопасность вверенной ему информационной инфраструктуры.
Четвертое издание Полный справочник по Содержит описание С99 - нового стандарта языка С, одобренного комитетами ANSI/ISO Содержит описание С99 - нового стандарта языка С, одобренного комитетами ANSI/ISO Наиболее полный ресурс по С - пересмотренное и дополненное издание Герберт Шилдт, ведущий автор книг по программированию мирового уровня, переработал свой не устаревающий справочник-бестселлер по С и включил в него самую свежую информацию по С99 — новому стандарту языка С, принятому комитетами ANSI и ISO. И умудренный опытом профессионал в области языка С, и новичок найдет в нем полные ответы на все свои вопросы, касающиеся языка С. В этом авторитетном руководстве Шилдт подробно описывает все особенности языка С, его библиотеки, приложения, дает профессиональные советы, приводит сотни примеров с объяснениями эксперта. А в заключительной главе автор преподносит особо ценный подарок — он приглашает читателя принять участие в разработке увлекательного проекта — в создании интерпретатора языка С, который читатель затем сможет использовать в готовом виде или дополнить его своими функциями. Все объяснения даются в стиле Шилдта — доходчиво, кратко, точно, — именно в том стиле, за который 1ерберта так любят миллионы читателей! В книге: полное описание языка С, включая'как а го пе~~онэма.“ .ный стандарт С89, так новые егэв^ва, добавленные в С99 подробные яВьяснения всех ключевых Слов, типов данных и операторов* описание указателей, средств • дискового ввода-вывода и динамического распределения памяти на профессиональном уровне подробная информация о всех функциях библиотеки С , знакомство с новыми средствами, гпенными б С99; среди них — применение restrict к указателям, зарезервированное слово inline, лаооивы переменной дг ины и типы данных long long реально используемые алгоритмы, структуры данных и приложения; среди них — стеки, очереди, деревья, разреженные массивы и алгоритмы сортировки; приведены также все необходимые сведения об алгоритмах поиска, использующих методы искусственного интеллекта • советы по эффективному использованию среды программирования на С • советы по переносу программ и их отладке • поЛный исходный код интерпретатора языка С, который можно использовать без каких-либо изменений или допе [нить его функциями, необходимыми для решения конкретных задач Четвертое издание ведущий специалист в области программирования на языке С член комитетов ANS1/ISO, которыми разрабатывался и принимался стандарт я зыка С. Шилдт автор таких книг, как Teach Yourself С, С+ : The Complete Reference, Windows 2000 Programming from the Ground Up, По шин справочник no Java и многих других бестселлеров. Категория: программирование на языке С Уровень: от начинающих до опытных программистов ISBN 5-8459-0226-6 Г. Шилдт дом “Вильямс” http://www.wHllamspubllshlng.com http://www.osbome.com A Division of The McGraw-Hill Companies OSBORNE
Вернуться
Автор: Герберт Шилдт
Дата выхода: 2023
Издательство: Компьютерное издательство «Диалектика»
Количество страниц: 1345
Скачать
Java — один из самых важных и широко используемых языков программирования в мире. На протяжении многих лет ему была присуща эта отличительная особенность. В отличие от ряда других языков программирования, влияние которых с течением времени ослаб евало, влияние Java становилось только сильнее. С момента своего первого выпуска язык Java выдвинулся на передний край программирования для Интернета. Его позиции закреплялись с каждой последующей версией. На сегодняшний день Java по-прежнему является первым и лучшим выбором для разработки веб-приложений, а также мощным языком программирования общего назначения, подходящий для самых разных целей. Проще говоря, большая часть современного кода написана на Java. Язык Java действительно настолько важен.
Ключевая причина успеха языка Java кроется в его гибкости. С момента своего первоначального выпуска 1.0 он постоянно адаптировался к изменениям в среде программирования и к изменениям в способах написания кода программистами. Самое главное то, что язык Java не просто следовал тенденциям — он помогал их создавать. Способность языка Java приспосабливаться к быстрым изменениям в мире программирования является важной частью того, почему он был и остается настолько успешным.
С момента первой публикации этой книги в 1996 году она выдержала множество переизданий, в каждом из которых отражалась непрерывная эволюция Java. Текущее двенадцатое издание книги обновлено с учетом Java SE 17 (JDK 17). В и тоге оно содержит значительный объем нового материала, обновлений и изменений. Особый интерес представляет обсуждение следующих ключевых возможностей, которые были добавлены в язык Java в сравнении с предыдущим изданием:
- усовершенствования оператора switch;
- записи;
- сопоставление с образцом в instanceof;
- запечатанные классы и интерфейсы;
- текстовые блоки.
В совокупности они составляют существенный набор новых функциональных средств, которые значительно расширяют диапазон охвата, область применимости и выразительность языка. Усовершенствования switch добавляют мощи и гибкости этому основополагающему оператору управления. Появившиеся записи предлагают эффективный способ агрегирования данных. Добавление сопоставления с образцом в instanceof обеспечивает более рациональный и устойчивый подход к решению обычной задачи программирования. Запечатанные классы и интерфейсы делают возможным детализированный контроль над наследованием. Текстовые блоки позволяют вводить многострочные строковые литералы, что значительно упрощает процесс вставки таких строк в исходный код. Все вместе новые функциональные средства существенно расширяют возможности разработки и внедрения решений.
Исходный код: Перейти
Если вам понравилась эта книга поделитесь ею с друзьями, тем самым вы помогаете нам |
---|
Герберт Шилдт
Полное руководство С#4.0
Об авторе
Герберт Шилдт (Herbert Schildt) является одним из самых известных специалистов по языкам программирования С#, C++, С и Java. Его книги по программированию изданы миллионными тиражами и переведены с английского на все основные иностранные языки. Его перу принадлежит целый ряд популярных книг, в том числе Полный справочник по Java, Полный справочник по C++, Полный справочник по С (все перечисленные книги вышли в издательстве «Вильямс» в 2007 и 2008 гг.). Несмотря на то что Герберт Шилдт интересуется всеми аспектами вычислительной техники, его основная специализация — языки программирования, в том числе компиляторы, интерпретаторы и языки программирования роботов. Он также проявляет живой интерес к стандартизации языков. Шилдт окончил Иллинойский университет и имеет степени магистра и бакалавра. Связаться с ним можно, посетив его веб-сайт по адресу www.HerbSchildt.com.
О научном редакторе
Майкл Ховард (Michael Howard) работает руководителем проекта программной защиты в группе техники информационной безопасности, входящей в подразделение разработки защищенных информационных систем (TwC) корпорации Microsoft, где он отвечает за внедрение надежных с точки зрения безопасности методов проектирования, программирования и тестирования информационных систем в масштабах всей корпорации. Ховард является автором методики безопасной разработки (Security Development Lifecycle — SDL) — процесса повышения безопасности программного обеспечения, выпускаемого корпорацией Microsoft.
Свою карьеру в корпорации Microsoft Ховард начал в 1992 году, проработав два первых года с ОС Windows и компиляторами в службе поддержки программных продуктов (Product Support Services) новозеландского отделения корпорации, а затем перейдя в консультационную службу (Microsoft Consulting Services), где он занимался клиентской поддержкой инфраструктуры безопасности и помогал в разработке заказных проектных решений и программного обеспечения. В 1997 году Ховард переехал в Соединенные Штаты и поступил на работу в отделение Windows веб-службы Internet Information Services, представлявшей собой веб-сервер следующего поколения в корпорации Microsoft, прежде чем перейти в 2000 году к своим текущим служебным обязанностям.
Ховард является редактором журнала IEEE Security & Privacy, часто выступает на конференциях, посвященных безопасности программных средств, и регулярно пишет статьи по вопросам безопасного программирования и проектирования программного обеспечения. Он является одним из авторов шести книг по безопасности информационных систем.
Благодарности
Особая благодарность выражается Майклу Ховарду за превосходное научное редактирование книги. Его знания, опыт, дельные советы и предложения оказались неоценимыми.
Предисловие
Программисты — люди требовательные, постоянно ищущие пути повышения производительности, эффективности и переносимости разрабатываемых ими программ. Они не менее требовательны к применяемым инструментальным средствам и особенно к языкам программирования. Существует немало языков программирования, но лишь немногие из них действительно хороши. Хороший язык программирования должен быть одновременно эффективным и гибким, а его синтаксис — кратким, но ясным. Он должен облегчать создание правильного кода, не мешая делать это, а также поддерживать самые современные возможности программирования, но не ультрамодные тенденции, заводящие в тупик. И наконец, хороший язык программирования должен обладать еще одним, едва уловимым качеством: вызывать у нас такое ощущение, будто мы находимся в своей стихии, когда пользуемся им. Именно таким языком и является С#.
Язык C# был создан корпорацией Microsoft для поддержки среды .NET Framework и опирается на богатое наследие в области программирования. Его главным разработчиком был Андерс Хейльсберг (Anders Hejlsberg) — известнейший специалист по программированию. C# происходит напрямую от двух самых удачных в области программирования языков: С и C++. От языка С он унаследовал синтаксис, многие ключевые слова и операторы, а от C++ — усовершенствованную объектную модель. Кроме того, C# тесно связан с Java — другим не менее удачным языком.
Имея общее происхождение, но во многом отличаясь, C# и Java похожи друг на друга как близкие, но не кровные родственники. В обоих языках поддерживается распределенное программирование и применяется промежуточный код для обеспечения безопасности и переносимости, но отличия кроются в деталях реализации. Кроме того, в обоих языках предоставляется немало возможностей для проверки ошибок при выполнении, обеспечения безопасности и управляемого исполнения, хотя и в этом случае отличия кроются в деталях реализации. Но в отличие от Java, язык C# предоставляет доступ к указателям — средствам программирования, которые поддерживаются в C++. Следовательно, C# сочетает в себе эффективность, присущую C++, и типовую безопасность, характерную для Java. Более того, компромиссы между эффективностью и безопасностью в этом языке программирования тщательно уравновешены и совершенно прозрачны.
На протяжении всей истории вычислительной техники языки программирования развивались, приспосабливаясь к изменениям в вычислительной среде, новшествам в теории языков программирования и новым тенденциям в осмыслении и подходе к работе программистов. И в этом отношении C# не является исключением. В ходе непрерывного процесса уточнения, адаптации и нововведений C# продемонстрировал способность быстро реагировать на потребности программистов в переменах. Об этом явно свидетельствуют многие новые возможности, введенные в C# с момента выхода исходной версии 1.0 этого языка в 2000 году.
Рассмотрим для примера первое существенное исправление, внесенное в версии C# 2.0, где был введен ряд свойств, упрощавших написание более гибкого, надежного и быстро действующего кода. Без сомнения, самым важным новшеством в версии C# 2.0 явилось внедрение обобщений. Благодаря обобщениям стало возможным создание типизированного, повторно используемого кода на С#. Следовательно, внедрение обобщений позволило основательно расширить возможности и повысить эффективность этого языка.
А теперь рассмотрим второе существенное исправление, внесенное в версии C# 3.0. Не будет преувеличением сказать, что в этой версии введены свойства, переопределившие саму суть C# и поднявшие на новый уровень разработку языков программирования. Среди многих новых свойств особенно выделяются два следующих: LINQ и лябмда-выражения. Сокращение LINQ означает язык интегрированных запросов. Это языковое средство позволяет создавать запросы к базе данных, используя элементы С#. А лябмда-выражения — это синтаксис функционалов с помощью лямбда-оператора =>, причем лябмда-выражения часто применяются в LINQ-выражениях.
И наконец, третье существенное исправление было внесено в версии C# 4.0, описываемой в этой книге. Эта версия опирается на предыдущие и в то же время предоставляет целый ряд новых средств для рационального решения типичных задач программирования. В частности, в ней внедрены именованные и необязательные аргументы, что делает более удобным вызов некоторых видов методов; добавлено ключевое слово dynamic, упрощающее применение C# в тех случаях, когда тип данных создается во время выполнения, например, при сопряжении с моделью компонентных объектов (СОМ) или при использовании рефлексии; а средства ковариантности и контравариантности, уже поддерживавшиеся в С#, были расширены с тем, чтобы использовать параметры типа. Благодаря усовершенствованиям среды .NET Framework, представленной в виде библиотеки С#, в данной версии поддерживается параллельное программирование средствами TPL (Task Parallel Library — Библиотека распараллеливания задач) и PLINQ (Parallel LINQ — Параллельный язык интегрированных запросов). Эти подсистемы упрощают создание кода, который масштабируется автоматически для более эффективного использования компьютеров с многоядерными процессорами. Таким образом, с выпуском версии C# 4.0 появилась возможность воспользоваться преимуществами высокопроизводительных вычислительных платформ.
Благодаря своей способности быстро приспосабливаться к постоянно меняющимся потребностям в области программирования C# по-прежнему остается живым и новаторским языком. А следовательно, он представляет собой один из самых эффективных и богатых своими возможностями языков в современном программировании. Это язык, пренебречь которым не может позволить себе ни один программист. И эта книга призвана помочь вам овладеть им.
Структура книги
В этой книге описывается версия 4.0 языка С#. Она разделена на две части. В части I дается подробное пояснение языка С#, в том числе новых средств, внедренных в версии 4.0. Это самая большая часть книги, в которой описываются ключевые слова, синтаксис и средства данного языка, а также операции ввода-вывода и обработки файлов, рефлексия и препроцессор.
Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.