Язык С
Содержание.
Аннотация
0.1. Введение
1. Учебное введение
1.1. Начинаем
1.2. Переменные и арифметика
1.3. Оператор FOR
1.4. Символические константы
1.5. Набор полезных программ
1.5.1. Ввод и вывод символов
1.5.2. Копирование файла
1.5.3. Подсчет символов
1.5.4. Подсчет строк
1.5.5. Подсчет слов
1.6. Массивы
1.7. Функции
1.8. Аргументы - вызов по значению
1.9. Массивы символов
1.10. Область действия: внешние переменные
1.11. Резюме
2. Типы, операции и выражения
2.1. Имена переменных
2.2. Типы и размеры данных
2.3. Константы
2.3.1. Символьная константа
2.3.2. Константное выражение
2.3.3. Строчная константа
2.4. Описания
2.5. Арифметические операции
2.6. Операции отношения и логические операции
2.7. Преобразование типов
2.8. Операции увеличения и уменьшения
2.9. Побитовые логические операции
2.10. Операции и выражения присваивания
2.11. Условные выражения
2.12. Старшинство и порядок вычисления
3. Поток управления
3.1. Операторы и блоки
3.2. IF - ELSE
3.3. ELSE - IF
3.4. Переключатель
3.5. Циклы - WHILE и FOR
3.6. Цикл DO - WHILE
3.7. Оператор BREAK
3.8. Оператор CONTINUE
3.9. Оператор GOTO и метки
4. Функции и структура программ
4.1. Основные сведения
4.2. Функции, возвращающие нецелые значения
4.3. Еще об аргументах функций
4.4. Внешние переменные
4.5. Правила, определяющие область действия
4.5.1. Область действия
4.6. Статические переменные
4.7. Регистровые переменные
4.8. Блочная структура
4.9. Инициализация
4.10. Рекурсия
4.11. Препроцессор языка “C”
4.11.1. Включение файлов
4.11.2. Mакроподстановка
5. Указатели и массивы
5.1. Указатели и адреса
5.2. Указатели и аргументы функций
5.3. указатели и массивы
5.4. Адресная арифметика
5.5. указатели символов и функции
5.6. Указатели - не целые
5.7. Многомерные массивы
5.8. Массивы указателей; указатели указателей
5.9. Инициализация массивов указателей
5.10. Указатели и многомерные массивы
5.11. Командная строка аргументов
5.12. Указатели на функции
6. Структуры
6.1. Основные сведения
6.2. Структуры и функции
6.3. Массивы структур
6.4. Указатели на структуры
6.5. Структуры, ссылающиеся на себя
6.6. Поиск в таблице
6.7. Поля
6.8. Объединения
6.9. Определение типа
7. Ввод и вывод
7.1. Обращение к стандартной библиотеке
7.2. Стандартный ввод и вывод - функции GETCHAR и PUTCHAR
7.3. Форматный вывод - функция PRINTF
7.4. Форматный ввод - функция SCANF
7.5. Форматное преобразование в памяти
7.6. Доступ к файлам
7.7. Обработка ошибок - STDERR и EXIT
7.8. Ввод и вывод строк
7.9. Несколько разнообразных функций
7.9.1. Проверка вида символов и преобразования
7.9.2. Функция UNGETC
7.9.3. Обращение к системе
7.9.4. Управление памятью
8. Интерфейс системы UNIX
8.1. Дескрипторы файлов
8.2. Низкоуровневый ввод/вывод - операторы READ и WRITE
8.3. Открытие, создание, закрытие и расщепление (UNLINK)
8.4. Произвольный доступ - SEEK и LSEEK
8.5. Пример - реализация функций FOPEN и GETC
8.6. Пример - распечатка справочников
8.7. Пример - распределитель памяти
9. Приложение а: справочное руководство по языку 'C'.
9.1. Введение
10. Лексические соглашения
10.1. Комментарии
10.2. Идентификаторы (имена)
10.3. Ключевые слова
10.4. Константы
10.4.1. Целые константы
10.4.2. Явные длинные константы
10.4.3. Символьные константы
10.4.4. Плавающие константы
10.5. Строки
10.6. Характеристики аппаратных средств
11. Синтаксическая нотация
12. Что в имени тебе моем?
13. Объекты и L-значения
14. Преобразования
14.1. Символы и целые
14.2. Типы FLOAT и DOUBLE
14.3. Плавающие и целочисленные величины
14.4. Указатели и целые
14.5. Целое без знака
14.6. Арифметические преобразования
15. Выражения
15.1. Первичные выражения
15.2. Унарные операции
15.3. Мультипликативные операции
15.4. Аддитивные операции
15.5. Операции сдвига
15.6. Операции отношения
15.7. Операции равенства
15.8. Побитовая операция 'и'
15.9. Побитовая операция исключающего 'или'
15.10. Побитовая операция включающего 'или'
15.11. Логическая операция 'и'
15.12. Операция логического 'или'
15.13. Условная операция
15.14. Операция присваивания
15.15. Операция запятая
16. Описания
16.1. Спецификаторы класса памяти
16.2. Спецификаторы типа
16.3. Описатели
16.4. Смысл описателей
16.5. Описание структур и объединений
16.6. Инициализация
16.7. Имена типов
16.8. TYPEDEF
17. Операторы
17.1. Операторное выражение
17.2. Составной оператор (или блок)
17.3. Условные операторы
17.4. Оператор WHILE
17.5. Оператор DO
17.6. Оператор FOR
17.7. Оператор SWITCH
17.8. Оператор BREAK
17.9. Оператор CONTINUE
17.10. Оператор возврата
17.11. Оператор GOTO
17.12. Помеченный оператор
17.13. Пустой оператор
18. Внешние определения
18.1. Внешнее определение функции
18.2. Внешние определения данных
19. Правила, определяющие область действия
19.1. Лексическая область действия
19.2. Область действия внешних идентификаторов
20. Строки управления компилятором
20.1. Замена лексем
20.2. Включение файлов
20.3. Условная компиляция
21. Неявные описания
22. Снова о типах
22.1. Структуры и объединения
22.2. Функции
22.3. Массивы, указатели и индексация
22.4. Явные преобразования указателей
23. Константные выражения
24. Соображения о переносимости
25. Анахронизмы
26. Сводка синтаксических правил
26.1. Выражения
26.2. Описания
26.3. Операторы
26.4. Внешние определения
26.5. Препроцессор
27. Присваивание структуры
28. Тип перечисления
29. Таблица изображений непечатных символов языка “C”.
Аннотация. Язык “C”(произносится “си”) - это универсальный язык программирования,
для которого характерны экономичность выражения, современный поток управления
и структуры данных, богатый набор операторов. Язык “C” не является ни языком “очень
высокого уровня”, ни “большим” языком, и не предназначается для некоторой
специальной области применения. но отсутствие ограничений и общность языка делают
его более удобным и эффективным для многих задач, чем языки, предположительно
более мощные. Язык “C”, первоначально предназначавшийся для написания операционной
системы “UNIX” на ЭВМ DEC PDP-11, был разработан и реализован на этой системе
Деннисом Ричи. Операционная система, компилятор с языка “C” и по существу
все прикладные программы системы “UNIX” (включая все программное обеспечение, использованное
при подготовке этой книги) написаны на “C”. Коммерческие компиляторы
с языка “C” существуют также на некоторых других 0.1. Введение. Язык “C” является
универсальным языком программирования. Он тесно связан с операционной системой
“UNIX” , так как был развит на этой системе и так как “UNIX” и ее программное
обеспечение написано на “C”. Сам язык , однако, несвязан с какой-либо одной
операционной системой или машиной;и хотя его называют языком системного программирования,
таккак он удобен для написания операционных систем, он с равнымуспехом
использовался при написании больших вычислительныхпрограмм, программ для
обработки текстов и баз данных.Язык “C” - это язык относительно “низкого уровня”.
Втакой характеристике нет ничего оскорбительного; это простоозначает, что “C”
имеет дело с объектами того же вида, что ибольшинство ЭВМ, а именно, с символами,
числами и адресами.Они могут объединяться и пересылаться посредством обычныхарифметических
и логических операций, осуществляемых реаль-ными ЭВМ.В языке “C”
отсутствуют операции, имеющие дело непос-редственно с составными объектами,
такими как строки симво-лов, множества, списки или с массивами, рассматриваемыми
какцелое. Здесь, например, нет никакого аналога операциям PL/1,оперирующим с
целыми массивами и строками. Язык не предос-тавляет никаких других возможностей
распределения памяти,кроме статического определения и механизма стеков, обеспечи-ваемого
локальными переменных функций; здесь нет ни“куч”(HEAP), ни “сборки мусора”,
как это предусматривается вАЛГОЛЕ-68. Наконец, сам по себе “C” не обеспечивает
никакихвозможностей ввода-вывода: здесь нет операторов READ илиWRITE и
никаких встроенных методов доступа к файлам. Все этимеханизмы высокого уровня должны
обеспечиваться явно вызыва-емыми функциями.Аналогично, язык “C” предлагает
только простые, после-довательные конструкции потоков управления: проверки, циклы,группирование
и подпрограммы, но не мультипрограммирование,параллельные операции,
синхронизацию или сопрограммы.Хотя отсутствие некоторых из этих средств
может выгля-деть как удручающая неполноценность (“выходит, что я долженобращаться
к функции, чтобы сравнить две строки символов?!”), но удержание языка в скромных
размерах дает реальныепреимущества. Так как “C” относительно мал, он не требуетмного
места для своего описания и может быть быстро выучен.Компилятор с “C”
может быть простым и компактным. Кроме то-го, компиляторы легко пишутся; при
использовании современнойтехнологии можно ожидать написания компилятора для новой
ЭВМза пару месяцев и при этом окажется, что 80 процентов прог-раммы нового
компилятора будет общей с программой для ужесуществующих компиляторов. Это обеспечивает
высокую степеньмобильности языка. Поскольку типы данных и стуктуры управле-ния,
имеющиеся в “C”, непосредственно поддерживаются боль-шинством существующих
ЭВМ, библиотека, необходимая во времяпрогона изолированных программ, оказывается
очень маленькой.На PDP -11, например, она содержит только программы для32-битового
умножения и деления и для выполнения программввода и вывода последовательностей.
Конечно, каждая реализа-ция обеспечивает исчерпывающую, совместимую
библиотеку функ-ций для выполнения операций ввода-вывода, обработки строк ираспределения
памяти, но так как обращение к ним осуществля-ется только явно, можно
, если необходимо, избежать их вызо-ва; эти функции могут быть компактно написаны
на самом “C”. * 8 - Опять же из-за того , что язык “C” отражает возможностисовременных
компьютеров, программы на “C” оказываются доста-точно эффективными,
так что не возникает побуждения писатьвместо этого программы на языке ассемблера.
Наиболее убеди-тельным примером этого является сама операционная система“UNIX”,
которая почти полностью написана на “C”. Из 13000строк программы системы
только около 800 строк самого низко-го уровня написаны на ассемблере. Кроме того,
по существувсе прикладное программное обеспечение системы “UNIX” напи-сано
на “C”; подавляющее большинство пользователей системы“UNIX”(включая одного из
авторов этой книги) даже не знаетязыка ассемблера PDP-11.Хотя “C” соответствует
возможностям многих ЭВМ, он независит от какой-либо конкретной архитектуры машины
и в силуэтого без особых усилий позволяет писать “переносимые” прог-раммы,
т.е. программы, которые можно пропускать без измене-ний на различных аппаратных
средствах. В наших кругах сталуже традицией перенос программного обеспечения,
разработан-ного на системе “UNIX”, на системы ЭВМ: HONEYWELL, IBM иINTERDATA. Фактически
компиляторы с “C” и программное обес-печение во время прогона программ
на этих четырех системах,по-видимому, гораздо более совместимы, чем стандартные
вер-сии фортрана американского национального института стандар-тов (ANSI). Сама
операционная система “UNIX” теперь работаеткак на PDP-11, так и на INTERDATA
8/32. За исключением прог-рамм, которые неизбежно оказываются в некоторой степени
ма-шинно-зависимыми, таких как компилятор, ассемблер и отлад-чик. Написанное
на языке “C” программное обеспечение иден-тично на обеих машинах. Внутри самой
операционной системы7000 строк программы, исключая математическое обеспечениеязыка
ассемблера ЭВМ и управления операциями ввода-вывода,совпадают на 95 процентов.Программистам,
знакомым с другими языками, для сравне-ния и противопоставления
может оказаться полезным упоминаниенескольких исторических, технических и
философских аспектов“C”.Многие из наиболее важных идей “C” происходят от гораз-до
более старого, но все еще вполне жизненного языка BCPL ,разработанного Мартином
Ричардсом. Косвенно язык BCPL оказалвлияние на “C” через язык “B”, написанный
Кеном Томпсоном в1970 году для первой операционной системы “UNIX” на ЭВМPDP-7.Хотя
язык “C” имеет несколько общих с BCPL характерныхособенностей, он никоим
образом не является диалектом пос-леднего. И BCPL и “B” - “безтипные” языки;
единственным ви-дом данных для них являются машинное слово, а доступ к дру-гим
объектам реализуется специальными операторами или обра-щением к функциям. В языке
“C” объектами основных типов дан-ных являются символы, целые числа нескольких
размеров и чис-ла с плавающей точкой. Кроме того, имеется иерархия произ-водных
типов данных, создаваемых указателями, массивами,структурами, объединениями
и функциями. * 9 - Язык “C” включает основные конструкции потока управле-ния, требуемые
для хорошо структуированных программ: группи-рование операторов, принятие
решений (IF), циклы с проверкойзавершения в начале (WHILE, FOR) или в конце
(DO) и выбородного из множества возможных вариантов (SWITCH). (Все этивозможности
обеспечивались и в BCPL, хотя и при несколькоотличном синтаксисе; этот язык
предчувствовал наступившуючерез несколько лет моду на структурное программирование).В
языке “C” имеются указатели и возможность адреснойарифметики. Аргументы
передаются функциям посредством копи-рования значения аргумента , и вызванная
функция не можетизменить фактический аргумент в вызывающей программе. Еслижелательно
добиться “вызова по ссылке”, можно неявно пере-дать указатель, и функция
сможет изменить объект, на которыйэтот указатель указывает. Имена массивов передаются
указани-ем начала массивов, так что аргументы типа массивов эффек-тивно
вызываются по ссылке.К любой функции можно обращаться рекурсивно, и ее ло-кальные
переменные обычно “автоматические”, т.е. Создаютсязаново при каждом обращении.
Описание одной функции не можетсодержаться внутри другой, но переменные могут
описываться всоответствии с обычной блочной структурой. Функции в “C” -программе
могут транслироваться отдельно. переменные по от-ношению к функции могут быть
внутренними, внешними, но из-вестными только в пределах одного исходного файла,
или пол-ностью глобальными. Внутренние переменные могут быть автома-тическими
или статическими. Автоматические переменные длябольшей эффективности можно помещать
в регистры, но объявле-ние регистра является только указанием для компилятора
и ни-как не связано с конкретными машинными регистрами.Язык “C” не является
языком со строгими типами в смыслепаскаля или алгола 68. Он сравнительно снисходителен
к пре-образованию данных, хотя и не будет автоматически преобразо-вывать
типы данных с буйной непринужденностью языка PL/1.Существующие компиляторы
не предусматривают никакой проверкиво время выполнения программы индексов массивов,
типов аргу-ментов и т.д.В тех ситуациях, когда желательна строгая проверка
ти-пов, используется специальная версия компилятора. Эта прог-рамма называется
LINT очевидно потому, она выбирает кусочкипуха из вашей программы. Программа
LINT не генерирует машин-ного кода, а делает очень строгую проверку всех тех сторонпрограммы,
которые можно проконтролировать во время компиля-ции и загрузки.
Она определяет несоответствие типов, несов-местимость аргументов, неиспользованные
или очевидным обра-зом неинициализированные переменные, потенциальные трудностипереносимости
и т.д. Для программ,которые благополучно про-ходят через LINT,
гарантируется отсутствие ошибок типа при-мерно с той же полнотой, как и для
программ, написанных,например, на АЛГОЛЕ-68. Другие возможности программы LINTбудут
отмечены, когда представится соответствующий случай. * 10 - Наконец, язык
“C”, подобно любому другому языку, имеетсвои недостатки. Некоторые операции имеют
неудачное старшин-ство; некоторые разделы синтаксиса могли бы быть лучше; су-шествует
несколько версий языка, отличающихся небольшими де-талями. Тем не менее
язык “C” зарекомендовал себя как исклю-чительно эффективный и выразительный
язык для широкого раз-нообразия применений программирования.Содержание книги организовано
следующим образом. Глава1 является учебным введением в центральную
часть языка “C”.Цель - позволить читателю стартовать так быстро,как тольковозможно,
так как мы твердо убеждены, что единственный спо-соб изучить новый язык -
писать на нем программы. При этом ,однако, предполагается рабочее владение основными
элементамипрограммирования; здесь не объясняется, что такое ЭВМ иликомпилятор,
не поясняется смысл выражений типа N=N+1. Хотямы и пытались, где это возможно,
продемонстрировать полезнуютехнику программирования. Эта книга не предназначается
бытьсправочным руководством по структурам данных и алгоритмам;там, где
мы вынуждены были сделать выбор, мы концентрирова-лись на языке.В главах со 2-й
по 6-ю различные аспекты “C” излагаютсяболее детально и несколько более формально,
чем в главе 1,хотя ударение по-прежнему делается на разборе примеров за-конченных,
полезных программ, а не на отдельных фрагментах.В главе 2 обсуждаются
основные типы данных, операторы ивыражения. В главе 3 рассматриваются управляющие
операторы:IF-ELSE ,WHILE ,FOR и т.д. Глава 4 охватывает функции иструктуру
программы - внешние переменные, правила определен-ных областей действия описания
и т.д. В главе 5 обсуждаютсяуказатели и адресная арифметика. Глава 6 содержит
подробноеописание структур и объединений.В главе 7 описывается стандартная библиотека
ввода-вы-вода языка “C”, которая обеспечивает стандартный интерфейс соперационной
системой. Эта библиотека ввода-вывода поддержи-вается на всех машинах,
на которых реализован “C”, так чтопрограммы, использующие ее для ввода, вывода
и других сис-темных функций, могут переноситься с одной системы на другуюпо
существу без изменений.В главе 8 описывается интерфейс между “C” - программамии
операционной системой “UNIX”. Упор делается на ввод-вывод,систему файлов и переносимость.
Хотя некоторые части этойглавы специфичны для операционной системы
“UNIX”, програм-мисты, не использующие “UNIX”, все же должны найти здесь по-лезный
материал, в том числе некоторое представление о том,как реализована одна версия
стандартной библиотеки и предло-жения для достижения переносимости программы.Приложение
A содержит справочное руководство по языку“C”. Оно является “официальным”
изложением синтаксиса и се-мантики “C” и (исключая чей-либо собственный
компилятор)окончательным арбитром для всех двусмысленностей и упущенийв предыдущих
главах. * 11 - Так как “C” является развивающимся языком, реализован-ным
на множестве систем, часть материла настоящей книги мо-жет не соответствовать текущему
состоянию разработки на ка-кой-то конкретной системе. Мы старались избегать
таких проб-лем и предостерегать о возможных трудностях. В сомнительныхслучаях,
однако, мы обычно предпочитали описывать ситуациюдля системы “UNIX” PDP-11
, так как она является средой длябольшинства программирующих на языке “C”. В приложении
атакже описаны расхождения в реализациях языка “C” на основ-ных системах.1. Учебное
введение.Давайте начнем с быстрого введения в язык “C”. Нашацель
- продемонстрировать существенные элементы языка на ре-альных программах, не увязая
при этом в деталях, формальныхправилах и исключениях. В этой главе мы не
пытаемся изложитьязык полностью или хотя бы строго (разумеется, приводимыепримеры
будут корректными). Мы хотим как можно скорее довес-ти вас до такого уровня,
на котором вы были бы в состоянииписать полезные программы, и чтобы добиться этого,
мы сосре-дотачиваемся на основном: переменных и константах, арифмети-ке,
операторах передачи управления, функциях и элементарныхсведениях о вводе и выводе.
Мы совершенно намеренно оставля-ем за пределами этой главы многие элементы
языка “C”, кото-рые имеют первостепенное значение при написании большихпрограмм,
в том числе указатели, сртуктуры, большую часть избогатого набора операторов
языка “C”, несколько операторовпередачи управления и несметное количество деталей.Такой
подход имеет, конечно, свои недостатки. Самым су-щественным является то,
что полное описание любого конкрет-ного элемента языка не излагается в одном
месте, а поясне-ния, в силу краткости, могут привести к неправильному истол-кованию.
Кроме того, из-за невозможности использовать всюмощь языка, примеры оказываются
не столь краткими и элегант-ными, как они могли бы быть. И хотя мы старались
свести этинедостатки к минимуму, все же имейте их ввиду.Другой недостаток
состоит в том, что последующие главыбудут неизбежно повторять некоторые части
этой главы. Мы на-деемся, что такое повторение будет скорее помогать, чем раз-дражать.Во
всяком случае, опытные программисты должны оказатьсяв состоянии проэкстраполировать
материал данной главы насвои собственные программистские нужды.
Начинающие же должныв дополнение писать аналогичные маленькие самостоятельныепрограммы.
И те, и другие могут использовать эту главу каккаркас, на который будут
навешиваться более подробные описа-ния, начинающиеся с главы 2.1.1. Hачинаем.
Единственный способ освоить новый языкпрограммирования - писать на нем программы.
Первая програм-ма, которая должна быть написана, - одна для всех языков:напечатать
слова : HELLO, WORLD.Это - самый существенный барьер; чтобы преодолеть
его,вы должны суметь завести где-то текст программы, успешно егоскомпилировать,
загрузить, прогнать и найти, где оказаласьваша выдача. Если вы научились справляться
с этими техничес-кими деталями, все остальное сравнительно просто. * 12
- Программа печати “HELLO, WORLD” на языке “C” имеет вид:MAIN () {PRINTF(“HELLO,
WORLD\N”); }Как пропустить эту программу - зависит от используемойвами системы.
В частности, на операционной системе “UNIX” выдолжны завести исходную программу
в файле, имя которогооканчивается на “.C” , например, HELLO.C , и затем скомпили-ровать
ее по команде CC HELLO.CЕсли вы не допустили какой-либо небрежности
, такой какпропуск символа или неправильное написание, компиляция прой-дет без
сообщений и будет создан исполняемый файл с именема.OUT . Прогон его по команде
A.OUTприведет к выводуHELLO, WORLDНа других системах эти правила будут иными;
проконсуль-тируйтесь с местным авторитетом.Упражнение 1-1.Пропустите эту программу
на вашей системе. Попробуйтене включать различные части программы и посмотрите
какие со-общения об ошибках вы при этом получите.Теперь некоторые пояснения
к самой программе. Любая“C”-программа, каков бы ни был ее размер, состоит из
однойили более “функций”, указывающих фактические операциикомпьютера, которые
должны быть выполнены. Функции в языке“C” подобны функциям и подпрограммам фортрана
и процедурамPL/1, паскаля и т.д. В нашем примере такой функцией являетсяMAIN.
Обычно вы можете давать функциям любые имена по вашемуусмотрению, но MAIN -
это особое имя; выполнение вашей прог-раммы начинается сначала с функции MAIN.
Это означает, чтокаждая программа должна в каком-то месте содержать функцию сименем
MAIN. Для выполнения определенных действий функцияMAIN обычно обращается
к другим функциям, часть из которыхнаходится в той же самой программе, а часть
- в библиотеках,содержащих ранее написанные функции. * 13 - Одним способом обмена
данными между функциями являетсяпередача посредством аргументов. Круглые скобки,
следующиеза именем функции, заключают в себе список аргументов; здесьмаIN
- функция без аргументов, что указывается как (). Опе-раторы, составляющие функцию,
заключаются в фигурные скобки{ и }, которые аналогичны DO-END в PL/1 или BEGIN-END
в ал-голе, паскале и т.д. Обращение к функции осуществляется ука-занием
ее имени, за которым следует заключенный в круглыескобки список аргументов. здесь
нет никаких операторов CALL,как в фортране или PL/1. Круглые скобки должны
присутство-вать и в том случае, когда функция не имеет аргументов.Строка PRINTF(“HELLO,
WORLD\N”);является обращением к функции, которое вызывает функциюс именем
PRINTF и аргуметом “HELLO, WORLD\N”. Функция PRINTFявляется библиотечной функцией,
которая выдает выходные дан-ные на терминал (если только не указано какое-то
другое мес-то назначения). В данном случае печатается строка символов,являющаяся
аргументом функции.Последовательность из любого количества символов, зак-люченных
в удвоенные кавычки “...”, называется 'символьнойстрокой' или 'строчной
константой'. Пока мы будем использо-вать символьные строки только в качестве
аргументов дляPRINTF и других функций.Последовательность \N в приведенной строке
являетсяобозначением на языке “C” для 'символа новой строки', кото-рый служит
указанием для перехода на терминале к левому краюследующей строки. Если вы
не включите \N (полезный экспери-мент), то обнаружите, что ваша выдача не закончится
перехо-дом терминала на новую строку. Использование последователь-ности \N
- единственный способ введения символа новой строкив аргумент функции PRINTF;
если вы попробуете что-нибудьвроде PRINTF(“HELLO, WORLD “); то “C”-компилятор
будет печатать злорадные диагностическиесообщения о недостающих кавычках.Функция
PRINTF не обеспечивает автоматического переходана новую строку, так что многократное
обращение к ней можноиспользовать для поэтапной сборки выходной строки.
Наша пер-вая программа, печатающая идентичную выдачу, с точно такимже успехом
могла бы быть написана в виде MAIN() {PRINTF(“HELLO, “);PRINTF(“WORLD”);PRINTF(“\N”);
} * 14 - Подчеркнем, что \N представляет только один символ. Ус-ловные 'последовательности',
подобные \N , дают общий и до-пускающий расширение механизм
для представления трудных дляпечати или невидимых символов. Среди прочих символов
в языке“C” предусмотрены следующие: \т - для табуляции, \B - длявозврата на
одну позицию, \” - для двойной кавычки и \\ длясамой обратной косой черты.Упражнение
1-2.Проведите эксперименты для того, чтобы узнать что прои-зойдет, если
в строке, являющейся аргументом функции PRINTFбудет содержаться \X, где X - некоторый
символ, не входящийв вышеприведенный список.1.2. Переменные и арифметика.Следующая
программа печатает приведенную ниже таблицутемператур по Фаренгейту
и их эквивалентов по стоградуснойшкале Цельсия, используя для перевода формулу
C = (5/9)*(F-32).0 -17.8 20 -6.7 40 4.4 60 15.6 ... ... 260 126.7 280 137.8 300
140.9 Теперь сама программа:/* PRINT FAHRENHEIT-CELSIUS TABLEFOR F = 0, 20, ...,
300 */MAIN() {INT LOWER, UPPER, STEP;FLOAT FAHR, CELSIUS;LOWER = 0; /* LOWER
LIMIT OF TEMPERATURETABLE */UPPER =300; /* UPPER LIMIT */STEP = 20; /* STEP SIZE
*/FAHR = LOWER;WHILE (FAHR = '0' &C = '0' &C = '0' &C 0 */INT X,N; {INT I,
P;P = 1;FOR (I =1; I 0;VERSION 2 */INT X,N; {INT P;FOR (P = 1; N > 0; --N)P = P
* X;RETURN (P); } Аргумент N используется как временная переменная; из не-го вычитается
единица до тех пор, пока он не станет нулем.Переменная I здесь больше
не нужна. чтобы ни происходило с Nвнутри POWER это никак не влияет на аргумент,
с которым пер-воначально обратились к функции POWER.При необходимости все же
можно добиться, чтобы функцияизменила переменную из вызывающей программы. Эта программадолжна
обеспечить установление адреса переменной /техничес-ки, через указатель
на переменную/, а в вызываемой функциинадо описать соответствующий аргумент
как указатель и ссы-латься к фактической переменной косвенно через него. Мы
рас-смотрим это подробно в главе 5.Когда в качестве аргумента выступает имя массива,
тофактическим значением, передаваемым функции, является адресначала массива.
/Здесь нет никакого копирования элементовмассива/. С помощью индексации и
адреса начала функция можетнайти и изменить любой элемент массива. Это - тема
следующе-го раздела. * 32 - 1.9. Массивы символов. По-видимому самым общим типом
массива в “C” являетсямассив символов. Чтобы проиллюстрировать использование
мас-сивов символов и обрабатывающих их функций, давайте напишемпрограмму, которая
читает набор строк и печатает самую длин-ную из них. Основная схема программы
достаточно проста: WHILE (имеется еще строка)IF (эта строка длиннее самой длинной
изпредыдущих)запомнить эту строку и ее длинунапечатать самую длинную строкуПо
этой схеме ясно, что программа естественным образомраспадается на несколько
частей. Одна часть читает новуюстроку, другая проверяет ее, третья запоминает,
а остальныечасти программы управляют этим процессом.Поскольку все так прекрасно
делится, было бы хорошо инаписать программу соответсвующим образом. Давайте сначаланапишем
отдельную функцию GETLINE, которая будет извлекатьследующую строку
из файла ввода; это - обобщение функцииGETCHAR. мы попытаемся сделать эту функцию
по возможностиболее гибкой, чтобы она была полезной и в других ситуациях.Как
минимум GETLINE должна передавать сигнал о возможном по-явлении конца файла;
более общий полезный вариант мог бы пе-редавать длину строки или нуль, если встретится
конец файла.нуль не может быть длиной строки, так как каждая строка со-держит
по крайней мере один символ; даже строка, содержащаятолько символ новой
строки, имеет длину 1.Когда мы находим строку, которая длиннее самой длиннойиз
предыдущих, то ее надо где-то запомнить. Это наводит намысль о другой функции,
COPY , которая будет копировать но-вую строку в место хранения.Наконец, нам нужна
основная программа для управленияфункциями GETLINE и COPY . Вот результат :#DEFINE
MAXLINE 1000 /* MAXIMUM INPUTLINE SIZE */MAIN() /* FIND LONGEST LINE */
{INT LEN; /* CURRENT LINE LENGTH */INT MAX; /* MAXIMUM LENGTH SEEN SO FAR */CHAR
LINE[MAXLINE]; /* CURRENT INPUT LINE */CHAR SAVE[MAXLINE]; /* LONGEST LINE,
SAVED */ MAX = 0;WHILE ((LEN = GETLINE(LINE, MAXLINE)) > 0)IF (LEN > MAX) {MAX
= LEN;COPY(LINE, SAVE); }IF (MAX > 0) /* THERE WAS A LINE */PRINTF(“%S”, SAVE);
} * 33 - GETLINE(S,LIM) /* GET LINE INTO S,RETURN LENGTH */CHAR S[];INT LIM;
{INT C, I;FOR(I=0;I 0 )IF ( LEN > MAX ) {MAX = LEN;COPY(); }IF ( MAX > 0 ) /* THERE
WAS A LINE */PRINTF( “%S”, SAVE ); } GETLINE() /* SPECIALIZED VERSION */ {INT
C, I;EXTERN CHAR LINE[];FOR (I = 0; I > =='0' &S[I]= 'A' &C J, илогические выражения,
связанные операциями &и \!\!, по оп-ределению имеют значение 1, если
они истинны, и 0, если ониложны. Таким образом, присваивание ISDIGIT = C >= '0'
&C > сдвиг вправо \^ дополнение (унарная операция) “\” иммитирует вертикальную
черту.Побитовая операция AND часто используется для маскированиянекоторого множества
битов; например, оператор C = N 0177 * 52 - передает в 'с' семь младших
битов N , полагая остальные рав-ными нулю. Операция 'э' побитового OR используется
для вклю-чения битов: C = X э MASKустанавливает на единицу те биты в х , которые
равны единицев MASK.Следует быть внимательным и отличать побитовые операциии
'э' от логических связок &и \!\! , Которые подразуме-вают вычисление значения
истинности слева направо. Например,если х=1, а Y=2, то значение хYравно нулю
, в то время какзначение X&Yравно единице./почему?/Операции сдвига > осуществляют
соответственносдвиг влево и вправо своего левого операнда на число битовыхпозиций,
задаваемых правым операндом. Таким образом , х> (P+1-N)) \^(\^0 > (P+1-N)
сдвигает желаемое поле в правый конецслова. Описание аргумента X как UNSIGNED
гарантирует, чтопри сдвиге вправо освобождающиеся биты будут заполняться ну-лями,
а не содержимым знакового бита, независимо от того, накакой машине пропускается
программа. Все биты константноговыражения \^0 равны 1; сдвиг его на N позиций
влево с по-мощью операции \^0> \^ \! Если е1 и е2 - выражения, то * 54 -
е1 оп= е2эквивалентное1 = (е1) оп (е2)за исключением того, что выражение е1 вычисляется
толькоодин раз. Обратите внимание на круглые скобки вокруг е2: X *= Y +
1тоX = X * (Y + 1)неX = X * Y + 1В качестве примера приведем функцию BITCOUNT,
которая подсчитывает число равных 1 битов у целого аргумента. BITCOUNT(N) /* COUNT
1 BITS IN N */UNSIGNED N; (INT B;FOR (B = 0; N != 0; N >>= 1)IF (N 01)B++;RETURN(B);
) Не говоря уже о краткости, такие операторы приваиванияимеют то преимущество,
что они лучше соответствуют образучеловеческого мышления. Мы говорим:
“прибавить 2 к I” или“увеличить I на 2”, но не “взять I, прибавить 2 и поместитьрезультат
опять в I”. Итак, I += 2. Кроме того, в громоздкихвыражениях, подобных
YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2Tакая операция присваивания облегчает
понимание программы,так как читатель не должен скрупулезно проверять, являютсяли
два длинных выражения действительно одинаковыми, или за-думываться, почему они
не совпадают. Такая операция присваи-вания может даже помочь компилятору получить
более эффектив-ную программу.Мы уже использовали тот факт, что операция присваиванияимеет
некоторое значение и может входить в выражения; самыйтипичный
пример * 55 - WHILE ((C = GETCHAR()) != EOF)присваивания, использующие другие операции
присваивания (+=,-= и т.д.) также могут входить в выражения, хотя это случа-ется
реже.Типом выражения присваивания является тип его левогооперанда.Упражнение
2-9.В двоичной системе счисления операция X(X-1)обнуляетсамый правый равный
1 бит переменной X.(почему?) используйтеэто замечание для написания более
быстрой версии функцииBITCOUNT. 2.11. Условные выражения. ОператорыIF (A > B)Z
= A;ELSEZ = B;конечно вычисляют в Z максимум из а и в. Условное выражение,записанное
с помощью тернарной операции “?:”, предоставляетдругую возможность для записи
этой и аналогичных конструк-ций. В выражении е1 ? Е2 : е3сначала вычисляется
выражение е1. Если оно отлично от нуля(истинно), то вычисляется выражение е2,
которое и становитсязначением условного выражения. В противном случае вычисляет-ся
е3, и оно становится значением условного выражения. Каж-дый раз вычисляется
только одно из выражения е2 и е3. Такимобразом, чтобы положить Z равным максимуму
из а и в, можнонаписать Z = (A > B) ? A : B; /* Z = MAX(A,B) */Следует подчеркнуть,
что условное выражение действитель-но является выражением и может использоваться
точно так же,как любое другое выражение. Если е2 и е3 имеют разные
типы,то тип результата определяется по правилам преобразования,рассмотренным ранее
в этой главе. например, если F имеет типFLOAT, а N - тип INT, то выражение
(N > 0) ? F : NИмеет тип DOUBLE независимо от того, положительно ли N илинет.
* 56 - Так как уровень старшинства операции ?: очень низок,прямо над присваиванием,
то первое выражение в условном вы-ражении можно не заключать в круглые скобки.
Однако, мы всеже рекомендуем это делать, так как скобки делают условнуючасть
выражения более заметной.Использование условных выражений часто приводит к корот-ким
программам. Например, следующий ниже оператор цикла пе-чатает N элементов
массива, по 10 в строке, разделяя каждыйстолбец одним пробелом и заканчивая
каждую строку (включаяпоследнюю) одним символом перевода строки. OR (I = 0; I
. LEFT TO RIGHT ! \^ ++ -- - (TYPE) * SIZEOF RIGHT TO LEFT * / % LEFT TO RIGHT
+ - LEFT TO RIGHT > LEFT TO RIGHT >= LEFT TO RIGHT * 57 - == != LEFT TO RIGHT
LEFT TO RIGHT ^ LEFT TO RIGHT \! LEFT TO RIGHT & LEFT TO RIGHT \!\! LEFT TO RIGHT
?: RIGHT TO LEFT = += -= ETC. RIGHT TO LEFT , (CHAPTER 3) LEFT TO RIGHT Операции
-> и . Используются для доступа к элементам струк-тур; они будут описаны в
главе 6 вместе с SIZEOF (размеробъекта). В главе 5 обсуждаются операции * (косвенная
адре-сация) и (адрес).Отметим, что уровень старшинства побитовых логических
опера-ций ,^ и э ниже уровня операций == и !=. Это приводит ктому, что осуществляющие
побитовую проверку выражения, по-добные IF ((X MASK) == 0) ...Для получения
правильных результатов должны заключаться вкруглые скобки.Как уже отмечалось
ранее, выражения, в которые входитодна из ассоциативных и коммутативных операций
(*, +, ,^,э), могут перегруппировываться, даже если они заключены вкруглые
скобки. В большинстве случаев это не приводит к ка-ким бы то ни было расхождениям;
в ситуациях, где такие рас-хождения все же возможны, для обеспечения нужного
порядкавычислений можно использовать явные промежуточные перемен-ные.В языке
“C”, как и в большинстве языков, не фиксируетсяпорядок вычисления операндов
в операторе. Например в опера-торе вида X = F() + G();сначала может быть вычислено
F, а потом G, и наоборот; поэ-тому, если либо F, либо G изменяют внешнюю переменную,
откоторой зависит другой операнд, то значение X может зависетьот порядка
вычислений. Для обеспечения нужной последователь-ности промежуточные результаты
можно опять запоминать вовременных переменных.Подобным же образом не фиксируется
порядок вычисленияаргументов функции, так что операторPRINTF(“%D %D\N”,++N,POWER(2,N));*
58 - может давать (и действительно дает) на разных машинах разныерезультаты
в зависимости от того, увеличивается ли N до илипосле обращения к
функции POWER. Правильным решением, конеч-но, является запись ++N;PRINTF(“%D %D\N”,N,POWER(2,N));Обращения
к функциям, вложенные операции присваивания,операции
увеличения и уменьшения приводят к так называемым“побочным эффектам” - некоторые
переменные изменяются какпобочный результат вычисления выражений. В любом
выражении,в котором возникают побочные эффекты, могут существоватьочень тонкие
зависимости от порядка, в котором определяютсявходящие в него переменные. примером
типичной неудачной си-туации является оператор A[I] = I++;Возникает вопрос,
старое или новое значение I служит в ка-честве индекса. Компилятор может поступать
разными способамии в зависимости от своей интерпретации выдавать разные ре-зультаты.
Тот случай, когда происходят побочные эффекты(присваивание фактическим
переменным), - оставляется на ус-мотрение компилятора, так как наилучший порядок
сильно зави-сит от архитектуры машины.Из этих рассуждений вытекает такая мораль:
написаниепрограмм, зависящих от порядка вычислений, является плохимметодом
программирования на любом языке. Конечно, необходимознать, чего следует избегать,
но если вы не в курсе, как не-которые вещи реализованы на разных машинах,
это неведениеможет предохранить вас от неприятностей. (Отладочная прог-рамма LINT
укажет большинство мест, зависящих от порядка вы-числений. * 59 - 3. Поток
управления Управляющие операторы языка определяют порядок вычисле-ний. В приведенных
ранее примерах мы уже встречались с наи-более употребительными управляющими
конструкциями языка “C”;здесь мы опишем остальные операторы управления и уточнимдействия
операторов, обсуждавшихся ранее. 3.1. Операторы и блоки Такие выражения,
как X=0, или I++, или PRINTF(...),становятся операторами, если за ними следует
точка с запя-той, как, например, X = 0;I++;PRINTF(...);В языке “C” точка
с запятой является признаком конца опера-тора, а не разделителем операторов, как
в языках типа алго-ла.Фигурные скобки /( и /) используются для объединенияописаний
и операторов в составной оператор или блок, так чтоони оказываются синтаксически
эквивалентны одному оператору.Один явный пример такого типа дают фигурные
скобки, в кото-рые заключаются операторы, составляющие функцию, другой -фигурные
скобки вокруг группы операторов в конструкциях IF,ELSE, WHILE и FOR.(на самом
деле переменные могут быть опи-саны внутри любого блока; мы поговорим об этом
в главе 4).Точка с запятой никогда не ставится после первой фигурнойскобки,
которая завершает блок. 3.2. IF - ELSE Оператор IF - ELSE используется при необходимости
сде-лать выбор. Формально синтаксис имеет видIF (выражение)оператор-1ELSEоператор-2,Где
часть ELSE является необязательной. Сначала вычисля-ется выражение;
если оно “истинно” /т.е. значение выраженияотлично от нуля/, то выполняется
оператор-1. Если оно ложно/значение выражения равно нулю/, и если есть часть
с ELSE,то вместо оператора-1 выполняется оператор-2. * 60 - Так как IF просто
проверяет численное значение выраже-ния, то возможно некоторое сокращение записи.
Самой очевид-ной возможностью является запись IF (выражение)вместоIF (выражение
!=0)иногда такая запись является ясной и естественной, но време-нами она
становится загадочной.То, что часть ELSE в конструкции IF - ELSE является нео-бязательной,
приводит к двусмысленности в случае, когда ELSEопускается во вложенной
последовательности операторов IF.Эта неоднозначность разрешается обычным образом
- ELSE свя-зывается с ближайшим предыдущим IF, не содержащим ELSE.Например,
в IF ( N > 0 )IF( A > B )Z = A;ELSEZ = B;конструкция ELSE относится к внутреннему
IF, как мы и пока-зали, сдвинув ELSE под соответствующий IF. Если это не
то,что вы хотите, то для получения нужного соответствия необхо-димо использовать
фигурные скобки: IF (N > 0) {IF (A > B)Z = A; }ELSEZ = B; Tакая двусмысленность
особенно пагубна в ситуациях типаIF (N > 0)FOR (I = 0; I 0) {PRINTF(“...”);RETURN(I);
}ELSE /* WRONG */PRINTF(“ERROR - N IS ZERO\N”);* 61 - Запись ELSE под
IF ясно показывает, чего вы хотите, но ком-пилятор не получит соответствующего
указания и свяжет ELSE свнутренним IF. Ошибки такого рода очень трудно обнаруживают-ся.Между
прочим, обратите внимание, что вIF (A > B)Z = A;ELSEZ = B;после
Z=A стоит точка с запятой. Дело в том, что согласнограмматическим правилам за
IF должен следовать оператор, авыражение типа Z=A, являющееся оператором, всегда
заканчива-ется точкой с запятой. 3.3. ELSE - IF КонструкцияIF (выражение)оператор
ELSE IF (выражение) оператор ELSE IF (выражение) операторELSEоператорвстречается
настолько часто, что заслуживает отдельногократкого рассмотрения. Такая
последовательность операторовIF является наиболее распространенным способом программиро-вания
выбора из нескольких возможных вариантов. выраженияпросматриваются
последовательно; если какое-то выражениеоказывается истинным,то выполняется
относящийся к нему опе-ратор, и этим вся цепочка заканчивается. Каждый оператор
мо-жет быть либо отдельным оператором, либо группой операторовв фигурных скобках.Последняя
часть с ELSE имеет дело со случаем, когда ниодно из проверяемых условий
не выполняется. Иногда при этомне надо предпринимать никаких явных действий;
в этом случаехвост ELSEоператорможет быть опущен, или его можно использовать
для контроля,чтобы засечь “невозможное” условие. * 62 - Для иллюстрации выбора
из трех возможных вариантов при-ведем программу функции, которая методом половинного
деленияопределяет, находится ли данное значение х в отсортированноммассиве
V. Элементы массива V должны быть расположены в по-рядке возрастания. Функция
возвращает номер позиции (числомежду 0 и N-1), в которой значение х находится
в V, и -1,если х не содержится в V. BINARY(X, V, N) /* FIND X IN V[0]...V[N-1]
*/INT X, V[], N; {INT LOW, HIGH, MID;LOW = 0;HIGH = N - 1;WHILE (LOW V[MID])LOW
= MID + 1;ELSE /* FOUND MATCH */RETURN(MID); }RETURN(-1); } Основной частью
каждого шага алгоритма является провер-ка, будет ли х меньше, больше или равен
среднему элементуV[MID]; использование конструкции ELSE - IF здесь вполне ес-тественно.
3.4. Переключатель Оператор SWITCH дает специальный способ выбора одного
измногих вариантов, который заключается в проверке совпадениязначения данного
выражения с одной из заданных констант исоответствующем ветвлении. В главе 1
мы привели программуподсчета числа вхождений каждой цифры, символов пустых про-межутков
и всех остальных символов, использующую последова-тельность IF...ELSE
IF...ELSE. Вот та же самая программа спереключателем. * 63 - MAIN() /* COUNT DIGITS,WHITE
SPACE, OTHERS */ {INT C, I, NWHITE, NOTHER, NDIGIT[10];NWHITE = NOTHER
= 0;FOR (I = 0; I = '0' &S[I] 0; GAP /= 2)FOR (I = GAP; I =0 &V[J]>V[J+GAP];
J-=GAP) {TEMP = V[J];V[J] = V[J+GAP];V[J+GAP] = TEMP; } } Здесь имеются три вложенных
цикла. Самый внешний цикл управ-ляет интервалом между сравниваемыми элементами,
уменьшая егоот N/2 вдвое при каждом проходе, пока он не станет равнымнулю.
Средний цикл сравнивает каждую пару элементов, разде-ленных на величину интервала;
самый внутренний цикл перес-тавляет любую неупорядоченную пару. Так как
интервал в концеконцов сводится к единице, все элементы в результате упоря-дочиваются
правильно. Отметим, что в силу общности конструк-ции FOR внешний цикл
укладывается в ту же самую форму, что иостальные, хотя он и не является арифметической
прогрессией.Последней операцией языка “C” является запятая “,”, ко-торая
чаще всего используется в операторе FOR. Два выраже-ния, разделенные запятой,
вычисляются слева направо, причемтипом и значением результата являются тип и значение
правогооперанда. Таким образом, в различные части оператора FORможно включить
несколько выражений, например, для параллель-ного изменения двух индексов.
Это иллюстрируется функциейREVERSE(S), которая располагает строку S в обратном
порядкена том же месте. REVERSE(S) /* REVERSE STRING S IN PLACE */CHAR S[];
{INT C, I, J;FOR(I = 0, J = STRLEN(S) - 1; I 0); /* DELETE IT */ IF (SIGN 0) {WHILE
(--N >= 0)IF (LINE[N] != ' ' &LINE[N] != '\T'&LINE[N] != '\N')BREAK;LINE[N+1]
= '\0';PRINTF(“%S\N”,LINE); } } Функция GETLINE возвращает длину строки. Внутренний
циклначинается с последнего символа LINE (напомним, что—Nуменьшает N до
использования его значения) и движется в об-ратном направлении в поиске первого
символа , который отли-чен от пробела, табуляции или новой строки. Цикл прерывает-ся,
когда либо найден такой символ, либо N становится отри-цательным (т.е.,
когда просмотрена вся строка). Советуем вамубедиться, что такое поведение правильно
и в том случае,когда строка состоит только из символов пустых промежутков.В
качестве альтернативы к BRеак можно ввести проверку всам цикл:WHILE ((N =
GETLINE(LINE,MAXLINE)) > 0) {WHILE (--N >= 0&(LINE[N] == ' ' \!\! LINE[N] == '\T'\!\!
LINE[N] == '\N')) ; ... } * 71 - Это уступает предыдущему варианту, так
как проверка стано-вится труднее для понимания. Проверок, которые требуют пе-реплетения
&,\!\!, ! И круглых скобок, по возможности сле-дует избегать. 3.8. Оператор
CONTINUE Оператор CONTINUE родственен оператору BRеак, но исполь-зуется реже;
он приводит к началу следующей итерации охваты-вающего цикла (FOR, WHILE,
DO ). В циклах WHILE и DO это оз-начает непосредственный переход к выполнению проверочнойчасти;
в цикле FOR управление передается на шаг реинициали-зации. (Оператор
CONTINUE применяется только в циклах, но нев переключателях. Оператор CONTINUE
внутри переключателявнутри цикла вызывает выполнение следующей итерации
цикла).В качестве примера приведем фрагмент, который обрабаты-вает только положительные
элементы массива а; отрицательныезначения пропускаются.FOR (I = 0; I
0)IF (INDEX(LINE, “THE”) >= 0)PRINTF(“%S”, LINE); } * 75 - GETLINE(S, LIM) /* GET
LINE INTO S, RETURN LENGTH *CHAR S[];INT LIM; {INT C, I;I = 0;WHILE(--LIM>0 &(C=GETCHAR())
!= EOF &C != '\N')S[I++] = C;IF (C == '\N')S[I++] = C;S[I] = '\0';RETURN(I);
} INDEX(S,T) /* RETURN INDEX OF T IN S,-1 IF NONE */CHAR S[], T[];
{INT I, J, K;FOR (I = 0; S[I] != '\0'; I++) {FOR(J=I, K=0; T[K] !='\0' &S[J] ==
T[K]; J++; K++) ;IF (T[K] == '\0')RETURN(I); }RETURN(-1); } Каждая функция имеет
вид имя (список аргументов, если ониимеются) описания аргументов, если они имеются
{описания и операторы , если они имеются } Как и указывается, некоторые
части могут отсутство-вать; минимальной функцией является DUMMY () { }которая не
совершает никаких действий./Такая ничего не делающая функция иногда оказываетсяудобной
для сохранения места для дальнейшего развития прог-раммы/. если функция
возвращает что-либо отличное от целогозначения, то перед ее именем может стоять
указатель типа;этот вопрос обсуждается в следующем разделе. * 76 - Программой
является просто набор определений отдельныхфункций. Связь между функциями осуществляется
через аргумен-ты и возвращаемые функциями значения /в этом случае/;
ееможно также осуществлять через внешние переменные. Функциимогут располагаться
в исходном файле в любом порядке, а самаисходная программа может размещаться
на нескольких файлах,но так, чтобы ни одна функция не расщеплялась.Оператор RETURN
служит механизмом для возвращения зна-чения из вызванной функции в функцию,
которая к ней обрати-лась. За RETURN может следовать любое выражение:RETURN (выражение)Вызывающая
функция может игнорировать возвращаемоезначение, если она
этого пожелает. Более того, после RETURNможет не быть вообще никакого выражения;
в этом случае в вы-зывающую программу не передается никакого значения. Управле-ние
также возвращется в вызывающую программу без передачикакого-либо значения
и в том случае, когда при выполнении мы“проваливаемся” на конец функции, достигая
закрывающейсяправой фигурной скобки. EСли функция возвращает значение изодного
места и не возвращает никакого значения из другогоместа, это не является незаконным,
но может быть признакомкаких-то неприятностей. В любом случае “значением”
функции,которая не возвращает значения, несомненно будет мусор. От-ладочная
программа LINT проверяет такие ошибки.Механика компиляции и загрузки “C”-программ,
располо-женных в нескольких исходных файлах, меняется от системы ксистеме.
В системе “UNIX”, например, эту работу выполняеткоманда 'CC', упомянутая в главе
1. Предположим, что трифункции находятся в трех различных файлах с именами
MAIN.с,GETLINE.C и INDEX.с . Тогда команда CC MAIN.C GETLINE.C INDEX.Cкомпилирует
эти три файла, помещает полученный настраиваемый объектный код в файлы MAIN.O,
GETLINE.O и INDEX.O и загружа-ет их всех в выполняемый файл, называемый A.OUT
.Если имеется какая-то ошибка, скажем в MAIN.C, то этотфайл можно перекомпилировать
отдельно и загрузить вместе спредыдущими объектными файлами по команде
CC MAIN.C GETLIN.O INDEX.OКоманда 'CC' использует соглашение о наименовании с “.с”
и “.о” для того, чтобы отличить исходные файлы от объектных.Упражнение 4-1.Составьте
программу для функции RINDEX(S,T), котораявозвращает позицию самого правого
вхождения т в S и -1, еслиS не содержит T.* 77 - 4.2. Функции, возвращающие
нецелые значения. До сих пор ни одна из наших программ не содержала како-го-либо
описания типа функции. Дело в том, что по умолчаниюфункция неявно описывается
своим появлением в выражении илиоператоре, как, например, в WHILE (GETLINE(LINE,
MAXLINE) > 0)Если некоторое имя, которое не было описано ранее, появ-ляется
в выражении и за ним следует левая круглая скобка, тооно по контексту считается
именем некоторой функции. Крометого, по умолчанию предполагается, что эта функция
возвраща-ет значение типа INT. Так как в выражениях CHAR преобразует-ся
в INT, то нет необходимости описывать функции, возвращаю-щие CHAR. Эти предположения
покрывают большинство случаев,включая все приведенные до сих пор примеры.Но
что происходит, если функция должна возвратить значе-ние какого-то другого типа
? Многие численные функции, такиекак SQRT, SIN и COS возвращают DOUBLE; другие
специальныефункции возвращают значения других типов. Чтобы показать,как поступать
в этом случае, давайте напишем и используемфункцию ATоF(S), которая преобразует
строку S в эквивалент-ное ей плавающее число двойной точности. Функция
ATоF явля-ется расширением атоI, варианты которой мы написали в главах2 и 3; она
обрабатывает необязательно знак и десятичную точ-ку, а также целую и дробную
часть, каждая из которых можеткак присутствовать, так и отсутствовать./эта процедура
пре-образования ввода не очень высокого качества; иначе она бызаняла больше
места, чем нам хотелось бы/.Во-первых, сама ATоF должна описывать тип возвращаемогоею
значения, поскольку он отличен от INT. Так как в выраже-ниях тип FLOAT
преобразуется в DOUBLE, то нет никакого смыс-ла в том, чтобы ATOF возвращала
FLOAT; мы можем с равным ус-пехом воспользоваться дополнительной точностью, так
что мыполагаем, что возвращаемое значение типа DOUBLE. Имя типадолжно стоять перед
именем функции, как показывается ниже: DOUBLE ATOF(S) /* CONVERT STRING S
TO DOUBLE */CHAR S[]; {DOUBLE VAL, POWER;INT I, SIGN;* 78 - FOR(I=0; S[I]==' '
\!\! S[I]=='\N' \!\! S[I]=='\T'; I++) ; /* SKIP WHITE SPACE */ SIGN = 1; IF (S[I]
== '+' \!\! S[I] == '-') /* SIGN */SIGN = (S[I++] == '+') ? 1 : -1;FOR (VAL
= 0; S[I] >= '0' &S[I] = '0' &S[I] 0)PRINTF(“\T%.2F\N”,SUM+=ATOF(LINE)); ОисаниеDOUBLE
SUM, ATOF(); говорит, что SUM является переменной типа DOUBLE , и чтоATOF
является функцией, возвращающей значение типа DOUBLE .Эта мнемоника означает,
что значениями как SUM, так иATOF(...) являются плавающие числа двойной точности.
* 79 - Если функция ATOF не будет описана явно в обоих местах,то в “C” предполагается,
что она возвращает целое значение,и вы получите бессмысленный ответ.
Если сама ATOF и обраще-ние к ней в MAIN имеют несовместимые типы и находятся
в од-ном и том же файле, то это будет обнаружено компилятором. Ноесли ATOF была
скомпилирована отдельно /что более вероятно/,то это несоответствие не будет
зафиксировано, так что ATOFбудет возвращать значения типа DOUBLE, с которым MAIN
будетобращаться, как с INT , что приведет к бессмысленным резуль-татам. /Программа
LINT вылавливает эту ошибку/.Имея ATOF, мы, в принципе, могли бы с ее помощью
напи-сать ATOI (преобразование строки в INT):ATOI(S) /* CONVERT STRING S TO
INTEGER */CHAR S[]; {DOUBLE ATOF();RETURN(ATOF(S)); } Обратите внимание на структуру
описаний и оператор RETURN.Значение выражения в RETURN (выражение)всегда
преобразуется к типу функции перед выполнением самоговозвращения. Поэтому при
появлении в операторе RETURN значе-ние функции атоF, имеющее тип DOUBLE, автоматически
преобра-зуется в INT, поскольку функция ATOI возвращает INT. (Какобсуждалось
в главе 2, преобразование значения с плавающейточкой к типу INT осуществляется
посредством отбрасываниядробной части).Упражнение 4-2.Расширьте ATOF таким
образом, чтобы она могла работать счислами вида123.45е-6где за числом с плавающей
точкой может следовать 'E' и пока-затель экспоненты, возможно со знаком.
4.3. Еще об аргументах функций. В главе 1 мы уже обсуждали тот факт , что аргументы
фун-кций передаются по значению, т.е. вызванная функция получаетсвою временную
копию каждого аргумента, а не его адрес. этоозначает, что вызванная функция
не может воздействовать наисходный аргумент в вызывающей функции. Внутри функции
каж-дый аргумент по существу является локальной переменной, ко-торая инициализируется
тем значением, с которым к этой функ-ции обратились. * 80 - Если в качестве
аргумента функции выступает имя массива,то передается адрес начала этого
массива; сами элементы некопируются. Функция может изменять элементы массива,
исполь-зуя индексацию и адрес начала. Таким образом, массив переда-ется по ссылке.
В главе 5 мы обсудим, как использование ука-зателей позволяет функциям воздействовать
на отличные отмассивов переменные в вызывающих функциях.Между прочим,
несуществует полностью удовлетворительногоспособа написания переносимой функции
с переменным числомаргументов. Дело в том, что нет переносимого способа, с
по-мощью которого вызванная функция могла бы определить, сколь-ко аргументов было
фактически передано ей в данном обраще-нии. Таким образом, вы, например, не
можете написать дейст-вительно переносимую функцию, которая будет вычислять макси-мум
от произвольного числа аргументов, как делают встроенныефункции MAX в фортране
и PL/1.Обычно со случаем переменного числа аргументов безопасноиметь дело,
если вызванная функция не использует аргументов,которые ей на самом деле не
были переданы, и если типы сог-ласуются. Самая распространенная в языке “C” функция
с пере-менным числом - PRINTF . Она получает из первого аргументаинформацию,
позволяющую определить количество остальных ар-гументов и их типы. Функция PRINTF
работает совершенно неп-равильно, если вызывающая функция передает ей недостаточноеколичество
аргументов, или если их типы не согласуются с ти-пами, указанными
в первом аргументе. Эта функция не являетсяпереносимой и должна модифицироваться
при использовании вразличных условиях.Если же типы аргументов известны,
то конец списка аргу-ментов можно отметить, используя какое-то соглашение; напри-мер,
считая, что некоторое специальное значение аргумента(часто нуль) является
признаком конца аргументов. 4.4. Внешние переменные. Программа на языке “C”
состоит из набора внешних объек-тов, которые являются либо переменными, либо
функциями. Тер-мин “внешний” используется главным образом в противопостав-ление
термину “внутренний”, которым описываются аргументы иавтоматические переменные,
определенные внурти функций.Внешние переменные определены вне какой-либо функции
и, та-ким образом, потенциально доступны для многих функций. Самифункции всегда
являются внешними, потому что правила языка“C” не разрешают определять одни
функции внутри других. Поумолчанию внешние переменные являются также и “глобальными”,так
что все ссылки на такую переменную, использующие одно ито же имя (даже
из функций, скомпилированных независимо),будут ссылками на одно и то же. В
этом смысле внешние пере-менные аналогичны переменным COмMON в фортране и EXTERNAL
вPL/1. Позднее мы покажем, как определить внешние переменныеи функции таким
образом, чтобы они были доступны не глобаль-но, а только в пределах одного исходного
файла. * 81 - В силу своей глобальной доступности внешние переменныепредоставляют
другую, отличную от аргументов и возвращаемыхзначений, возможность для
обмена данными между функциями.Если имя внешней переменной каким-либо образом
описано, толюбая функция имеет доступ к этой переменной, ссылаясь к нейпо этому
имени.В случаях, когда связь между функциями осуществляется спомощью большого
числа переменных, внешние переменные оказы-ваются более удобными и эффективными,
чем использованиедлинных списков аргументов. Как, однако, отмечалось в главе1,
это соображение следует использовать с определенной осто-рожностью, так как
оно может плохо отразиться на структурепрограмм и приводить к программам с большим
числом связей поданным между функциями.Вторая причина использования внешних
переменных связанас инициализацией. В частности, внешние массивы могут бытьинициализированы
а автоматические нет. Мы рассмотрим вопрособ инициализации в конце
этой главы.Третья причина использования внешних переменных обуслов-лена их областью
действия и временем существования. Автома-тические переменные являются внутренними
по отношению к фун-кциям; они возникают при входе в функцию и исчезают
при вы-ходе из нее. Внешние переменные, напротив, существуют посто-янно. Они
не появляютя и не исчезают, так что могут сохра-нять свои значения в период от
одного обращения к функции додругого. В силу этого, если две функции используют
некоторыеобщие данные, причем ни одна из них не обращается к другой ,то часто
наиболее удобным оказывается хранить эти общие дан-ные в виде внешних переменных,
а не передавать их в функциюи обратно с помощью аргументов.Давайте продолжим
обсуждение этого вопроса на большомпримере. Задача будет состоять в написании
другой программыдля калькулятора, лучшей,чем предыдущая. Здесь допускаютсяоперации
+,-,*,/ и знак = (для выдачи ответа).вместо инфикс-ного представления калькулятор
будет использовать обратнуюпольскую нотацию,поскольку ее несколько легче
реализовать.вобратной польской нотации знак следует за операндами; инфик-сное
выражение типа (1-2)*(4+5)=записывается в виде12-45+*=круглые скобки при этом
не нужны * 82 - Реализация оказывается весьма простой.каждый операнд по-мещается
в стек; когда поступает знак операции,нужное числооперандов (два для бинарных
операций) вынимается,к ним при-меняется операция и результат направляется обратно
встек.так в приведенном выше примере 1 и 2 помещаются в стеки затем заменяются
их разностью, -1.после этого 4 и 5 вво-дятся в стек и затем заменяются своей
суммой,9.далее числа* 1 и 9 заменяются в стеке на их произведение,равное -9.опе-рация
= печатает верхний элемент стека, не удаляя его (так что промежуточные
вычисления могут быть проверены).Сами операции помещения чисел в стек и их извлеченияочень
просты,но, в связи с включением в настоящую программуобнаружения ошибок
и восстановления,они оказываются доста-точно длинными. Поэтому лучше оформить
их в виде отдельныхфункций,чем повторять соответствующий текст повсюду в прог-рамме.
Кроме того, нужна отдельная функция для выборки изввода следующей операции
или операнда. Таким образом, струк-тура программы имеет вид: WHILE( поступает
операция или операнд, а не конецIF ( число )поместить его в стекеLSE IF (
операция )вынуть операнды из стекавыполнить операциюпоместить результат в стекELSEошибкаОсновной
вопрос, который еще не был обсужден, заключает-ся в том,где
поместить стек, т. Е. Какие процедуры смогутобращаться к нему непосредственно.
Одна из таких возможнос-тей состоит в помещении стека в MAIN и передачи самого
стекаи текущей позиции в стеке функциям, работающим со стеком. Нофункции MAIN нет
необходимости иметь дело с переменными, уп-равляющими стеком; ей естественно
рассуждать в терминах по-мещения чисел в стек и извлечения их оттуда. В силу этого
мырешили сделать стек и связанную с ним информацию внешнимипеременными , доступными
функциям PUSH (помещение в стек) иPOP (извлечение из стека), но не MAIN.Перевод
этой схемы в программу достаточно прост. Ведущаяпрограмма является по
существу большим переключателем по ти-пу операции или операнду; это, по-видимому,
более характер-ное применеие переключателя, чем то, которое было продемонс-трировано
в главе 3. #DEFINE MAXOP 20 /* MAX SIZE OF OPERAND, OPERАTOR *#DEFINE
NUMBER '0' /* SIGNAL THAT NUMBER FOUND */#DEFINE TOOBIG '9' /* SIGNAL THAT STRING
IS TOO BIG * * 83 - MAIN() /* REVERSE POLISH DESK CALCULATOR */ /(INT TUPE;CHAR
S[MAXOP];DOUBLE OP2,ATOF(),POP(),PUSH();WHILE ((TUPE=GETOP(S,MAXOP)) !=EOF);SWITCH(TUPE)
/(CASE NUMBER:PUSH(ATOF(S));BREAK;CASE '+':PUSH(POP()+POP());BREAK;CASE
'*':PUSH(POP()*POP());BREAK;CASE '-':OP2=POP();PUSH(POP()-OP2);BREAK;CASE
'/':OP2=POP();IF (OP2 != 0.0)PUSH(POP()/OP2);ELSEPRINTF(“ZERO DIVISOR POPPED\N”);BREAK;CASE
'=':PRINTF(“\T%F\N”,PUSH(POP()));BREAK;CASE 'C':CLEAR();BREAK;CASE
TOOBIG:PRINTF(“%.20S ... IS TOO LONG\N”,S)BREAK; /) /)#DEFINE MAXVAL 100 /*
MAXIMUM DEPTH OF VAL STACK */* 84 - INT SP = 0; /* STACK POINTER */DOUBLE VAL[MAXVAL];
/*VALUE STACK */DOUBLE PUSH(F) /* PUSH F ONTO VALUE STACK */DOUBLE F;
/(IF (SP 0)RETURN(VAL[--SP]); ELSE /( PRINTF(“ERROR: STACK EMPTY\N”); CLEAR();
RETURN(0); /) /) CLEAR() /* CLEAR STACK */ /(SP=0; /) Команда C очищает стек с
помощью функции CLEAR, котораятакже используется в случае ошибки функциями PUSH
и POP. кфункции GETOP мы очень скоро вернемся.Как уже говорилось в главе 1, переменная
является внеш-ней, если она определена вне тела какой бы то ни было функ-ции.
Поэтому стек и указатель стека, которые должны исполь-зоваться функциями
PUSH, POP и CLEAR, определены вне этихтрех функций. Но сама функция MAIN не
ссылается ни к стеку,ни к указателю стека - их участие тщательно замаскировано.
Всилу этого часть программы, соответствующая операции = , ис-пользует конструкцию
PUSH(POP()); для того, чтобы проанализировать верхний элемент стека, неизменяя
его.Отметим также, что так как операции + и * коммутативны,порядок, в котором
объединяются извлеченные операнды, несу-щественен, но в случае операций - и
/ необходимо различатьлевый и правый операнды. * 85 - Упражнение 4-3.Приведенная
основная схема допускает непосредственноерасширение возможностей калькулятора.
Включите операцию де-ления по модулю /%/ и унарный минус. Включите команду “сте-реть”,
которая удаляет верхний элемент стека. Введите коман-ды для работы с
переменными. /Это просто, если имена пере-менных будут состоять из одной буквы
из имеющихся двадцатишести букв/. 4.5. Правила, определяющие область действия.
Функции и внешние переменные, входящие в состав“C”-программы, не обязаны компилироваться
одновременно;программа на исходном языке может располагаться в несколькихфайлах,
и ранее скомпилированные процедуры могут загружатьсяиз библиотек. Два
вопроса представляют интерес:Как следует составлять описания, чтобы переменные
пра-вильно воспринимались во время компиляции ?Как следует составлять описания,
чтобы обеспечить пра-вильную связь частей программы при загрузке ?4.5.1. Область
действия. Областью действия имени является та часть программы, вкоторой это
имя определено. Для автоматической переменной,описанной в начале функции, областью
действия является тафункция, в которой описано имя этой переменной, а переменныеиз
разных функций, имеющие одинаковое имя, считаются не от-носящимися друг
к другу. Это же справедливо и для аргументовфункций.Область действия внешней
переменной простирается от точ-ки, в которой она объявлена в исходном файле,
до конца этогофайла. Например, если VAL, SP, PUSH, POP и CLEAR определеныв одном
файле в порядке, указанном выше, а именно: INT SP = 0;DOUBLE VAL[MAXVAL];DOUBLE
PUSH(F) {...}DOUBLE POP() {...}CLEAR() {...}то переменные VAL и SP можно использовать
в PUSH, POP иCLEAR прямо по имени; никакие дополнительные описания ненужны.С
другой стороны, если нужно сослаться на внешнюю пере-менную до ее определения,
или если такая переменная опреде-лена в файле, отличном от того, в котором
она используется,то необходимо описание EXTERN. * 86 - Важно различать описание
внешней переменной и ее опреде-ление. описание указывает свойства переменной
/ее тип, раз-мер и т.д./; определение же вызывает еще и отведение памяти.Если
вне какой бы то ни было функции появляются строчки INT SP;DOUBLE VAL[MAXVAL];то
они определяют внешние переменные SP и VAL, вызывают от-ведение памяти для них
и служат в качестве описания для ос-тальной части этого исходного файла. В то
же время строчки EXTERN INT SP;EXTERN DOUBLE VAL[];описывают в остальной части
этого исходного файла переменнуюSP как INT, а VAL как массив типа DOUBLE /размер
которогоуказан в другом месте/, но не создают переменных и не отво-дят им места
в памяти.Во всех файлах, составляющих исходную программу, должносодержаться
только одно определение внешней переменной; дру-гие файлы могут содержать описания
EXTERN для доступа к ней./Описание EXTERN может иметься и в том файле, где
находитсяопределение/. Любая инициализация внешней переменной прово-дится только
в определении. В определении должны указыватьсяразмеры массивов, а в описании
EXTERN этого можно не делать.Хотя подобная организация приведенной выше программы
ималовероятна, но VAL и SP могли бы быть определены и инициа-лизированы в
одном файле, а функция PUSH, POP и CLEAR опре-делены в другом. В этом случае для
связи были бы необходимыследующие определения и описания: в файле 1: INT SP
= 0; /* STACK POINTER */DOUBLE VAL[MAXVAL]; /* VALUE STACK */в файле 2: EXTERN
INT SP;EXTERN DOUBLE VAL[];DOUBLE PUSH(F) {...} DOUBLE POP() {...} CLEAR() {...}
так как описания EXTERN 'в файле 1' находятся выше и внетрех указанных функций,
они относятся ко всем ним; одногонабора описаний достаточно для всего 'файла
2'. * 87 - Для программ большого размера обсуждаемая позже в этойглаве возможность
включения файлов, #INCLUDE, позволяетиметь во всей программе только одну копию
описаний EXTERN ивставлять ее в каждый исходный файл во время его компиляции.Обратимся
теперь к функции GETOP, выбирающей из файлаввода следующую операцию
или операнд. Основная задача прос-та: пропустить пробелы, знаки табуляции и новые
строки. Еслиследующий символ отличен от цифры и десятичной точки, товозвратить
его. В противном случае собрать строку цифр /онаможет включать десятичную
точку/ и возвратить NUMBER каксигнал о том, что выбрано число.Процедура существенно
усложняется, если стремиться пра-вильно обрабатывать ситуацию, когда вводимое
число оказыва-ется слишком длинным. Функция GETOP считывает цифры подряд/возможно
с десятичной точкой/ и запоминает их, пока после-довательность не прерывается.
Если при этом не происходитпереполнения, то функция возвращает NUMBER и
строку цифр.Если же число оказывается слишком длинным, то GETOP отбрасы-вает остальную
часть строки из файла ввода, так что пользо-ватель может просто перепечатать
эту строку с места ошибки;функция возвращает TOOBIG как сигнал о переполнении.
GETOP(S, LIM) /* GET NEXT OPRERATOR OR OPERAND */CHAR S[];INT LIM; {INT I,
C;WHILE((C=GETCH())==' '\!\! C=='\T' \!\! C=='\N') ;IF (C != '.' &(C '9'))RETURN©;S[0]
= C;FOR(I=1; (C=GETCHAR()) >='0' &C ='0' &C 0) ? BUF[--BUFP] : GETCHAR());
} UNGETCH© /* PUSH CHARACTER BACK ON INPUT */INT C; {IF (BUFP > BUFSIZE)PRINTF(“UNGETCH:
TOO MANY CHARACTERS\N”);ELSEBUF [BUFP++] = C; } Мы использовали
для хранения возвращаемых символов массив, ане отдельный символ, потому что такая
общность может приго-диться в дальнейшем. * 89 - Упражнение 4-4.Напишите функцию
UNGETS(S) , которая будет возвращать воввод целую строку. Должна ли UNGETS
иметь дело с BUF и BUFPили она может просто использовать UNGETCH ?Упражнение
4-5.Предположите, что может возвращаться только один символ. Из-мените GETCH и
UNGETCH соответствующим образом.Упражнение 4-6.Наши функции GETCH и UNGETCH не
обеспечивают обработку возв-ращенного символа EOF переносимым образом. Решите,
какимсвойством должны обладать эти функции, если возвращаетсяEOF, и реализуйте
ваши выводы. 4.6. Статические переменные. Статические переменные представляют собой
третий класспамяти, в дополнении к автоматическим переменным и EXTERN, скоторыми
мы уже встречались.Статические переменные могут быть либо внутренними, либовнешними.
Внутренние статические переменные точно так же,как и автоматические,
являются локальными для некоторой фун-кции, но, в отличие от автоматических,
они остаются сущест-вовать, а не появляются и исчезают вместе с обращением кэтой
функции. это означает, что внутренние статические пере-менные обеспечивают постоянное,
недоступное извне хранениевнутри функции. Символьные строки, появляющиеся
внутри функ-ции, как, например, аргументы PRINTF , являются внутреннимистатическими.Внешние
статические переменные определены в остальнойчасти того исходного
файла, в котором они описаны, но не вкаком-либо другом файле. Таким образом,
они дают способскрывать имена, подобные BUF и BUFP в комбинацииGETCH-UNGETCH,
которые в силу их совместного использованиядолжны быть внешними, но все же не
доступными для пользова-телей GETCH и UNGETCH , чтобы исключалась возможность
конф-ликта. Если эти две функции и две переменные объеденить водном файле следующим
образом STATIC CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */STATIC INT BUFP=0;
/*NEXT FREE POSITION IN BUF */GETCH() {...}UNGETCH() {...}то никакая другая
функция не будет в состоянии обратиться кBUF и BUFP; фактически, они не будут
вступать в конфликт стакими же именами из других файлов той же самой программы.Статическая
память, как внутренняя, так и внешняя, спе-цифицируется словом STATIC
, стоящим перед обычным описани-ем. Переменная является внешней, если она описана
вне какойбы то ни было функции, и внутренней, если она описана внутринекоторой
функции. * 90 - Нормально функции являются внешними объектами; их именаизвестны
глобально. возможно, однако, объявить функцию какSTATIC ; тогда ее имя становится
неизвестным вне файла, вкотором оно описано.В языке “C” “STATIC” отражает
не только постоянство, нои степень того, что можно назвать “приватностью”.
Внутренниестатические объекты определены только внутри одной функции;внешние статические
объекты /переменные или функции/ опреде-лены только внутри того исходного
файла, где они появляются,и их имена не вступают в конфликт с такими же именами
пере-менных и функций из других файлов.Внешние статические переменные и
функции предоставляютспособ организовывать данные и работающие с ними внутренниепроцедуры
таким образом, что другие процедуры и данные немогут прийти с ними в
конфликт даже по недоразумению. Напри-мер, функции GETCH и UNGETCH образуют “модуль”
для ввода ивозвращения символов; BUF и BUFP должны быть статическими,чтобы
они не были доступны извне. Точно так же функции PUSH,POP и CLEAR формируют
модуль обработки стека; VAR и SP тожедолжны быть внешними статическими. 4.7. Регистровые
переменные. Четвертый и последний класс памяти называется регистро-вым.
Описание REGISTER указывает компилятору, что данная пе-ременная будет часто
использоваться. Когда это возможно, пе-ременные, описанные как REGISTER, располагаются
в машинныхрегистрах, что может привести к меньшим по размеру и болеебыстрым
программам. Описание REGISTER выглядит как REGISTER INT X;REGISTER CHAR C;и
т.д.; часть INT может быть опущена. Описание REGISTER мож-но использовать только
для автоматических переменных и фор-мальных параметров функций. В этом последнем
случае описаниявыглядят следующим образом: F(C,N)REGISTER INT C,N; {REGISTER
INT I; ... } * 91 - На практике возникают некоторые ограничения на регистро-вые
переменные, отражающие реальные возможности имеющихсяаппаратных средств. В
регистры можно поместить только нес-колько переменных в каждой функции, причем
только определен-ных типов. В случае превышения возможного числа или исполь-зования
неразрешенных типов слово REGISTER игнорируется.Кроме того невозможно извлечь
адрес регистровой переменной(этот вопрос обсуждается в главе 5). Эти специфические
огра-ничения варьируются от машины к машине. Так, например, наPDP-11 эффективными
являются только первые три описанияREGISTER в функции, а в качестве
типов допускаются INT, CHARили указатель. 4.8. Блочная структура. Язык “C” не
является языком с блочной структурой в смыс-ле PL/1 или алгола; в нем нельзя описывать
одни функциивнутри других.Переменные же, с другой стороны, могут определяться
пометоду блочного структурирования. Описания переменных (вклю-чая инициализацию)
могут следовать за левой фигурной скоб-кой,открывающей любой оператор,
а не только за той, с кото-рой начинается тело функции. Переменные, описанные
таким об-разом, вытесняют любые переменные из внешних блоков, имеющиетакие же имена,
и остаются определенными до соответствующейправой фигурной скобки. Например
в IF (N > 0) {INT I; /* DECLARE A NEW I */FOR (I = 0; I 0); /* DISCARD IT */WHILE
(--I >= 0)PUTCHAR(S[I]); } Альтернативой этому способу является рекурсивное
реше-ние, когда при каждом вызове функция PRINTD сначала сноваобращается к себе,
чтобы скопировать лидирующие цифры, а за-тем печатает последнюю цифру. PRINTD(N)
/* PRINT N IN DECIMAL (RECURSIVE)*/INT N; (INT I;IF (N 0) THENBEGINA = 1;B
= 2ENDИмеется также возможность определения макроса с аргумен-тами, так что
заменяющий текст будет зависеть от вида обра-щения к макросу. Определим, например,
макрос с именем MAXследующим образом: #DEFINE MAX(A, B) ((A) > (B) ? (A) :
(B))когда строкаX = MAX(P+Q, R+S);будет заменена строкойX = ((P+Q) > (R+S) ? (P+Q)
: (R+S));Такая возможность обеспечивает “функцию максимума”, котораярасширяется
в последовательный код, а не в обращение к функ-ции. При правильном обращении
с аргументами такой макрос бу-дет работать с любыми типами данных; здесь нет
необходимостив различных видах MAX для данных разных типов, как это былобы с
функциями. * 97 - Конечно, если вы тщательно рассмотрите приведенное вышерасширение
MAX, вы заметите определенные недостатки. Выраже-ния вычисляются дважды; это
плохо, если они влекут за собойпобочные эффекты, вызванные, например, обращениями
к функци-ям или использованием операций увеличения. Нужно позаботить-ся о
правильном использовании круглых скобок, чтобы гаранти-ровать сохранение требуемого
порядка вычислений. (Рассмотри-те макрос #DEFINE SQUARE(X) X * Xпри обращении
к ней, как SQUARE(Z+1)). Здесь возникают даженекоторые чисто лексические проблемы:
между именем макро илевой круглой скобкой, открывающей список ее аргументов,
недолжно быть никаких пробелов.Тем не менее аппарат макросов является весьма
ценным.Один практический пример дает описываемая в главе 7 стандар-тная библиотека
ввода-вывода, в которой GETCHAR и PUTCHARопределены как макросы (очевидно
PUTCHAR должна иметь аргу-мент), что позволяет избежать затрат на обращение
к функциипри обработке каждого символа.Другие возможности макропроцессора описаны
в приложенииА.Упражнение 4-9.Определите макрос SWAP(X, Y), который обменивает
значе-ниями два своих аргумента типа INT. (В этом случае поможетблочная структура).*
98 - 5.Указатели и массивыУказатель - это переменная, содержащая адрес
другой пе-ременной. указатели очень широко используются в языке “C”.Это происходит
отчасти потому, что иногда они дают единст-венную возможность выразить нужное
действие, а отчасти пото-му, что они обычно ведут к более компактным и эффективнымпрограммам,
чем те, которые могут быть получены другими спо-собами.Указатели
обычно смешивают в одну кучу с операторамиGOTO, характеризуя их как чудесный
способ написания прог-рамм, которые невозможно понять. Это безусловно спрAведливо,если
указатели используются беззаботно; очень просто ввестиуказатели, которые
указывают на что-то совершенно неожидан-ное. Однако, при определенной дисциплине,
использование ука-зателей помогает достичь ясности и простоты. Именно этот
ас-пект мы попытаемся здесь проиллюстрировать. 5.1. Указатели и адреса Так
как указатель содержит адрес объекта, это дает воз-можность “косвенного” доступа
к этому объекту через указа-тель. Предположим, что х - переменная, например,
типа INT, арх - указатель, созданный неким еще не указанным способом.Унарная операция
выдает адрес объекта, так что оператор рх = хприсваивает адрес х переменной
рх; говорят, что рх “ука-зывает” на х. Операция применима только к переменным
иэлементам массива, конструкции вида (х-1)и 3являются не-законными. Нельзя также
получить адрес регистровой перемен-ной.Унарная операция * рассматривает свой
операнд как адресконечной цели и обращается по этому адресу, чтобы извлечьсодержимое.
Следовательно, если Y тоже имеет тип INT, то Y = *рх;присваивает Y содержимое
того, на что указывает рх. Так пос-ледовательность рх = хY = *рх; присваивает
Y то же самое значение, что и операторY = X;Переменные, участвующие во
всем этом необходимо описать:INT X, Y;INT *PX;* 99 - с описанием для X и Y мы уже
неодонократно встречались.Описание указателя INT *PX;является новым и должно
рассматриваться как мнемоническое;оно говорит, что комбинация *PX имеет тип INT.
Это означает,что если PX появляется в контексте *PX, то это эквивалентнопеременной
типа INT. Фактически синтаксис описания перемен-ной имитирует синтаксис
выражений, в которых эта переменнаяможет появляться. Это замечание полезно во всех
случаях,связанных со сложными описаниями. Например, DOUBLE ATOF(), *DP;говорит,
что ATOF() и *DP имеют в выражениях значения типаDOUBLE. Вы должны также заметить,
что из этого описания следу-ет, что указатель может указывать только на
определенный видобъектов.Указатели могут входить в выражения. Например, если
PXуказывает на целое X, то *PX может появляться в любом кон-тексте, где может встретиться
X. Так оператор Y = *PX + 1присваивает Y значение, на 1 большее значения
X;PRINTF(“%D\N”, *PX)печатает текущее значение X;D = SQRT((DOUBLE) *PX)получает
в D квадратный корень из X, причем до передачи фун-кции SQRT значение X преобразуется
к типу DOUBLE. (Смотриглаву 2).В выражениях видаY = *PX + 1унарные
операции * и связаны со своим операндом болеекрепко, чем арифметические операции,
так что такое выражениеберет то значение, на которое указывает PX, прибавляет
1 иприсваивает результат переменной Y. Мы вскоре вернемся к то-му, что может
означать выражение Y = *(PX + 1)Ссылки на указатели могут появляться и в левой
частиприсваиваний. Если PX указывает на X, то*PX = 0* 100 - полагает X равным
нулю, а*PX += 1увеличивает его на единицу, как и выражение(*PX)++Круглые скобки
в последнем примере необходимы; если их опус-тить, то поскольку унарные операции,
подобные * и ++, выпол-няются справа налево, это выражение увеличит PX, а не
ту пе-ременную, на которую он указывает.И наконец, так как указатели являются
переменными, то сними можно обращаться, как и с остальными переменными. ЕслиPY
- другой указатель на переменную типа INT, то PY = PXкопирует содержимое PX в
PY, в результате чего PY указываетна то же, что и PX. 5.2. Указатели и аргументы
функций Так как в “с” передача аргументов функциям осуществляет-ся “по значению”,
вызванная процедура не имеет непосредст-венной возможности изменить переменную
из вызывающей прог-раммы. Что же делать, если вам действительно надо изменитьаргумент?
например, программа сортировки захотела бы поме-нять два нарушающих
порядок элемента с помощью функции сименем SWAP. Для этого недостаточно написать
SWAP(A, B);определив функцию SWAP при этом следующим образом:SWAP(X, Y) /*
WRONG */INT X, Y; {INT TEMP;TEMP = X;X = Y;Y = TEMP; } из-за вызова по значению
SWAP не может воздействовать наагументы A и B в вызывающей функции.К счастью,
все же имеется возможность получить желаемыйэффект. Вызывающая программа передает
указатели подлежащихизменению значений:SWAP(A,B) * 101 - так как операция выдает
адрес переменной, то A являетсяуказателем на A. В самой SWAP аргументы описываются
как ука-затели и доступ к фактическим операндам осуществляется черезних.
SWAP(PX, PY) /* INTERCHANGE *PX AND *PY */INT *PX, *PY; {INT TEMP;TEMP = *PX;*PX
= *PY;*PY = TEMP; } Указатели в качестве аргументов обычно используются вфункциях,
которые должны возвращать более одного значения.(Можно сказать, что SWAP
вOзвращает два значения, новые зна-чения ее аргументов). В качестве примера
рассмотрим функциюGETINT, которая осуществляет преобразование поступающих всвоболном
формате данных, разделяя поток символов на целыезначения, по одному целому
за одно обращение. Функция GETINTдолжна возвращать либо найденное значение, либо
признак кон-ца файла, если входные данные полностью исчерпаны. Эти зна-чения
должны возвращаться как отдельные объекты, какое бызначение ни использовалось
для EOF, даже если это значениевводимого целого.Одно из решений, основывающееся
на описываемой в главе 7функции ввода SCANF, состоит в том, чтобы при выходе
на ко-нец файла GETINT возвращала EOF в качестве значения функции;любое другое
возвращенное значение говорит о нахождении нор-мального целого. Численное же значение
найденного целоговозвращается через аргумент, который должен быть указателемцелого.
Эта организация разделяет статус конца файла и чис-ленные значения.Следующий
цикл заполняет массив целыми с помощью обраще-ний к функции GETINT:INT
N, V, ARRAY[SIZE];FOR (N = 0; N = '0' &C = ALLOCBUF &P = ALLOCBUF &P = и т.д.,
работаютнадлежащим образом. Например, P 0 IF S>T */CHAR S[], T[]; {INT I;I =
0;WHILE (S[I] == T[I])IF (S[I++] == '\0')RETURN(0);RETURN(S[I]-T[I]); } Вот версия
STRCMP с указателями:STRCMP(S, T) /* RETURN 0 IF S>T */ CHAR *S, *T; {FOR (
; *S == *T; S++, T++)IF (*S == '\0')RETURN(0);RETURN(*S-*T); }так как ++ и—могут
быть как постфиксными, так ипрефиксными операциями, встречаются другие комбинации
* и++ и --, хотя и менее часто.Например*++P* 111 - увеличивает P до извлечения
символа, на который указываетP, а*--Pсначала уменьшает P.Упражнение 5-2.Напишите
вариант с указателями функции STRCAT из главы2: STRCAT(S, T) копирует строку
T в конец S.Упражнение 5-3.Напишите макрос для STRCPY.Упражнение 5-4.Перепишите
подходящие программы из предыдущих глав и уп-ражнений, используя указатели
вместо индексации массивов.Хорошие возможности для этого предоставляют функции
GETLINE/главы 1 и 4/, ATOI, ITOA и их варианты /главы 2, 3 и 4/,REVERSE /глава
3/, INDEX и GETOP /глава 4/.5.6. Указатели - не целые. Вы, возможно, обратили
внимание в предыдущих “с”-прог-раммах на довольно непринужденное отношение к
копированиюуказателей. В общем это верно, что на большинстве машин ука-затель можно
присвоить целому и передать его обратно, не из-менив его; при этом не происходит
никакого масштабированияили преобразования и ни один бит не теряется. к
сожалению,это ведет к вольному обращению с функциями, возвращающимиуказатели, которые
затем просто передаются другим функциям,* необходимые описания указателей
часто опускаются. Рассмот-рим, например, функцию STRSAVE(S), которая копирует
строку Sв некоторое место для хранения, выделяемое посредством обра-щения к функции
ALLOC, и возвращает указатель на это место.Правильно она должна быть записана
так:CHAR STRSAVE(S) / SAVE STRING S SOMEWHERE */ CHAR *S; {CHAR *P, *ALLOC();IF
((P = ALLOC(STRLEN(S)+1)) != NULL)STRCPY(P, S);RETURN(P); } на практике существует
сильное стремление опускать описания:* 112 - STRSAVE(S) / SAVE STRING
S SOMEWHERE */ {CHAR *P;IF ((P = ALLOC(STRLEN(S)+1)) != NULL)STRCPY(P, S);RETURN(P);
} Эта программа будет правильно работать на многих маши-нах, потому что
по умолчанию функции и аргументы имеют типINT, а указатель и целое обычно можно
безопасно пересылатьтуда и обратно. Однако такой стиль программирования в своемсуществе
является рискованным, поскольку зависит от деталейреализации и архитектуры
машины и может привести к непра-вильным результатам на конкретном используемом
вами компиля-торе. Разумнее всюду использовать полные описания. (Отладоч-ная
программа LINT предупредит о таких конструкциях, еслиони по неосторожности
все же появятся). 5.7. Многомерные массивы. В языке “C” предусмотрены прямоугольные
многомерные мас-сивы, хотя на практике существует тенденция к их значительноболее
редкому использованию по сравнению с массивами указа-телей. В этом разделе
мы рассмотрим некоторые их свойства.Рассмотрим задачу преобразования дня месяца
в день годаи наоборот. Например, 1-ое марта является 60-м днем невисо-косного
года и 61-м днем високосного года. Давайте введемдве функции для выполнения
этих преобразований: DAY_OF_YEARпреобразует месяц и день в день года, а MONTH_DAY
преобразу-ет день года в месяц и день. Так как эта последняя функциявозвращает
два значения, то аргументы месяца и дня должныбыть указателями: MONTH_DAY(1977,
60, M,&)Полагает M равным 3 и D равным 1 (1-ое марта).Обе эти функции нуждаются
в одной и той же информацион-ной таблице, указывающей число дней в каждом
месяце. Так какчисло дней в месяце в високосном и в невисокосном году отли-чается,
то проще представить их в виде двух строк двумерногомассива, чем пытаться
прослеживать во время вычислений, чтоименно происходит в феврале. Вот этот массив
и выполняющиеэти преобразования функции: STATIC INT DAY_TAB[2][13] = {(0, 31,
28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31),(0, 31, 29, 31, 30, 31, 30, 31, 31,
30, 31, 30, 31) }; * 113 - DAY_OF_YEAR(YEAR, MONTH, DAY) /* SET DAY OF YEAR
*/ INT YEAR, MONTH, DAY; /* FROM MONTH DAY */ {INT I, LEAP;LEAP = YEAR%4 == 0 &YEAR%100
!= 0 \!\! YEAR%400 == 0;FOR (I = 1; I DAY_TAB[LEAP][I]; I++)YEARDAY -=
DAY_TAB[LEAP][I];*PMONTH = I;*PDAY = YEARDAY; } Массив DAY_TAB должен быть внешним
как для DAY_OF_YEAR, таки для MONTH_DAY, поскольку он используется обеими
этими фун-кциями.Массив DAY_TAB является первым двумерным массивом, с ко-торым
мы имеем дело. По определению в “C” двумерный массивпо существу является одномерным
массивом, каждый элемент ко-торого является массивом. Поэтому индексы записываются
как DAY_TAB[I][J]а неDAY_TAB [I, J] как в большинстве языков. В остальном
с двумерными массивамиможно в основном обращаться таким же образом, как в другихязыках.
Элементы хранятся по строкам, т.е. При обращении кэлементам в порядке
их размещения в памяти быстрее всего из-меняется самый правый индекс.Массив
инициализируется с помощью списка начальных зна-чений, заключенных в фигурные
скобки; каждая строка двумер-ного массива инициализируется соответствующим подсписком.
Мыпоместили в начало массива DAY_TAB столбец из нулей для то-го, чтобы
номера месяцев изменялись естественным образом от1 до 12, а не от 0 до 11. Так
как за экономию памяти у нас пока не награждают, такой способ проще, чем подгонка
индек-сов.Если двумерный массив передается функции, то описаниесоответствующего
аргумента функции должно содержать количес-тво столбцов; количество строк несущественно,
поскольку, каки прежде, фактически передается указатель. В нашем
конкрет-ном случае это указатель объектов, являющихся массивами из * 114 - 13 чисел
типа INT. Таким образом, если бы требовалось пере-дать массив DAY_TAB функции
F, то описание в F имело бы вид: F(DAY_TAB)INT DAY_TAB[2][13]; { ... } Так
как количество строк является несущественным, то описа-ние аргумента в F могло
бы быть таким: INT DAY_TAB[][13];или такимINT (*DAY_TAB)[13];в которм говорится,
что аргумент является указателем массиваиз 13 целых. Круглые скобки здесь необходимы,
потому чтоквадратные скобки [] имеют более высокий уровень старшинст-ва,
чем *; как мы увидим в следующем разделе, без круглыхскобок INT *DAY_TAB[13];является
описанием массива из 13 указателей на целые.5.8. Массивы указателей;
указатели указателейТак как указатели сами являются переменными, то вы впол-не
могли бы ожидать использования массива указателей. Этодействительно так. Мы проиллюстрируем
это написанием прог-раммы сортировки в алфавитном порядке набора текстовыхстрок,
предельно упрощенного варианта утилиты SORT операци-онной систем
UNIX.В главе 3 мы привели функцию сортировки по шеллу, кото-рая упорядочивала
массив целых. Этот же алгоритм будет рабо-тать и здесь, хотя теперь мы будем иметь
дело со строчкамитекста различной длины, которые, в отличие от целых, нельзясравнивать
или перемещать с помощью одной операции. Мы нуж-даемся в таком представлении
данных, которое бы позволялоудобно и эффективно обрабатывать строки текста
переменнойдлины.Здесь и возникают массивы указателей. Если подлежащиесортировке
сроки хранятся одна за другой в длинном символь-ном массиве (управляемом,
например, функцией ALLOC), то ккаждой строке можно обратиться с помощью указателя
на еепервый символ. Сами указатели можно хранить в массиве. двестроки можно
сравнить, передав их указатели функции STRCMP. * 115 - Если две расположенные
в неправильном порядке строки должныбыть переставлены, то фактически переставляются
указатели вмассиве указателей, а не сами тексты строк. Этим исключаютсясразу
две связанные проблемы: сложного управления памятью ибольших дополнительных
затрат на фактическую перестановкустрок.Процесс сортировки включает три шага:чтение
всех строк вводаих сортировкавывод их в правильном порядке Как обычно, лучше
разделить программу на несколько функций всоответствии с естественным делением
задачи и выделить веду-щую функцию, управляющую работой всей программы.Давайте
отложим на некоторое время рассмотрение шага сорти-ровки и сосредоточимся на
структуре данных и вводе-выводе.Функция, осуществляющая ввод, должна извлечь
символы каждойстроки, запомнить их и построить массив указателей строк.Она должна
также подсчитать число строк во вводе, так какэта информация необходима при
сортировке и выводе. так какфункция ввода в состоянии справиться только с конечным
чис-лом вводимых строк, в случае слишком большого их числа онаможет возвращать
некоторое число, отличное от возможногочисла строк, например -1. Функция осуществляющая
вывод, дол-жна печатать строки в том порядке, в каком они появляются
вмассиве указателей. #DEFINE NULL 0#DEFINE LINES 100 /* MAX LINES TO BE SORTED
*/ MAIN() /* SORT INPUT LINES */ \(CHAR *LINEPTR[LINES]; /*POINTERS TO TEXT
LINES */INT NLINES; /* NUMBER OF INPUT LINES READ */IF ((NLINES = READLINES(LINEPTR,
LINES)) >= 0) \( SORT(LINEPTR, NLINES);WRITELINES(LINEPTR, NLINES); \)ELSEPRINTF(“INPUT
TOO BIG TO SORT\N”); \) #DEFINE MAXLEN 1000* 116 - READLINES(LINEPTR,
MAXLINES) /* READ INPUT LINES */CHAR LINEPTR[]; / FOR SORTING */INT MAXLINES;
\(INT LEN, NLINES;CHAR *P, *ALLOC(), LINE[MAXLEN];NLINES = 0;WHILE ((LEN =
GETLINE(LINE, MAXLEN)) > 0)IF (NLINES >= MAXLINES)RETURN(-1);ELSE IF ((P = ALLOC(LEN))
== NULL)RETURN (-1);ELSE \(LINE[LEN-1] = '\0'; /* ZAP NEWLINE */STRCPY(P,LINE);LINEPTR[NLINES++]
= P; \)RETURN(NLINES); \) Символ новой строки в конце
каждой строки удаляется, так чтоон никак не будет влиять на порядок, в котором
сортируютсястроки. WRITELINES(LINEPTR, NLINES) /* WRITE OUTPUT LINES */CHAR *LINEPTR[];INT
NLINES; \(INT I;FOR (I = 0; I = 0)PRINTF(“%S\N”, *LINEPTR++); \)
здесь *LINEPTR сначала указывает на первую строку; каждоеувеличение передвигает
указатель на следующую строку, в товремя как NLINES убывает до нуля.Справившись
с вводом и выводом, мы можем перейти к сор-тировке. программа сортировки по шеллу
из главы 3 требуеточень небольших изменений: должны быть модифицированы описа-ния,
а операция сравнения выделена в отдельную функцию. Ос-новной алгоритм
остается тем же самым, и это дает нам опре-деленную уверенность, что он по-прежнему
будет работать. SORT(V, N) /* SORT STRINGS V[0] ... V[N-1] */ CHAR V[]; /
INTO INCREASING ORDER */INT N; \(INT GAP, I, J;CHAR *TEMP;FOR (GAP = N/2; GAP >
0; GAP /= 2)FOR (I = GAP; I = 0; J -= GAP) \(IF (STRCMP(V[J], V[J+GAP]) 12) ?
NAME[0] : NAME[N]); \) * 119 - Описание массива указателей на символы NAME точно
такое же,как аналогичное описание LINEPTR в примере с сортировкой.Инициализатором
является просто список символьных строк;каждая строка присваивается соответствующей
позиции в масси-ве. Более точно, символы I-ой строки помещаются в какое-тоиное
место, а ее указатель хранится в NAME[I]. Посколькуразмер массива NAME
не указан, компилятор сам подсчитываетколичество инициализаторов и соответственно
устанавливаетправильное число. 5.10. Указатели и многомерные массивыНачинающие
изучать язык “с” иногда становятся в тупикперед вопросом о различии между
двумерным массивом и масси-вом указателей, таким как NAME в приведенном выше примере.Если
имеются описания INT A[10][10];INT *B[10];то A и B можно использовать
сходным образом в том смысле,что как A[5][5], так и B[5][5] являются законными
ссылкамина отдельное число типа INT. Но A - настоящий массив: поднего отводится
100 ячеек памяти и для нахождения любого ука-занного элемента проводятся обычные
вычисления с прямоуголь-ными индексами. Для B, однако, описание выделяет только
10указателей; каждый указатель должен быть установлен так,чтобы он указывал
на массив целых. если предположить, чтокаждый из них указывает на массив из
10 элементов, то тогдагде-то будет отведено 100 ячеек памяти плюс еще десять ячеекдля
указателей. Таким образом, массив указателей используетнесколько больший
объем памяти и может требовать наличие яв-ного шага инициализации. Но при этом
возникают два преиму-щества: доступ к элементу осуществляется косвенно через ука-затель,
а не посредством умножения и сложения, и строки мас-сива могут иметь
различные длины. Это означает, что каждыйэлемент B не должен обязательно указывать
на вектор из 10элементов; некоторые могут указывать на вектор из двух эле-ментов,
другие - из двадцати, а третьи могут вообще ни начто не указывать.Хотя
мы вели это обсуждение в терминах целых, несомнен-но, чаще всего массивы указателей
используются так, как мыпродемонстрировали на функции MONTH_NAME, - для хранениясимвольных
строк различной длины.Упражнение 5-6.Перепишите функции DAY_OF_YEAR
и MONTH_DAY, используявместо индексации указатели.* 120 - 5.11. Командная
строка аргументов Системные средства, на которые опирается реализация язы-ка “с”,
позволяют передавать командную строку аргументов илипараметров начинающей выполняться
программе. Когда функцияMAIN вызывается к исполнению, она вызывается
с двумя аргу-ментами. Первый аргумент (условно называемый ARGC) указываетчисло
аргументов в командной строке, с которыми происходитобращение к программе; второй
аргумент (ARGV) является ука-зателем на массив символьных строк, содержащих
эти аргумен-ты, по одному в строке. Работа с такими строками - это обыч-ное использование
многоуровневых указателей.Самую простую иллюстрацию этой возможности
и необходимыхпри этом описаний дает программа ECHO, которая просто печа-тает
в одну строку аргументы командной строки, разделяя ихпробелами. Таким образом,
если дана команда ECHO HELLO, WORLDто выходом будетHELLO, WORLDпо соглашению ARGV[0]
является именем, по которому вызывает-ся программа, так что ARGC по меньшей
мере равен 1. В приве-денном выше примере ARGC равен 3, а ARGV[0], ARGV[1] иARGV[2]
равны соответственно “ECHO”, “HELLO,” и “WORLD”.Первым фактическим агументом
является ARGV[1], а последним -ARGV[ARGC-1]. Если ARGC равен 1, то за именем
программы неследует никакой командной строки аргументов. Все это показа-но
в ECHO: MAIN(ARGC, ARGV) /* ECHO ARGUMENTS; 1ST VERSION */INT ARGC;CHAR *ARGV[];
\(INT I;FOR (I = 1; I 0)PRINTF(“%S%C”,*++ARGV, (ARGC > 1) ? ' ' : '\N'); \)
* 121 - Так как ARGV является указателем на начало массива строк-ар-гументов, то,
увеличив его на 1 (++ARGV), мы вынуждаем егоуказывать на подлинный аргумент ARGV[1],
а не на ARGV[0].Каждое последующее увеличение передвигает его на следующийаргумент;
при этом *ARGV становится указателем на этот аргу-мент. одновременно
величина ARGC уменьшается; когда она об-ратится в нуль, все аргументы будут
уже напечатаны.Другой вариант:MAIN(ARGC, ARGV) /* ECHO ARGUMENTS; 3RD VERSION */
INT ARGC;CHAR *ARGV[]; \(WHILE (--ARGC > 0)PRINTF((ARGC > 1) ? “%S” : “%S\N”,
*++ARGV); \) Эта версия показывает, что аргумент формата функции PRINTFможет быть
выражением, точно так же, как и любой другой. Та-кое использование встречается
не очень часто, но его все жестоит запомнить.Как второй пример, давайте внесем
некоторые усовершенст-вования в программу отыскания заданной комбинации символовиз
главы 4. Если вы помните, мы поместили искомую комбинациюглубоко внутрь
программы, что очевидно является совершеннонеудовлетворительным. Следуя утилите
GREP системы UNIX, да-вайте изменим программу так, чтобы эта комбинация указыва-лась
в качестве первого аргумента строки. #DEFINE MAXLINE 1000MAIN(ARGC, ARGV)
/* FIND PATTERN FROM FIRST ARGUMENT */ INT ARGC;CHAR *ARGV[]; \(CHAR LINE[MAXLINE];IF
(ARGC != 2)PRINTF (“USAGE: FIND PATTERN\N”);ELSEWHILE (GETLINE(LINE,
MAXLINE) > 0)IF (INDEX(LINE, ARGV[1] >= 0)PRINTF(“%S”, LINE); \) Теперь может быть
развита основная модель, иллюстрирую-щая дальнейшее использование указателей.
Предположим, чтонам надо предусмотреть два необязательных аргумента. Одинутверждает:
“напечатать все строки за исключением тех, кото-рые содержат данную комбинацию”,
второй гласит: “перед каж-дой выводимой строкой должен печататься ее
номер”. * 122 - Общепринятым соглашением в “с”-программах является то,что аргумент,
начинающийся со знака минус, вводит необяза-тельный признак или параметр.
Если мы, для того, чтобы сооб-щить об инверсии, выберем -X, а для указания о нумерациинужных
строк выберем -N(“номер”), то команда FIND -X -N THEпри входных
данныхNOW IS THE TIMEFOR ALL GOOD MENTO COME TO THE AIDOF THEIR PARTY. Должна выдать2:FOR
ALL GOOD MENНужно, чтобы необязательные аргументы могли располагать-ся
в произвольном порядке, и чтобы остальная часть программыне зависела от количества
фактически присутствующих аргумен-тов. в частности, вызов функции INDEX
не должен содержатьссылку на ARGV[2], когда присутствует один необязательныйаргумент,
и на ARGV[1], когда его нет. Более того, для поль-зователей удобно, чтобы
необязательные аргументы можно былообъединить в виде: FIND -NX THEвот сама программа:
#DEFINE MAXLINE 1000MAIN(ARGC, ARGV) /* FIND PATTERN FROM FIRST ARGUMENT
*/ INT ARGC;CHAR *ARGV[]; \(CHAR LINE[MAXLINE], *S;LONG LINENO = 0;INT EXCEPT
= 0, NUMBER = 0;WHILE (--ARGC > 0 &(*++ARGV)[0] == '-')FOR (S = ARGV[0]+1; *S
!= '\0'; S++)SWITCH (*S) \(CASE 'X':EXCEPT = 1;BREAK;* 123 - CASE 'N':NUMBER =
1;BREAK;DEFAULT:PRINTF(“FIND: ILLEGAL OPTION %C\N”, *S);ARGC = 0;BREAK; \)IF (ARGC
!= 1)PRINTF(“USAGE: FIND -X -N PATTERN\N”);ELSEWHILE (GETLINе(LINE, MAXLINE)
> 0) \(LINENO++;IF ((INDEX(LINE, *ARGV) >= 0) != EXCEPT) \IF (NUMBER)PRINTF(“%LD:
“, LINENO);PRINTF(“%S”, LINE); \) \) \) Аргумент ARGV увеличивается перед
каждым необязательнымаргументом, в то время как аргумент ARGC уменьшается. еслинет
ошибок, то в конце цикла величина ARGC должна равняться1, а *ARGV должно указывать
на заданную комбинацию. Обратитевнимание на то, что *++ARGV является указателем
аргументнойстроки; (*++ARGV)[0] - ее первый символ. Круглые скобкиздесь
необходимы, потому что без них выражение бы принялосовершенно отличный (и неправильный)
вид *++(ARGV[0]). Дру-гой правильной формой была бы **++ARGV.Упражнение
5-7.Напишите программу ADD, вычисляющую обратное польскоевыражение из командной
строки. Например,ADD 2 3 4 + *вычисляет 2*(3+4). Упражнение 5-8.Модифицируйте
программы ENTAB и DETAB (указанные в ка-честве упражнений в главе 1) так,
чтобы они получали списоктабуляционных остановок в качестве аргументов. Если аргумен-ты
отсутствуют, используйте стандартную установку табуляций.Упражнение 5-9.Расширьте
ENTAB и DETAB таким образом, чтобы они воспри-нимали сокращенную нотациюENTAB
M +N* 124 - означающую табуляционные остановки через каждые N столбцов,начиная
со столбца M. Выберите удобное (для пользователя)поведение функции по
умолчанию.Упражнение 5-10.Напишите программу для функции TAIL, печатающей послед-ние
N строк из своего файла ввода. Пусть по умолчанию N рав-но 10, но это число
может быть изменено с помощью необяза-тельного аргумента, так что TAIL -Nпечатает
последние N строк. программа должна действовать ра-ционально, какими бы
неразумными ни были бы ввод или значе-ние N. Составьте программу так, чтобы она
оптимальным обра-зом использовала доступную память: строки должны храниться,как
в функции SORT, а не в двумерном массиве фиксированногоразмера. 5.12. Указатели
на функцииВ языке “с” сами функции не являются переменными, ноимеется возможность
определить указатель на функцию, которыйможно обрабатывать, передавать другим
функциям, помещать вмассивы и т.д. Мы проиллюстрируем это, проведя модификациюнаписанной
ранее программы сортировки так, чтобы при заданиинеобязательного
аргумента -N она бы сортировала строки вводачисленно, а не лексикографически.Сортировка
часто состоит из трех частей - сравнения, ко-торое определяет упорядочивание
любой пары объектов, перес-тановки, изменяющей их порядок, и алгоритма
сортировки, осу-ществляющего сравнения и перестановки до тех пор, покаобъекты
не расположатся в нужном порядке. Алгоритм сортиров-ки не зависит от операций сравнения
и перестановки, так что,передавая в него различные функции сравнения и
перестановки,мы можем организовать сортировку по различным критериям.Именно такой
подход используется в нашей новой программесортировки.Как и прежде, лексикографическое
сравнение двух строкосуществляется функцией STRCMP, а перестановка
функциейSWAP; нам нужна еще функция NUMCMP, сравнивающая две строкина основе численного
значения и возвращающая условное указа-ние того же вида, что и STRCMP.
Эти три функции описываютсяв MAIN и указатели на них передаются в SORT. В свою
очередьфункция SORT обращается к этим функциям через их указатели.мы урезали обработку
ошибок в аргументах с тем, чтобы сосре-доточиться на главных вопросах.
* 125 - #DEFINE LINES 100 /* MAX NUMBER OF LINESTO BE SORTED */MAIN(ARGC, ARGV)
/* SORT INPUT LINES */ INT ARGC;CHAR *ARGV[]; \(CHAR LINEPTR[LINES]; / POINTERS
TO TEXT LINES */INT NLINES; /* NUMBER OF INPUT LINES READ */INT STRCMP(), NUMCMP();
/* COMPARSION FUNCTIONS */INT SWAP(); /* EXCHANGE FUNCTION */INT NUMERIC
= 0; /* 1 IF NUMERIC SORT */ IF(ARGC>1 &ARGV[1][0] == '-' &ARGV[1][1]=='N')NUMERIC
= 1;IF(NLINES = READLINES(LINEPTR, LINES)) >= 0) \(IF (NUMERIC)SORT(LINEPTR,
NLINES, NUMCMP, SWAP);ELSESORT(LINEPTR, NLINES, STRCMP, SWAP);WRITELINES(LINEPTR,
NLINES);\) ELSEPRINTF(“INPUT TOO BIG TO SORT\N”); \) Здесь STRCMP, NIMCMP
и SWAP - адреса функций; так как извес-тно, что это функции, операция здесь не
нужна совершенноаналогично тому, как она не нужна и перед именем массива.Передача
адресов функций организуется компилятором.Второй шаг состоит в модификации
SORT:SORT(V, N, COMP, EXCH) /* SORT STRINGS V[0] ... V[N-1] */ CHAR V[]; / INTO
INCREASING ORDER */ INT N;INT (*COMP)(), (*EXCH)(); \(INT GAP, I, J;FOR(GAP =
N/2; GAP > 0; GAP /= 2)FOR(I = GAP; I = 0; J -= GAP) \(IF((*COMP)(V[J], V[J+GAP])
V2)RETURN(1);ELSERETURN (0); \) Заключительный шаг состоит в добавлении функции
SWAP,переставляющей два указателя. Это легко сделать, непосредст-венно используя
то, что мы изложили ранее в этой главе.* 127 - SWAP(PX, PY) /* INTERCHANGE
*PX AND *PY */CHAR *PX[], *PY[]; \(CHAR *TEMP;TEMP = *PX;*PX = *PY;*PY = TEMP;
\)Имеется множество других необязятельных аргументов, ко-торые могут быть включены
в программу сортировки: некоторыеиз них составляют интересные упражнения.Упражнение
5-11.Модифицируйте SORT таким образом, чтобы она работала сметкой -R,
указывающей на сортировку в обратном (убывающем)порядке. Конечно, -R должна
работать с -N.Упражнение 5-12.Добавьте необязательный аргумент -F, объединяющий
вместепрописные и строчные буквы, так чтобы различие регистров неучитывалось во
время сортировки: данные из верхнего и нижне-го регистров сортируются вместе,
так что буква 'а' прописноеи 'а' строчное оказываются соседними , а не разделенными
це-лым алфавитом.Упражнение 5-13.Добавьте необязательный аргумент -D (“словарное
упорядо-чивание”), при наличии которого сравниваются только буквы,числа
и пробелы. Позаботьтесь о том, чтобы эта функция рабо-тала и вместе с -F.Упражнение
5-14.Добавьте возможность обработки полей, так чтобы можнобыло сортировать
поля внутри строк. Каждое поле должно сор-тироваться в соответствии с независимым
набором необязатель-ных аргументов. (предметный указатель этой книги сортировал-ся
с помощью аргументов -DF для категории указателя и с -Nдля номеров страниц).*
128 - 6. Структуры.Структура - это набор из одной или более переменных,возможно
различных типов, сгруппированных под одним именемдля удобства обработки.
(В некоторых языках, самый известныйиз которых паскаль, структуры называются
“записями”).Традиционным примером структуры является учетная карточ-ка работающего:
“служащий” описывается набором атрибутов та-ких, как фамилия, имя, отчество
(ф.и.о.), адрес, код соци-ального обеспечения, зарплата и т.д. Некоторые из
этих атри-бутов сами могут оказаться структурами: ф.и.о. Имеет нес-колько компонент,
как и адрес, и даже зарплата.Структуры оказываются полезными при организации
сложныхданных особенно в больших программах, поскольку во многихситуациях они
позволяют сгруппировать связанные данные такимобразом, что с ними можно обращаться,
как с одним целым, ане как с отдельными объектами. В этой главе мы постараемсяпродемонстрировать
то, как используются структуры. Програм-мы, которые мы
для этого будем использовать, больше, чеммногие другие в этой книге, но все же
достаточно умеренныхразмеров. 6.1. Основные сведения.Давайте снова обратимся
к процедурам преобразования датыиз главы 5. Дата состоит из нескольких частей таких,
какдень, месяц, и год, и, возможно, день года и имя месяца. Этипять переменных
можно объеденить в одну структуру вида: STRUCT DATE \(INT DAY;INT MONTH;INT
YEAR;INT YEARDAY;CHAR MON_NAME[4]; \); Описание структуры, состоящее из заключенного
в фигурныескобки списка описаний, начинается с ключевого слова STRUCT.За
словом STRUCT может следовать необязательное имя, называ-емое ярлыком структуры
(здесь это DATе). Такой ярлык именуетструктуры этого вида и может использоваться
в дальнейшем каксокращенная запись подробного описания.Элементы или переменные,
упомянутые в структуре, называ-ются членами. Ярлыки и члены структур могут
иметь такие жеимена, что и обычные переменные (т.е. Не являющиеся членамиструктур),
поскольку их имена всегда можно различить по кон-тексту. Конечно, обычно
одинаковые имена присваивают толькотесно связанным объектам. * 129 - Точно так
же, как в случае любого другого базисного ти-па, за правой фигурной скобкой,
закрывающей список членов,может следовать список переменных.Оператор STRUCT \(
...\) X,Y,Z;синтаксически аналогиченINT X,Y,Z;в том смысле, что каждый из операторов
описывает X , Y и Z вкачестве переменных соотвествующих типов и приводит
к выде-лению для них памяти.Описание структуры, за которым не следует списка пере-менных,
не приводит к выделению какой-либо памяти; оно толь-ко определяет шаблон
или форму структуры. Однако, если такоеописание снабжено ярлыком, то этот
ярлык может быть исполь-зован позднее при определении фактических экземпляров струк-тур.
Например, если дано приведенное выше описание DATE, то STRUCT DATE D;определяет
переменную D в качестве структуры типа DATE.Внешнюю или статическую
структуру можно инициализировать,поместив вслед за ее определением список инициализаторов
дляее компонент: STRUCT DATE D=\( 4, 7, 1776, 186, “JUL”\);Член определенной
структуры может быть указан в выраже-нии с помощью конструкции видаимя
структуры . ЧленОперация указания члена структуры “.” связывает имя структу-ры
и имя члена. В качестве примера определим LEAP (признаквисокосности года) на основе
даты, находящейся в структуреD, LEAP = D.YEAR % 4 == 0 &D.YEAR % 100 != 0\!\!
D.YEAR % 400 == 0;или проверим имя месяцаIF (STRCMP(D.MON_NAME, “AUG”) ==
0) ...Или преобразуем первый символ имени месяца так, чтобы ононачиналось со строчной
буквы D.MON_NAME[0] = LOWER(D.MON_NAME[0]);* 130 - Структуры могут быть
вложенными; учетная карточка служа-щего может фактически выглядеть так:STRUCT
PERSON \(CHAR NAME[NAMESIZE];CHAR ADDRESS[ADRSIZE];LONG ZIPCODE; /* почтовый индекс
*/LONG SS_NUMBER; /* код соц. Обеспечения */DOUBLE SALARY; /* зарплата */STRUCT
DATE BIRTHDATE; /* дата рождения */STRUCT DATE HIREDATE; /* дата поступленияна
работу */ \); Структура PERSON содержит две структуры типа DATE . Если мыопределим
EMP как STRUCT PERSON EMP;тоEMP.BIRTHDATE.MONTHбудет ссылаться на месяц
рождения. Операция указания членаструктуры “.” ассоциируется слева направо.
6.2. Структуры и функции.В языке “C” существует ряд ограничений на использованиеструктур.
Обязательные правила заключаются в том, что единс-твенные операции, которые
вы можете проводить со структура-ми, состоят в определении ее адреса с помощью
операции идоступе к одному из ее членов. Это влечет за собой то, чтоструктуры
нельзя присваивать или копировать как целое, и чтоони не могут быть переданы
функциям или возвращены ими. (Впоследующих версиях эти ограничения будут сняты).
На указа-тели структур эти ограничения однако не накладываются, такчто структуры
и функции все же могут с удобством работатьсовместно. И наконец, автоматические
структуры, как и авто-матические массивы, не могут быть инициализированы;
инициа-лизация возможна только в случае внешних или статическихструктур.Давайте
разберем некоторые из этих вопросов, переписав сэтой целью функции перобразования
даты из предыдущей главытак, чтобы они использовали структуры. Так как
правила зап-рещают непосредственную передачу структуры функции, то мыдолжны либо
передавать отдельно компоненты, либо передатьуказатель всей структуры. Первая
возможность демонстрируетсяна примере функции DAY_OF_YEAR, как мы ее написали
в главе5: D.YEARDAY = DAY_OF_YEAR(D.YEAR, D.MONTH, D.DAY);* 131 - другой способ
состоит в передаче указателя. если мы опишемHIREDATE как STRUCT DATE HIREDATE;и
перепишем DAY_OF_YEAR нужным образом, мы сможем тогда на-писатьHIREDATE YEARDAY
= DAY_OF_YEAR(HIREDATE)передавая указатель на HIREDATE функции DAY_OF_YEAR .
Функ-ция должна быть модифицирована, потому что ее аргумент те-перь является указателем,
а не списком переменных.DAY_OF_YEAR(PD) /* SET DAY OF YEAR FROM MONTH,
DAY */ STRUCT DATE *PD; \(INT I, DAY, LEAP;DAY = PD->DAY;LEAP = PD->YEAR % 4
== 0 &PD->YEAR % 100 != 0\!\! PD->YEAR % 400 == 0;FOR (I =1; I MONTH; I++)DAY
+= DAY_TAB[LEAP][I];RETURN(DAY); \) ОписаниеSTRUCT DATE *PD;говорит, что PD является
указателем структуры типа DATE.Запись, показанная на примере PD->YEARявляется
новой. Если P - указатель на структуру, тоP-> член структурыобращается к конкретному
члену. (Операция -> - это знак ми-нус, за которым следует знак “>”.)Так
как PD указывает на структуру, то к члену YEAR можнообратиться и следующим
образом(*PD).YEARно указатели структур используются настолько часто, что за-пись
-> оказывается удобным сокращением. Круглые скобки в(*PD).YEAR необходимы, потому
что операция указания члена * 132 - стуктуры старше , чем * . Обе операции,
“->” и “.”, ассоции-руются слева направо, так что конструкции слева и справазквивалентны
P->Q->MEMB (P->Q)->MEMB EMP.BIRTHDATE.MONTH (EMP.BIRTHDATE).MONTH
Для полноты ниже приводится другая функция, MONTH_DAY, пере-писанная с использованием
структур.MONTH_DAY(PD) /* SET MONTH AND DAY FROM DAY OF YEAR */ STRUCT DATE
*PD; \(INT I, LEAP;LEAP = PD->YEAR % 4 == 0 &PD->YEAR % 100 != 0\!\! PD->YEAR
% 400 == 0;PD->DAY = PD->YEARDAY;FOR (I = 1; PD->DAY > DAY_TAB[LEAP][I]; I++)PD->DAY
-= DAY_TAB[LEAP][I];PD->MONTH = I; \) Операции работы со структурами “->”
и “.” наряду со ()для списка аргументов и [] для индексов находятся на самомверху
иерархии страшинства операций и, следовательно, связы-ваются очень крепко.
Если, например, имеется описание STRUCT \(INT X;INT *Y;\) *P;то выражение++P->Xувеличивает
х, а не р, так как оно эквивалентно выражению++(P->х). Для изменения
порядка выполнения операций можноиспользовать круглые скобки: (++P)->х увеличивает
P до дос-тупа к х, а (P++)->X увеличивает P после. (круглые скобки впоследнем
случае необязательны. Почему ?)Совершенно аналогично *P->Y извлекает то,
на что указы-вает Y; *P->Y++ увеличивает Y после обработки того, на чтоон указывает
(точно так же, как и *S++); (*P->Y)++ увеличи-вает то, на что указывает Y;
*P++->Y увеличивает P после вы-борки того, на что указывает Y. * 133 - 6.3. Массивы
сруктур.Структуры особенно подходят для управления массивамисвязанных переменных.
Рассмотрим, например, программу подс-чета числа вхождений каждого ключевого
слова языка “C”. Намнужен массив символьных строк для хранения имен и массив
це-лых для подсчета. одна из возможностей состоит в использова-нии двух параллельных
массивов KEYWORD и KEYCOUNT: CHAR *KEYWORD [NKEYS];INT KEYCOUNT [NKEYS];Но
сам факт, что массивы параллельны, указывает на возмож-ность другой организации.
Каждое ключевое слово здесь по су-ществу является парой: CHAR *KEYWORD;INT
KEYCOUNT;и, следовательно, имеется массив пар. Описание структурыSTRUCT KEY
\(CHAR *KEYWORD;INT KEYCOUNT;\) KEYTAB [NKEYS];оперделяет массив KEYTAB структур
такого типа и отводит дляних память. Каждый элемент массива является структурой.
Этоможно было бы записать и так: STRUCT KEY \(CHAR *KEYWORD;INT KEYCOUNT;
\);STRUCT KEY KEYTAB [NKEYS];Так как структура KEYTAB фактически содержит постоянныйнабор
имен, то легче всего инициализировать ее один раз идля всех членов
при определении. Инициализация структурвполне аналогична предыдущим инициализациям
- за определени-ем следует заключенный в фигурные скобки список инициализа-торов:
STRUCT KEY \(CHAR *KEYWORD;INT KEYCOUNT;\) KEYTAB[] =\(“BREAK”, 0,“CASE”,
0,“CHAR”, 0,“CONTINUE”, 0,“DEFAULT”, 0, /* ... */“UNSIGNED”, 0,“WHILE”, 0 \);
Инициализаторы перечисляются парами соответственно членамструктуры. Было бы более
точно заключать в фигурные скобкиинициализаторы для каждой “строки” или структуры
следующимобразом: \( “BREAK”, 0 \),\( “CASE”, 0 \), . . . * 134 - Но когда
инициализаторы являются простыми переменными илисимвольными строками и все они
присутствуют, то во внутрен-них фигурных скобках нет необходимости. Как обычно,
компиля-тор сам вычислит число элементов массива KEYTAB, если иници-ализаторы
присутствуют, а скобки [] оставлены пустыми.Программа подсчета ключевых слов
начинается с определе-ния массива KEYTAB. ведущая программа читает свой файл вво-да,
последовательно обращаясь к функции GETWORD, которая из-влекает из ввода
по одному слову за обращение. Каждое словоищется в массиве KEYTAB с помощью варианта
функции бинарногопоиска, написанной нами в главе 3. (Конечно, чтобы эта
функ-ция работала, список ключевых слов должен быть расположен впорядке возрастания).
#DEFINE MAXWORD 20 MAIN() /* COUNT “C” KEYWORDS */ \(INT N, T;CHAR WORD[MAXWORD];WHILE
((T = GETWORD(WORD,MAXWORD)) != EOF)IF (T == LETTER)IF((N = BINARY(WORD,KEYTAB,NKEYS))
>= 0)KEYTAB[N].KEYCOUNT++;FOR (N =0; N 0)PRINTF(“%4D %S\N”,KEYTAB[N].KEYCOUNT,
KEYTAB[N].KEYWORD); \)BINARY(WORD, TAB, N) /* FIND WORD
IN TAB[0]...TAB[N-1] */CHAR *WORD;STRUCT KEY TAB[];INT N; \(INT LOW, HIGH, MID,
COND;LOW = 0;HIGH = N - 1;WHILE (LOW 0)LOW = MID + 1;ELSERETURN (MID); \)RETURN(-1);
\)Мы вскоре приведем функцию GETWORD; пока достаточно сказать,что она возвращает
LETTER каждый раз, как она находит слово,и копирует это слово в свой первый
аргумент.* 135 - Величина NKEYS - это количество ключевых слов в массивеKEYTAB
. Хотя мы можем сосчитать это число вручную, гораздолегче и надежнее поручить
это машине, особенно в том случае,если список ключевых слов подвержен изменениям.
Одной извозможностей было бы закончить список инициализаторов указа-нием
на нуль и затем пройти в цикле сквозь массив KEYTAB,пока не найдется конец.Но,
поскольку размер этого массива полностью определен кмоменту компиляции, здесь
имеется более простая возможность.Число элементов просто есть SIZE OF KEYTAB /
SIZE OF STRUCT KEYдело в том, что в языке “C” предусмотрена унарная операцияSIZEOF,
выполняемая во время компиляции, которая позволяетвычислить размер любого
объекта. Выражение SIZEOF(OBJECT)выдает целое, равное размеру указанного объекта.
(Размер оп-ределяется в неспецифицированных единицах, называемых “бай-тами”,
которые имеют тот же размер, что и переменные типаCHAR). Объект может быть фактической
переменной, массивом иструктурой, или именем основного типа, как INT
или DOUBLE,или именем производного типа, как структура. В нашем случаечисло ключевых
слов равно размеру массива, деленному на раз-мер одного элемента массива.
Это вычисление используется вутверждении #DEFINE для установления значения NKEYS:
#DEFINE NKEYS (SIZEOF(KEYTAB) / SIZEOF(STRUCT KEY))Теперь перейдем к функции
GETWORD. Мы фактически написа-ли более общий вариант функции GETWORD, чем необходимо
дляэтой программы, но он не на много более сложен. ФункцияGETWORD возвращает
следующее “слово” из ввода, где словомсчитается либо строка букв и цифр,
начинающихся с буквы, ли-бо отдельный символ. Тип объекта возвращается в качетве
зна-чения функции; это - LETTER, если найдено слово, EOF дляконца файла и сам
символ, если он не буквенный. GETWORD(W, LIM) /* GET NEXT WORD FROM INPUT */CHAR
*W;INT LIM; \(INT C, T;IF (TYPE(C=*W++=GETCH()) !=LETTER) \(*W='\0';RETURN©;
\) * 136 - WHILE (--LIM > 0) \(T = TYPE(C = *W++ = GETCH());IF (T ! = LETTER &T
! = DIGIT) \(UNGETCH©;BREAK; \) \)*(W-1) - '\0';RETURN(LETTER); \) Функция GETWORD
использует функции GETCH и UNGETCH, которыемы написали в главе 4: когда набор
алфавитных символов пре-рывается, функция GETWORD получает один лишний символ.
В ре-зультате вызова UNGETCH этот символ помещается назад во вводдля следующего
обращения.Функция GETWORD обращается к функции TYPE для определе-ния типа
каждого отдельного символа из файла ввода. Вот ва-риант, справедливый только для
алфавита ASCII.TYPE© /* RETURN TYPE OF ASCII CHARACTER */ INT C; \(IF (C>= 'A'
&C= 'A' &C= '0' &CKEYCOUNT++;FOR (P=KEYTAB; P>KEYTAB + NKEYS; P++)IF (P->KEYCOUNT
> 0)PRINTF(“%4D %S/N”, P->KEYCOUNT, P->KEYWORD); \)STRUCT KEY BINARY(WORD,
TAB, N) / FIND WORD */CHAR WORD / IN TAB[0]...TAB[N-1] */STRUCT KEY TAB [];INT
N; \(INT COND;STRUCT KEY *LOW = TAB[0]STRUCT KEY *HIGH = TAB[N-1]STRUCT KEY *MID;WHILE
(LOW KEYWORD)) 0)LOW = MID + 1;ELSERETURN(MID); \)RETURN(NULL); \) Здесь
имеется несколько моментов, которые стоит отме-тить. Во-первых, описание функции
BINARI должно указывать,что она возвращает указатель на структуру типа KEY,
а не нацелое; это объявляется как в функции MAIN, так и в BINARY.Если функция
BINARI находит слово, то она возвращает указа-тель на него; если же нет, она
возвращает NULL. * 138 - Во-вторых, все обращения к элементам массива KEYTAB осу-ществляются
через указатели. Это влечет за собой одно сущес-твенное изменение
в функции BINARY: средний элемент большенельзя вычислять просто по формуле MID
= (LOW + HIGH) / 2потому что сложение двух указателей не дает какого-нибудьполезного
результата (даже после деления на 2) и в действи-тельности является незаконным.
эту формулу надо заменить на MID = LOW + (HIGH-LOW) / 2в результате которой
MID становится указателем на элемент,расположенный посередине между LOW и
HIGH.Вам также следует разобраться в инициализации LOW иHIGH. указатель можно инициализировать
адресом ранее опреде-ленного объекта; именно как мы здесь и поступили.В
функции MAIN мы написалиFOR (P=KEYTAB; P WORD = STRSAVE(W);P->COUNT =
1;P->LEFT = P->RIGHT = NULL;\) ELSE IF ((COND = STRCMP(W, P->WORD)) == 0)P->COUNT++;
/* REPEATED WORD */ELSE IF (COND LEFT = TREE(P->LEFT, W);ELSE /* GREATER
INTO RIGHT SUBTREE */P->RIGHT = TREE(P->RIGHT, W);RETURN(P); \) * 141 - Память
для нового узла выделяется функцией TALLOC, явля-ющейся адаптацией для данного
случая функции ALLOC, написан-ной нами ранее. Она возвращает указатель свободного
прост-ранства, пригодного для хранения нового узла дерева. (Мывскоре обсудим
это подробнее). Новое слово копируется функ-цией STRSAVE в скрытое место, счетчик
инициализируется еди-ницей, и указатели обоих потомков полагаются равными нулю.Эта
часть программы выполняется только при добавлении новогоузла к ребру дерева.
Мы здесь опустили проверку на ошибкивозвращаемых функций STRSAVE и TALLOC
значений (что неразум-но для практически работающей программы).Функция TREEPRINT
печатает дерево, начиная с левого под-дерева; в каждом узле сначала печатается
левое поддерево(все слова, которые младше этого слова), затем само слово, азатем
правое поддерево (все слова, которые старше). Если вынеуверенно оперируете
с рекурсией, нарисуйте дерево сами инапечатайте его с помощью функции TREEPRINT
; это одна изнаиболее ясных рекурсивных процедур, которую можно найти. TREEPRINT
(P) /* PRINT TREE P RECURSIVELY */STRUCT TNODE *P; \(IF (P != NULL) \(TREEPRINT
(P->LEFT);PRINTF(“%4D %S\N”, P->COUNT, P->WORD);TREEPRINT (P->RIGHT); \)
\) Практическое замечание: если дерево становится “несба-лансированным” из-за того,
что слова поступают не в случай-ном порядке, то время работы программы может
расти слишкомбыстро. В худшем случае, когда поступающие слова уже упоря-дочены,
настоящая программа осуществляет дорогостоящую ими-тацию линейного поиска. Существуют
различные обобщения дво-ичного дерева, особенно 2-3 деревья и AVL деревья,
которыене ведут себя так “в худших случаях”, но мы не будем здесьна них останавливаться.Прежде
чем расстаться с этим примером, уместно сделатьнебольшое
отступление в связи с вопросом о распределении па-мяти. Ясно, что в программе желательно
иметь только одинраспределитель памяти, даже если ему приходится размещатьразличные
виды объектов. Но если мы хотим использовать одинраспределитель
памяти для обработки запросов на выделениепамяти для указателей на переменные типа
CHAR и для указате-лей на STRUCT TNODE, то при этом возникают два вопроса.
Пер-вый: как выполнить то существующее на большинстве реальныхмашин ограничение,
что объекты определенных типов должныудовлетворять требованиям выравнивания (например,
часто це-лые должны размещаться в четных адресах)? Второй: как орга-низовать
описания, чтобы справиться с тем, что функция ALLOCдолжна возвращать различные
виды указателей ? * 142 - Вообще говоря, требования выравнивания легко
выполнитьза счет выделения некоторого лишнего пространства, простообеспечив то,
чтобы распределитель памяти всегда возвращалуказатель, удовлетворяющий всем ограничениям
выравнивания.Например, на PDP-11 достаточно, чтобы функция ALLOC всегдавозвращала
четный указатель, поскольку в четный адрес можнопоместить любой тип
объекта. единственный расход при этом -лишний символ при запросе на нечетную
длину. Аналогичныедействия предпринимаются на других машинах. Таким образом,реализация
ALLOC может не оказаться переносимой, но ее ис-пользование будет переносимым.
Функция ALLOC из главы 5 непредусматривает никакого определенного выравнивания;
в главе8 мы продемонстрируем, как правильно выполнить эту задачу.Вопрос
описания типа функции ALLOC является мучительнымдля любого языка, который серьезно
относится к проверке ти-пов. Лучший способ в языке “C” - объявить, что ALLOC
возвра-щает указатель на переменную типа CHAR, а затем явно преоб-разовать
этот указатель к желаемому типу с помощью операцииперевода типов. Таким образом,
если описать P в виде CHAR *P;то(STRUCT TNODE *) Pпреобразует его в выражениях
в указатель на структуру типа TNODE . Следовательно, функцию TALLOC можно записать
в виде:STRUCT TNODE *TALLOC() \(CHAR *ALLOC();RETURN ((STRUCT TNODE *) ALLOC(SIZEOF(STRUCT
TNODE))); \) это более чем достаточно для работающих в настоящее
времякомпиляторов, но это и самый безопасный путь с учетом будую-щего.Упражнение
6-4.Напишите программу, которая читает “C”-программу и печа-тает в алфавитном
порядке каждую группу имен переменных, ко-торые совпадают в первых семи символах,
но отличаются где-тодальше. (Сделайте так, чтобы 7 было параметром).Упражнение
6-5.Напишите программу выдачи перекрестных ссылок, т.е.Программу, которая
печатает список всех слов документа и длякаждого из этих слов печатает список
номеров строк, в кото-рые это слово входит.Упражнение 6-6.Напишите программу,
которая печатает слова из своегофайла ввода, расположенные в порядке убывания
частоты их по-явления. Перед каждым словом напечатайте число его появле-ний. *
143 - 6.6. Поиск в таблице.Для иллюстрации дальнейших аспектов использования струк-тур
в этом разделе мы напишем программу, представляющую со-бой содержимое пакета
поиска в таблице. Эта программа явля-ется типичным представителем подпрограмм
управления символь-ными таблицами макропроцессора или компилятора. Рассмотрим,например,
оператор #DEFINE языка “C”. Когда встречаетсястрока вида #DEFINE
YES 1то имя YES и заменяющий текст 1 помещаются в таблицу. Позд-нее, когда имя
YES появляется в операторе вида INWORD = YES;Oно должно быть замещено на 1.Имеются
две основные процедуры, которые управляют имена-ми и заменяющими их текстами.
Функция INSTALL(S,T) записыва-ет имя S и заменяющий текст T в таблицу; здесь
S и T простосимвольные строки. Функция LOOKUP(S) ищет имя S в таблице ивозвращает
либо указатель того места, где это имя найдено,либо NULL, если этого имени
в таблице не оказалось.При этом используется поиск по алгоритму хеширования -поступающее
имя преобразуется в маленькое положительное чис-ло, которое затем используется
для индексации массива указа-телей. Элемент массива указывает на начало
цепочных блоков,описывающих имена, которые имеют это значение хеширования.Если
никакие имена при хешировании не получают этого значе-ния, то элементом массива
будет NULL.Блоком цепи является структура, содержащая указатели насоответствующее
имя, на заменяющий текст и на следующий блокв цепи. Нулевой указатель следующего
блока служит признакомконца данной цепи. STRUCT NLIST \( /* BASIC TABLE
ENTRY */CHAR *NAME;CHAR *DEF;STRUCT NLIST NEXT; / NEXT ENTRY IN CHAIN */ \);
Массив указателей это простоDEFINE HASHSIZE 100TATIC STRUCT NLIST HASHTAB[HASHSIZE]
/ POINTER TABLE */Значение функции хеширования, используемой обеими функ-циями
LOOKUP и INSTALL , получается просто как остаток отделения суммы символьных
значений строки на размер массива.(Это не самый лучший возможный алгоритм, но
его достоинствосостоит в исключительной простоте). * 144 - HASH(S) /* FORM HASH
VALUE FOR STRING */CHAR *S; \(INT HASHVAL;FOR (HASHVAL = 0; *S != '\0'; )HASHVAL
+= *S++;RETURN(HASHVAL % HASHSIZE); \) В результате процесса хеширования выдается
начальный ин-декс в массиве HASHTAB ; если данная строка может бытьгде-то
найдена, то именно в цепи блоков, начало которой ука-зано там. Поиск осуществляется
функцией LOOKUP. Если функцияLOOKUP находит, что данный элемент уже присутствует,
то онавозвращает указатель на него; если нет, то она возвращаетNULL.
STRUCT NLIST LOOKUP(S) / LOOK FOR S IN HASHTAB */CHAR *S; \(STRUCT NLIST *NP;FOR
(NP = HASHTAB[HASH(S)]; NP != NULL;NP=NP->NEXT)IF (STRCMP(S, NP->NAME) == 0)RETURN(NP);
/* FOUND IT */RETURN(NULL); /* NOT FOUND */ Функция INSTALL использует
функцию LOOKUP для определе-ния, не присутствует ли уже вводимое в данный момент
имя;если это так, то новое определение должно вытеснить старое.В противном
случае создается совершенно новый элемент. Еслипо какой-либо причине для нового
элемента больше нет места,то функция INSTALL возвращает NULL. STRUCT NLIST INSTALL(NAME,
DEF) / PUT (NAME, DEF) */CHAR *NAME, *DEF; \(STRUCT NLIST *NP, *LOOKUP();CHAR
*STRSAVE(), *ALLOC();INT HASHVAL;IF((NP = LOOKUP(NAME)) == NULL) \(
/* NOT FOUND */NP = (STRUCT NLIST *) ALLOC(SIZEOF(*NP));IF (NP == NULL)RETURN(NULL);IF
((NP->NAME = STRSAVE(NAME)) == NULL)RETURN(NULL);HASHVAL = HASH(NP->NAME);NP->NEXT
= HASHTAB[HASHVAL];HASHTAB[HASHVAL] = NP;\) ELSE /* ALREADY THERE */FREE((NP->DEF);/*
FREE PREVIOUS DEFINITION */IF ((NP->DEF = STRSAVE(DEF)) == NULL)RETURN
(NULL);RETURN(NP); \) * 145 - Функция STRSAVE просто копирует строку,
указанную в ка-честве аргумента, в место хранения, полученное в результатеобращения
к функции ALLOC. Мы уже привели эту функцию в гла-ве 5. Так как обращение
к функции ALLOC и FREE могут проис-ходить в любом порядке и в связи с проблемой
выравнивания,простой вариант функции ALLOC из главы 5 нам больше не под-ходит;
смотрите главы 7 и 8.Упражнение 6-7.Напишите процедуру, которая будет удалять
имя и опреде-ление из таблицы, управляемой функциями LOOKUP и INSTALL.Упражнение
6-8.Разработайте простую, основанную на функциях этого раз-дела, версию процессора
для обработки конструкций #DEFINE ,пригодную для использования с “C”-программами.
Вам могуттакже оказаться полезными функции GETCHAR и UNGETCH. 6.7. Поля.Когда
вопрос экономии памяти становится очень существен-ным, то может оказаться
необходимым помещать в одно машинноеслово несколько различных объектов; одно
из особенно расп-росраненных употреблений - набор однобитовых признаков вприменениях,
подобных символьным таблицам компилятора. внеш-не обусловленные форматы
данных, такие как интерфейсы аппа-ратных средств также зачастую предполагают
возможность полу-чения слова по частям.Представьте себе фрагмент компилятора,
который работаетс символьной таблицей. С каждым идентификатором программысвязана
определенная информация, например, является он илинет ключевым словом, является
ли он или нет внешним и/илистатическим и т.д. Самый компактный способ закодировать
та-кую информацию - поместить набор однобитовых признаков в от-дельную переменную
типа CHAR или INT.Обычный способ, которым это делается, состоит в опреде-лении
набора “масок”, отвечающих соответствущим битовым по-зициям, как в #DEFINE
KEYWORD 01 #DEFINE EXTERNAL 02 #DEFINE STATIC 04 (числа должны быть степенями
двойки). Тогда обработка битовсведется к “жонглированию битами” с помощью
операций сдвига,маскирования и дополнения, описанных нами в главе 2.Некоторые часто
встречающиеся идиомы:FLAGS \!= EXTERNAL \! STATIC;включает биты EXTERNAL и
STATIC в FLAGS, в то время какFLAGS =\^(еXTERNAL \! STATIC);* 146 - их выключает,
аIF ((FLAGS (EXTERNAL \! STATIC)) == 0) ...истинно, если оба бита выключены.Хотя
этими идиомами легко овладеть, язык “C” в качествеальтернативы предлагает
возможность определения и обработкиполей внутри слова непосредственно, а не посредством
побито-вых логических операций. Поле - это набор смежных битоввнутри
одной переменной типа INT. Синтаксис определения иобработки полей основывается
на структурах. Например, сим-вольную таблицу конструкций #DEFINE, приведенную выше,
можнобы было заменить определением трех полей:STRUCT \(UNSIGNED IS_KEYWORD
: 1;UNSIGNED IS_EXTERN : 1;UNSIGNED IS_STATIC : 1;\) FLAGS;Здесь определяется
переменная с именем FLAGS, которая содер-жит три 1-битовых поля. Следующее за двоеточием
число задаетширину поля в битах. Поля описаны как UNSIGNED, чтобы под-черкнуть,
что они действительно будут величинами без знака.На отдельные поля можно
ссылаться, как FLAGS.IS_STATIE,FLAGS. IS_EXTERN, FLAGS.IS_KEYWORD И т.д.,
то есть точно также, как на другие члены структуры. Поля ведут себя подобнонебольшим
целым без знака и могут участвовать в арифметичес-ких выражениях точно так
же, как и другие целые. Таким обра-зом, предыдущие примеры более естественно
переписать так: FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 1;для включения битов;FLAGS.IS_EXTERN
= FLAGS.IS_STATIC = 0;для выключения битов;IF (FLAGS.IS_EXTERN == 0
&FLAGS.IS_STATIC == 0)...для их проверки.Поле не может перекрывать границу INT;
если указаннаяширина такова, что это должно случиться, то поле выравнива-ется
по границе следующего INT. Полям можно не присваиватьимена; неименованные поля
(только двоеточие и ширина) ис-пользуются для заполнения свободного места. Чтобы
вынудитьвыравнивание на границу следующего INT, можно использоватьспециальную
ширину 0. * 147 - При работе с полями имеется ряд моментов, на которыеследует
обратить внимание. По-видимому наиболее существеннымявляется то, что отражая природу
различных аппаратных сред-ств, распределение полей на некоторых машинах осуществляетсяслева
направо, а на некоторых справа налево. Это означает,что хотя
поля очень полезны для работы с внутренне опреде-ленными структурами данных, при
разделении внешне определяе-мых данных следует тщательно рассматривать вопрос
о том, ка-кой конец поступает первым.Другие ограничения, которые следует иметь
в виду: поляне имеют знака; они могут храниться только в переменных типаINT (или,
что эквивалентно, типа UNSIGNED); они не являютсямассивами; они не имеют адресов,
так что к ним не применимаоперация .
6.8. Объединения.Oбъединения - это
переменная, которая в различные момен-ты времени может содержать объекты разных
типов и размеров,причем компилятор берет на себя отслеживание размера и тре-бований
выравнивания. Объединения представляют возможностьработать с различными
видами данных в одной области памяти,не вводя в программу никакой машинно-зависимой
информации.В качестве примера, снова из символьной таблицы компиля-тора, предположим,
что константы могут быть типа INT , FLOATили быть указателями на символы.
значение каждой конкретнойконстанты должно храниться в переменной соотвествующего
ти-па, но все же для управления таблицей самым удобным было бы,если это
значение занимало бы один и тот же объем памяти ихранилось в том же самом месте
независимо от его типа. это иявляется назначением объединения - выделить отдельную
пере-менную, в которой можно законно хранить любую одну из пере-менных
нескольких типов. Как и в случае полей, синтаксис ос-новывается на структурах.
UNION U_TAG \(INT IVAL;FLOAT FVAL;CHAR *PVAL;\) UVAL;Переменная UVAL будет иметь
достаточно большой размер,чтобыхранить наибольший из трех типов, независимо от
машины, накоторой осуществляется компиляция, - программа не будет за-висить от
характеристик аппаратных средств. Любой из этихтрех типов может быть присвоен
UVAR и затем использован ввыражениях, пока такое использование совместимо: извлекаемыйтип
должен совпадать с последним помещенным типом. Делопрограммиста - следить
за тем, какой тип хранится в объеди-нении в данный момент; если что-либо
хранится как один тип,а извлекается как другой, то результаты будут зависеть отиспользуемой
машины. * 149 - Синтаксически доступ к членам объединения осуществляетсяследующим
образом:имя объединения.членилиуказатель объединения ->член то
есть точно так же, как и в случае структур. если для отс-леживания типа, хранимого
в данный момент в UVAL, использу-ется переменная UTYPE, то можно встретить
такой участокпрограммы: IF (UTYPE == INT)PRINTF(“%D\N”, UVAL.IVAL);ELSE IF (UTYPE
== FLOAT)PRINTF(“%F\N”, UVAL.FVAL);ELSE IF (UTYPE == STRING)PRINTF(“%S\N”,
UVAL.PVAL);ELSEPRINTF(“BAD TYPE %D IN UTYPE\N”, UTYPE);Объединения могут появляться
внутри структур и массивови наоборот. Запись для обращения к члену объединения
вструктуре (или наоборот) совершенно идентична той, котораяиспользуется во
вложенных структурах. например, в массивеструктур, определенным следующим образом
STRUCT \(CHAR *NAME;INT FLAGS;INT UTYPE;UNION \(INT IVAL;FLOAT FVAL;CHAR *PVAL;\)
UVAL;\) SYMTAB[NSYM];на переменную IVAL можно сослаться какSYMTAB[I].UVAL.IVALа
на первый символ строки PVAL как*SYMTAB[I].UVAL.PVALВ сущности объединение
является структурой, в которой всечлены имеют нулевое смещение. Сама структура
достаточно ве-лика, чтобы хранить “самый широкий” член, и выравниваниепригодно
для всех типов, входящих в объединение. Как и вслучае структур, единственными
операциями, которые в настоя-щее время можно проводить с объединениями, являются
доступ к * 150 - члену и извлечение адреса; объединения не могут быть присво-ены,
переданы функциям или возвращены ими. указатели объеди-нений можно использовать
в точно такой же манере, как и ука-затели структур.Программа распределения
памяти, приводимая в главе 8 ,показывает, как можно использовать объединение,
чтобы сде-лать некоторую переменную выровненной по определенному видуграницы
памяти. 6.9. Определение типаВ языке “C” предусмотрена возможность, называемая
TYPEDEFдля введения новых имен для типов данных. Например, описаниеTYPEDEF INT
LENGTH;делает имя LENGTH синонимом для INT. “Тип” LENGTH может быть использован
в описаниях, переводов типов и т.д. Точно таким же образом, как и тип INT:
LENGTH LEN, MAXLEN; LENGTH *LENGTHS[]; Аналогично описаниюTYPEDEF CHAR *STRING;делает
STRING синонимом для CHAR*, то есть для указателя насимволы, что затем можно
использовать в описаниях вида STRING P, LINEPTR[LINES], ALLOC();Обратите внимание,
что объявляемый в конструкции TYPEDEFтип появляется в позиции имени переменной,
а не сразу засловом TYPEDEF. Синтаксически конструкция TYPEDEF подобнаописаниям
класса памяти EXTERN, STATIC и т. Д. мы также ис-пользовали прописные
буквы, чтобы яснее выделить имена.В качестве более сложного примера мы используем
конст-рукцию TYPEDEF для описания узлов дерева, рассмотренных ра-нее в этой
главе:TYPEDEF STRUCT TNODE \( /* THE BASIC NODE */CHAR WORD; / POINTS TO THE TEXT
*/INT COUNT; /* NUMBER OF OCCURRENCES */ STRUCT TNODE LEFT; / LEFT CHILD */
STRUCT TNODE RIGHT; / RIGHT CHILD */\) TREENODE, *TREEPTR;В результате получаем
два новых ключевых слова: TREENODE(структура) и TREEPTR (указатель на структуру).
Тогда функ-цию TALLOC можно записать в виде * 151 -TREEPTR TALLOC() \(CHAR
*ALLOC();RETURN((TREEPTR) ALLOC(SIZEOF(TREENODE))); \) Необходимо подчеркнуть,
что описание TYPEDEF не приводитк созданию нового в каком-либо смысле типа; оно
только до-бавляет новое имя для некоторого существующего типа. приэтом не возникает
и никакой новой семантики: описанные такимспособом переменные обладают точно
теми же свойствами, что ипеременные, описанные явным образом. По существу
конструкцияTYPEDEF сходна с #DEFINE за исключением того, что она интер-претируется
компилятором и потому может осуществлять подста-новки текста, которые выходят
за пределы возможностей мак-ропроцессора языка “C”. Например, TYPEDEF INT (*PFI)
();создает тип PFI для “указателя функции, возвращающей значе-ние типа INT”,
который затем можно было бы использовать впрограмме сортировки из главы 5 в
контексте вида PFI STRCMP, NUMCMP, SWAP; Имеются две основные причины применения
описанийTYPEDEF. Первая причина связана с параметризацией программы,чтобы облегчить
решение проблемы переносимости. Если для ти-пов данных, которые могут быть
машинно-зависимыми, использо-вать описание TYPEDEF, то при переносе программы
на другуюмашину придется изменить только эти описания. Одна из типич-ных ситуаций
состоит в использовании определяемых с помощьюTYPEDEF имен для различных целых
величин и в последующемподходящем выборе типов SHORT, INT и LONG для каждой
имею-щейся машины.Второе назначение TYPEDEF состоит в обеспечении лучшей доку-ментации
для программы - тип с именем TREEPTR может оказать-ся более удобным для
восприятия, чем тип, который описантолько как указатель сложной структуры.И
наконец, всегда существует вероятность, что в будущем ком-пилятор или некоторая
другая программа, такая как LINT, смо-жет использовать содержащуюся в описаниях
TYPEDEF информациюдля проведения некоторой дополнительной проверки программы.*
152 - 7. Ввод и выводСредства ввода/вывода не являются составной частью языка“с”,
так что мы не выделяли их в нашем предыдущем изложении.Однако реальные программы
взаимодействуют со своей окружаю-щей средой гораздо более сложным образом,
чем мы видели досих пор. В этой главе будет описана “стандартная библиотекаввода/вывода”,
то есть набор функций, разработанных дляобеспечения стандартной системы
ввода/вывода для “с”- прог-рамм. Эти функции предназначены для удобства
программногоинтерфейса, и все же отражают только те операции, которыемогут быть
обеспечены на большинстве современных операцион-ных систем. Процедуры достаточно
эффективны для того, чтобыпользователи редко чувствовали необходимость обойти
их “радиэффективности”, как бы ни была важна конкретная задача. И,наконец, эти
процедуры задуманы быть “переносимыми” в томсмысле, что они должны существовать
в совместимом виде налюбой системе, где имеется язык “с”, и что программы, кото-рые
ограничивают свои взаимодействия с системой возможностя-ми, предоставляемыми
стандартной библиотекой, можно будетпереносить с одной системы на другую
по существу без измене-ний.Мы здесь не будем пытаться описать всю библиотеку вво-да/вывода;
мы более заинтересованы в том, чтобы продемонст-рировать сущность
написания “с”-программ, которые взаимодей-ствуют со своей операционной средой.
7.1. Обращение к стандартной библиотекеКаждый исходный файл, который обращается
к функции изстандартной библиотеки, должен вблизи начала содержать стро-ку #INCLUDE
в файле STDIO.H определяются некоторые макросы и переменные,используемые
библиотекой ввода/вывода. Использование угловыхскобок вместо обычных двойных кавычек
- указание компиляторуискать этот файл в справочнике, содержащем заголовки
стан-дартной информации (на системе UNIX обычно LUSRLINELUDE).Кроме того, при
загрузке программы может оказаться необ-ходимым указать библиотеку явно; на системе
PDP-11 UNIX,например, команда компиляции программы имела бы вид:CC исходные
файлы и т.д. -LSгде -LS указывает на загрузку из стандартной библиотеки.7.2. Стандартный
ввод и вывод - функции GETCHAR иPUTCHARСамый простой механизм ввода
заключается в чтении по од-ному символу за раз из “стандартного ввода”, обычно
с терми-нала пользователя, с помощью функции GETCHAR. ФункцияGETCHAR() при каждом
к ней обращении возвращает следующий * 153 - вводимый символ. В большинстве
сред, которые поддерживаютязык “с”, терминал может быть заменен некоторым файлом
с по-мощью обозначения : если PROG использует PUTCHAR,то командная строка
PROG>OUTFILEприведет к записи стандартного вывода в файл OUTFILE, а нена терминал.
На системе UNIX можно также использовать поточ-ный механизм. Строка PROG \!
ANOTHERPROGпомещает стандартный вывод PROG в стандартный ввод ANOTHERPROG. И опять
PROG не будет осведомлена об изменении направления.Вывод, осуществляемый функцией
PRINTF, также поступает встандартный вывод, и обращения к PUTCHAR и PRINTF
могут пе-ремежаться.Поразительное количество программ читает только из одно-го
входного потока и пишет только в один выходной поток; длятаких программ ввод
и вывод с помощью функций GETCHAR,PUTCHAR и PRINTF может оказаться вполне адекватным
и для на-чала определенно достаточным. Это особенно справедливо тог- * 154
- да, когда имеется возможность указания файлов для ввода ивывода и поточный
механизм для связи вывода одной программыс вводом другой. Рассмотрим, например,
программу LOWER, ко-торая преобразует прописные буквы из своего ввода в строч-ные:
#INCLUDE MAIN() /* CONVERT INPUT TO LOWER CASE */ \(INT C;WHILE ((C = GETCHAR())
!= EOF)PUTCHAR(ISUPPER© ? TOLOWER© : C); \) “Функции” ISUPPER и TOLOWER
на самом деле являются макроса-ми, определенными в STDIO.H . Макрос ISUPPER проверяет,
яв-ляется ли его аргумент буквой из верхнего регистра, и возв-ращает ненулевое
значение, если это так, и нуль в противномслучае. Макрос TOLOWER преобразует
букву из верхнего регист-ра в ту же букву нижнего регистра. Независимо от
того, какэти функции реализованы на конкретной машине, их внешнее по-ведение
совершенно одинаково, так что использующие их прог-раммы избавлены от знания символьного
набора.Если требуется преобразовать несколько файлов, то можнособрать
эти файлы с помощью программы, подобной утилите CATсистемы UNIX, CAT FILE1 FILE2
... \! LOWER>OUTPUTи избежать тем самым вопроса о том, как обратиться к этимфайлам
из программы. (Программа CAT приводится позже в этойглаве).Кроме того отметим,
что в стандартной библиотеке вво-да/вывода “функции” GETCHAR и PUTCHAR на
самом деле могутбыть макросами. Это позволяет избежать накладных расходов наобращение
к функции для обработки каждого символа. В главе 8мы продемонстрируем,
как это делается. 7.3. Форматный вывод - функция PRINTF Две функции: PRINTF для
вывода и SCANF для ввода (следу-ющий раздел) позволяют преобразовывать численные
величины всимвольное представлEние и обратно. Они также позволяют ге-нерировать
и интерпретировать форматные строки. Мы уже всюдув предыдущих главах неформально
использовали функцию PRINTF;здесь приводится более полное и точное описание.
Функция PRINTF(CONTROL, ARG1, ARG2, ...)* 155 - преобразует, определяет формат
и печатает свои аргументы встандартный вывод под управлением строки CONTROL.
Управляю-щая строка содержит два типа объектов: обычные символы, ко-торые просто
копируются в выходной поток, и спецификациипреобразований, каждая из которых
вызывает преобразование ипечать очередного аргумента PRINTF.Каждая спецификация
преобразования начинается с символа% и заканчивается символом преобразования.
Между % и симво-лом преобразования могут находиться:* знак минус, который указывает
о выравнивании преобразован-ного аргумента по левому краю его поля.* Строка
цифр, задающая минимальную ширину поля. Преобразо-ванное число будет напечатано
в поле по крайней мере этойширины, а если необходимо, то и в более широком.
Если пре-образованный аргумент имеет меньше символов, чем указаннаяширина поля,
то он будет дополнен слева (или справа, еслибыло указано выравнивание по левому
краю)заполняющими сим-волами до этой ширины. Заполняющим символом обычно являет-ся
пробел, а если ширина поля указывается с лидирующим ну-лем, то этим символом
будет нуль (лидирующий нуль в данномслучае не означает восьмеричной ширины
поля).* Точка, которая отделяет ширину поля от следующей строкицифр.* Строка
цифр (точность), которая указывает максимальноечисло символов строки, которые
должны быть напечатаны, иличисло печатаемых справа от десятичной точки цифр для
пере-менных типа FLOAT или DOUBLE.* Модификатор длины L, который указывает, что
соответствую-щий элемент данных имеет тип LONG, а не INT.Ниже приводятся символы
преобразования и их смысл:D - аргумент преобразуется к десятичному виду.O -
Аргумент преобразуется в беззнаковую восьмеричную форму(без лидирующего нуля).X
- Аргумент преобразуется в беззнаковую шестнадцатеричнуюформу (без лидирующих
0X).U - Аргумент преобразуется в беззнаковую десятичную форму.C - Аргумент рассматривается
как отдельный символ.S - Аргумент является строкой: символы строки
печатаются дотех пор, пока не будет достигнут нулевой символ или не бу-дет напечатано
количество символов, указанное в специфика-ции точности.E - Аргумент, рассматриваемый
как переменная типа FLOAT илиDOUBLE, преобразуется в десятичную
форму в виде[-]M.NNNNNNE[+-]XX, где длина строки из N определяетсяуказанной точностью.
Точность по умолчанию равна 6.F - Аргумент, рассматриваемый как переменная
типа FLOAT илиDOUBLE, преобразуется в десятичную форму в виде[-]MMM.NNNNN,
где длина строки из N определяется указаннойточностью. Точность по умолчанию равна
6. отметим, что этаточность не определяет количество печатаемых в формате Fзначащих
цифр. * 156 - G - Используется или формат %E или %F, какой короче; незна-чащие
нули не печатаются.Если идущий за % символ не является символом преобразования,то
печатается сам этот символ; следовательно,символ % можнонапечатать,
указав %%.Большинство из форматных преобразований очевидно и былопроиллюстрировано
в предыдущих главах. Единственным исключе-нием является то, как точность взаимодействует
со строками.Следующая таблица демонстрирует влияние задания различныхспецификаций
на печать “HELLO, WORLD” (12 символов). Мы по-местили двоеточия
вокруг каждого поля для того, чтобы вымогли видеть его протяженность. :%10S:
:HELLO, WORLD: :%10-S: :HELLO, WORLD: :%20S: : HELLO, WORLD: :%-20S: :HELLO, WORLD
: :%20.10S: : HELLO, WOR: :%-20.10S: :HELLO, WOR : :%.10S: :HELLO, WOR: Предостережение:
PRINTF использует свой первый аргументдля определения числа последующих
аргументов и их типов. Ес-ли количество аргументов окажется недостаточным
или они бу-дут иметь несоответственные типы, то возникнет путаница и выполучите
бессмысленные результаты.Упражнение 7-1. Напишите программу, которая будет
печатать разумным об-разом произвольный ввод. Как минимум она должна печататьнеграфические
символы в восьмеричном или шестнадцатеричномвиде (в соответствии с
принятыми у вас обычаями) и склады-вать длинные строки. 7.4. Форматный ввод - функция
SCANFОсуществляющая ввод функция SCANF является аналогомPRINTF и позволяет
проводить в обратном направлении многиеиз тех же самых преобразований. Функция
SCANF(CONTROL, ARG1, ARG2, ...)читает символы из стандартного ввода, интерпретирует
их всоответствии с форматом, указанном в аргументе CONTROL, ипомещает результаты
в остальные аргументы. Управляющий аргу-мент описывается ниже; другие
аргументы, каждый из которыхдолжен быть указателем, определяют, куда следует поместитьсоответствующим
образом преобразованный ввод.Управляющая строка обычно
содержит спецификации преобра-зования, которые используются для непосредственной
интерпре-тации входных последовательностей. Управляющая строка можетсодержать:*
пробелы, табуляции или символы новой строки (“символы пус-тых промежутков”),
которые игнорируются.* 157 -* Обычные символы (не %), которые предполагаются совпадающи-ми
со следующими отличными от символов пустых промежутковсимволами входного
потока.* Спецификации преобразования, состоящие из символа %, нео-бязательного
символа подавления присваивания *, необяза-тельного числа, задающего максимальную
ширину поля и сим-вола преобразования.Спецификация преобразования управляет
преобразованиемследующего поля ввода. нормально результат помещается в пе-ременную,
которая указывается соответствующим аргументом.Если, однако , с помощью
символа * указано подавление прис-ваивания, то это поле ввода просто пропускается
и никакогоприсваивания не производится. Поле ввода определяется какстрока
символов, которые отличны от символов простых проме-жутков; оно продолжается
либо до следующего символа пустогопромежутка, либо пока не будет исчерпана ширина
поля, еслиона указана. Отсюда следует, что при поиске нужного ей вво-да, функция
SCANF будет пересекать границы строк, посколькусимвол новой строки входит
в число пустых промежутков.Символ преобразования определяет интерпретацию поля
вво-да; согласно требованиям основанной на вызове по значениюсемантики языка “с”
соответствующий аргумент должен бытьуказателем. Допускаются следующие символы
преобразования:D - на вводе ожидается десятичное целое; соответствующий ар-гумент
должен быть указателем на целое.O - На вводе ожидается восьмеричное целое
(с лидирующим ну-лем или без него); соответствующий аргумент должен бытьуказателем
на целое.X - На вводе ожидается шестнадцатеричное целое (с лидирующи-ми 0X
или без них); соответствующий аргумент должен бытьуказателем на целое.H - На вводе
ожидается целое типа SHORT; соответсвующий ар-гумент должен быть указателем
на целое типа SHORT.C - Ожидается отдельный символ; соответствующий аргументдолжен
быть указателем на символы; следующий вводимыйсимвол помещается в указанное
место. Обычный пропуск сим-волов пустых промежутков в этом случае подавляется;
длячтения следующего символа, который не является символомпустого промежутка,
пользуйтесь спецификацией преобразо-вания %1S.S - Ожидается символьная строка;
соответствующий аргументдолжен быть указателем символов, который указывает намассив
символов, который достаточно велик для принятиястроки и добавляемого в конце
символа \0.F - Ожидается число с плавающей точкой; соответствующий ар-гумент
должен быть указателем на переменную типа FLOAT.Е - символ преобразования E является
синонимом для F. Форматввода переменной типа FLOAT включает необязательный
знак,строку цифр, возможно содержащую десятичную точку и нео-бязательное поле
экспоненты, состоящее из буквы E, за ко-торой следует целое, возможно имеющее
знак. * 158 - Перед символами преобразования D, O и X может стоять L,которая
означает , что в списке аргументов должен находитьсяуказатель на переменную типа
LONG, а не типа INT. Аналогич-но, буква L может стоять перед символами преобразования
Eили F, говоря о том, что в списке аргументов должен нахо-диться указатель
на переменную типа DOUBLE, а не типа FLOAT.Например, обращениеINT I;FLOAT
X;CHAR NAME[50];SCANF(“D%F %S”, I,X,NAME);со строкой на вводе25 54.32E-1 THOMPSONприводит
к присваиванию I значения 25,X - значения 5.432 иNAME - строки “THOMPSON”,
надлежащим образом законченнойсимволом \ 0. эти три поля ввода можно разделить
столькимипробелами, табуляциями и символами новых строк, сколько выпожелаете.
Обращение INT I;FLOAT X;CHAR NAME[50];SCANF(“%2D %F %*D %2S”, I,X,NAME);с
вводом56789 0123 45A72присвоит I значение 56, X - 789.0, пропустит 0123 и поместитв
NAME строку “45”. при следующем обращении к любой процеду-ре ввода рассмотрение
начнется с буквы A. В этих двух приме-рах NAME является указателем и, следовательно,
перед ним ненужно помещать знак .
Вкачестве другого примера перепишем
теперь элементарныйкалькулятор из главы 4, используя для преобразования вводафункцию
SCANF:#INCLUDE MAIN() /* RUDIMENTARY DESK CALCULATOR */ \(DOUBLE SUM,
V;SUM =0;WHILE (SCANF(“%LF”, V)!=EOF)PRINTF(“\T%.2F\N”, SUM += V); \) выполнение
функции SCANF заканчивается либо тогда, когда онаисчерпывает свою управляющую
строку, либо когда некоторыйэлемент ввода не совпадает с управляющей спецификацией.
Вкачестве своего значения она возвращает число правильно сов-падающих и
присвоенных элементов ввода. Это число может быть * 159 - использовано для определения
количества найденных элементовввода. при выходе на конец файла возвращается
EOF; подчерк-нем, что это значение отлично от 0, что следующий вводимыйсимвол
не удовлетворяет первой спецификации в управляющейстроке. При следующем обращении
к SCANF поиск возобновляетсянепосредственно за последним введенным символом.Заключительное
предостережение: аргументы функции SCANFдолжны быть указателями.
Несомненно наиболее распространен-ная ошибка состоит в написании SCANF(“%D”,
N);вместоSCANF(“%D”, N)7.5. Форматное преобразование в памяти От функции SCANF
и PRINTF происходят функции SSCANF иSPRINTF, которые осуществляют аналогичные
преобразования, нооперируют со строкой, а не с файлом. Обращения к этим функ-циям
имеют вид: SPRINTF(STRING, CONTROL, ARG1, ARG2, ...)SSCANF(STRING, CONTROL,
ARG1, ARG2, ...)Как и раньше , функция SPRINTF преобразует свои аргументыARG1,
ARG2 и т.д. В соответствии с форматом, указанным вCONTROL, но помещает результаты
в STRING, а не в стандартныйвывод. KОнечно, строка STRING должна быть достаточно
велика,чтобы принять результат. Например, если NAME - это символь-ный массив,
а N - целое, то SPRINTF(NAME, “TEMP%D”, N);создает в NAME строку вида TEMPNNN,
где NNN - значение N.Функция SSCANF выполняет обратные преобразования - онапросматривает
строку STRING в соответствии с форматом в ар-гументе CONTROL и
помещает результирующие значения в аргу-менты ARG1, ARG2 и т.д.эти аргументы должны
быть указателя-ми. В результате обращения SSCANF(NAME, “TEMP%D”, N)переменная
N получает значение строки цифр, следующих заTEMP в NAME.Упражнение 7-2.Перепишите
настольный калькулятор из главы 4, используядля ввода и преобразования
чисел SCANF и/или SSCANF.* 160 - 7.6. Доступ к файламВсе до сих пор написанные
программы читали из стандарт-ного ввода и писали в стандартный вывод, относительно
кото-рых мы предполагали, что они магическим образом предоставле-ны программе
местной операционной системой.Следующим шагом в вопросе ввода-вывода является
написа-ние программы, работающей с файлом, который не связан зара-нее с программой.
одной из программ, которая явно демонстри-рует потребность в таких операциях,
является CAT, котораяобъединяет набор из нескольких именованных файлов в
стандар-тный вывод. Программа CAT используется для вывода файлов натерминал и в
качестве универсального сборщика ввода дляпрограмм, которые не имеют возможности
обращаться к файлампо имени. Например, команда CAT X.C.Y.Cпечатает содержимое
файлов X.C и Y.C в стандартный вывод.Вопрос состоит в том, как организовать чтение
из имено-ванных файлов, т.е., как связать внешние имена, которымимыслит пользователь,
с фактически читающими данные операто-рами. Эти правила просты. Прежде
чем можно считывать из неко-торого файла или записывать в него, этот файл
должен бытьоткрыт с помощью функции FOPEN из стандартной библиотеки.функция FOPEN
берет внешнее имя (подобное X.C или Y.C), про-водит некоторые обслуживающие
действия и переговоры с опера-ционной системой (детали которых не должны нас касаться)
ивозвращает внутреннее имя, которое должно использоваться припоследующих
чтениях из файла или записях в него.Это внутреннее имя, называемое “указателем
файла”, фак-тически является указателем структуры, которая содержит ин-формацию
о файле, такую как место размещения буфера, текущаяпозиция символа в буфере,
происходит ли чтение из файла илизапись в него и тому подобное. Пользователи не
обязаны знатьэти детали, потому что среди определений для стандартноговвода-вывода,
получаемых из файла STDIO.H, содержится опре-деление структуры с именем
FILE. Единственное необходимоедля указателя файла описание демонстрируется примером:
FILE *FOPEN(), *FP;Здесь говорится, что FP является указателем на FILE иFOPEN
возвращает указатель на FILE. Oбратите внимание, чтоFILE является именем типа,
подобным INT, а не ярлыку струк-туры; это реализовано как TYPEDEF. (Подробности
того, каквсе это работает на системе UNIX, приведены в главе 8).Фактическое
обращение к функции FOPEN в программе имеетвид:FP=FOPEN(NAME,MODE);* 161 -
Первым аргументом функции FOPEN является “имя” файла, кото-рое задается в виде символьной
строки. Второй аргумент MODE(“режим”) также является символьной строкой,
которая указы-вает, как этот файл будет использоваться. Допустимыми режи-мами
являются: чтение (“R”), запись (“W”) и добавление(“A”).Если вы откроете файл,
который еще не сущетвует, для за-писи или добавления, то такой файл будет создан
(если этовозможно). Открытие существующего файла на запись приводит котбрасыванию
его старого содержимого. Попытка чтения несу-ществующего файла является
ощибкой. Ошибки могут быть обус-ловлены и другими причинами (например, попыткой
чтения изфайла, не имея на то разрешения). При наличии какой-либоошибки функция
возвращает нулевое значение указателя NULL(которое для удобства также определяется
в файле STDIO.H).Другой необходимой вещью является способ чтения или за-писи,
если файл уже открыт. Здесь имеется несколько возмож-ностей, из которых GETC
и PUTC являются простейшими.функцияGETC возвращает следующий символ из файла;
ей необходим ука-затель файла, чтобы знать, из какого файла читать. Таким об-разом,C=GETC(FP)помещает
в “C” следующий символ из файла, указанного посред-ством
FP, и EOF, если достигнут конец файла.Функция PUTC, являющаяся обращением к
функции GETC,PUTC(C,FP)помещает символ “C” в файл FP и возвращает “C”. Подобно
фун-кциям GETCHAR и PUTCHAR, GETC и PUTC могут быть макросами, а не функциями.При
запуске программы автоматически открываются три фай-ла, которые снабжены определенными
указателями файлов. Этимифайлами являются стандартный ввод, стандартный
вывод и стан-дартный вывод ошибок; соответствующие указатели файлов назы-ваются
STDIN, STDOUT и STDERR. Обычно все эти указатели свя-заны с терминалом, но
STDIN и STDOUT могут быть перенаправ-лены на файлы или в поток (PIPE), как описывалось
в разделе7.2.Функции GETCHAR и PUTCHAR могут быть определены в терми-налах
GETC, PUTC, STDIN и STDOUT следующим образом:#DEFINE GETCHAR() GETC(STDIN)
#DEFINE PUTCHAR© PUTC(C,STDOUT)При работе с файлами для форматного ввода и вывода
можно ис-пользовать функции FSCANF и FPRINTF. Они идентичны функциямSCANF и
PRINTF, за исключением того, что первым аргументомявляется указатель файла, определяющий
тот файл, который бу-дет читаться или куда будет вестись запись; управляющаястрока
будет вторым аргументом. * 162 - Покончив с предварительными замечаниями,
мы теперь всостоянии написать программу CAT для конкатенации файлов.Используемая
здесь основная схема оказывается удобной вомногих программах: если
имеются аргументы в командной стро-ке, то они обрабатываются последовательно. Если
такие аргу-менты отсутствуют, то обрабатывается стандартный ввод. Этопозволяет
использовать программу как самостоятельно, так икак часть большей задачи.
#INCLUDE MAIN(ARGC, ARGV) /*CAT: CONCATENATE FILES*/INT ARGC;CHAR *ARGV[]; \(FILE
*FP, *FOPEN();IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/FILECOPY(STDIN);ELSEWHILE
(--ARGC > 0)IF ((FP=FOPEN(*++ARGV,”R”))==NULL) \(PRINTF(“CAT:CAN'T OPEN
%\N”,*ARGV);BREAK;\) ELSE \(FILECOPY(FP);FCLOSE(FP); \) \)FILECOPY(FP) /*COPY FILE
FP TO STANDARD OUTPUT*/FILE *FP; \(INT C;WHILE ((C=GETC(FP)) !=EOF)PUTC(C,
STDOUT); \) Указатели файлов STDIN и STDOUT заранее определены в библио-теке ввода-вывода
как стандартный ввод и стандартный вывод;они могут быть использованы
в любом месте, где можно исполь-зовать объект типа FILE*.они однако являются константами,
ане переменными, так что не пытайтесь им что-либо присваи-вать.Функция
FCLOSE является обратной по отношению к FOPEN;она разрывает связь между указателем
файла и внешним именем,установленную функцией FOPEN, и высвобождает указатель
файладля другого файла.большинство операционных систем имеют не-которые
ограничения на число одновременно открытых файлов,которыми может распоряжаться
программа. Поэтому, то как мыпоступили в CAT, освободив не нужные нам более объекты,
яв-ляется хорошей идеей. Имеется и другая причина для примене-ния функции
FCLOSE к выходному файлу - она вызывает выдачуинформации из буфера, в котором
PUTC собирает вывод. (Принормальном завершении работы программы функция FCLOSE
вызы-вается автоматически для каждого открытого файла).* 163 - 7.7. Обработка ошибок
- STDERR и EXITОбработка ошибок в CAT неидеальна. Неудобство заключает-ся
в том, что если один из файлов по некоторой причине ока-зывается недоступным,
диагностическое сообщение об этом пе-чатается в конце объединенного вывода. Это
приемлемо, есливывод поступает на терминал, но не годится, если вывод пос-тупает
в некоторый файл или через поточный (PIPELINE) меха-низм в другую программу.Чтобы
лучше обрабатывать такую ситуацию, к программеточно таким же образом, как
STDIN и STDOUT, присоединяетсявторой выходной файл, называемый STDERR. Если это
вообщевозможно, вывод, записанный в файле STDERR, появляется натерминале пользователя,
даже если стандартный вывод направ-ляется в другое место.Давайте переделаем
программу CAT таким образом, чтобысообщения об ошибках писались в стандартный
файл ошибок.“INCLUDE MAIN(ARGC,ARGV) /*CAT: CONCATENATE FILES*/INT ARGC;CHAR
*ARGV[]; \(FILE *FP, *FOPEN();IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/FILECOPY(STDIN);ELSEWHILE
(--ARGC > 0)IF((FP=FOPEN(*++ARGV,”R#))==NULL) \(PRINTF(STDERR,“CAT:
CAN'T OPEN,%S\N”, ARGV);EXIT(1);\) ELSE \(FILECOPY(FP); \)EXIT(0);
\) Программа сообщает об ошибках двумя способами. Диагностичес-кое сообщение,
выдаваемое функцией FPRINTF, поступает вSTDERR и, таким образом, оказывается на
терминале пользова-теля, а не исчезает в потоке (PIPELINE) или в выходном фай-ле.Программа
также использует функцию EXIT из стандартнойбиблиотеки, обращение
к которой вызывает завершение выполне-ния программы. Аргумент функции EXIT доступен
любой програм-ме, обращающейся к данной функции, так что успешное или неу-дачное
завершение данной программы может быть проверено дру-гой программой, использующей
эту в качестве подзадачи. Посоглашению величина 0 в качетсве возвращаемого
значения сви-детельствует о том, что все в порядке, а различные ненулевыезначения
являются признаками нормальных ситуаций. * 164 - Функция EXIT вызывает
функцию FCLOSE для каждого откры-того выходного файла, с тем чтобы вывести всю
помещенную вбуферы выходную информацию, а затем вызывает функцию _EXIT.Функция
_EXIT приводит к немедленному завершению без очисткикаких-либо буферов; конечно,
при желании к этой функции мож-но обратиться непосредственно. 7.8. Ввод и вывод
строкСтандартная библиотека содержит функцию FGETS, совершен-но аналогичную
функции GETLINE, которую мы использовали навсем протяжении книги. В результате
обращения FGETS(LINE, MAXLINE, FP)следующая строка ввода (включая символ новой
строки) считы-вается из файла FP в символьный массив LINE; самое большоеMAXLINE_1
символ будет прочитан. Результирующая строка за-канчивается символом \ 0.
Нормально функция FGETS возвращаетLINE; в конце файла она возвращает NULL. (Наша
функцияGETLINE возвращает длину строки, а при выходе на конец файла* нуль).Предназначенная
для вывода функция FPUTS записываетстроку (которая не обязана содержать
символ новой строки) вфайл:FPUTS(LINE, FP)Чтобы показать, что в функциях
типа FGETS и FPUTS нет ничего таинственного, мы приводим их ниже, скопированными
непосредственно из стандартной библиотеки ввода-вывода:#INCLUDE CHAR *FGETS(S,N,IOP)
/*GET AT MOST N CHARS FROM IOP*/CHAR *S;INT N;REGISTER FILE *IOP; \(REGISTER
INT C;REGISTER CHAR *CS;CS = S;WHILE(--N>0&(C=GETC(IOP)) !=EOF)IF ((*CS++
= C)=='\N')BREAK;*CS = '\0';RETURN((C==EOF &CS==S) 7 NULL : S); \)FPUTS(S,IOP)
/*PUT STRING S ON FILS IOP*/REGISTER CHAR *S;REGISTER FILE *IOP; \(REGISTER INT
C;WHILE (C = *S++)PUTC(C,IOP); \) * 165 - Упражнение 7-3.Напишите программу
сравнения двух файлов, которая будетпечатать первую строку и позицию символа, где
они различают-ся.Упражнение 7-4.Переделайте программу поиска заданной комбинации
симво-лов из главы 5 таким образом, чтобы в качестве ввода исполь-зовался
набор именованных файлов или, если никакие файлы неуказаны как аргументы, стандартный
ввод. Следует ли печататьимя файла при нахождении подходящей строки?Упражнение
7-5.Напишите программу печати набора файлов, которая начина-ет каждый новый
файл с новой страницы и печатает для каждогофайла заголовок и счетчик текущих
страниц.7.9. Несколько разнообразных функцийСтандартная библиотека предоставляет
множество разнооб-разных функций, некоторые из которых оказываются особеннополезными.
Мы уже упоминали функции для работы со строками:STRLEN, STRCPY, STRCAT
и STRCMP. Вот некоторые другие. 7.9.1. Проверка вида символов и преобразованияНекоторые
макросы выполняют проверку символов и преобра-зования: SALPHA© не
0, если “C” алфавитный символ,0 - если нет.SUPPER© Не 0, если “C” буква верхнего
регистра,0 - если нет.SLOWER© Не 0, если “C” буква нижнего регистра,0 - если
нет.SDIGIT© Не 0, если “C” цифра,0 - если нет.SSPACL© Не 0, если “C” пробел, табуляцияили
новая строка, 0 - если нет.OUPPER© Преобразует “C” в букву верхнего
регистра.OLOWER© Преобразует “C” в букву нижнего регистра. 7.9.2. Функция UNGETCСтандартная
библиотека содержит довольно ограниченнуюверсию функции UNGETCH, написанной
нами в главе 4; она назы-вается UNGETC. В результате обращения UNGETC(C,FP)символ
“C” возвращается в файл FP. Позволяется возвращать вкаждый файл только
один символ. Функция UNGETC может бытьиспользована в любой из функций ввода
и с макросами типаSCANF, GETC или GETCHAR. * 166 - 7.9.3. Обращение к системеФункция
SYSTEM(S) выполняет команду, содержащуюся в сим-вольной строке S, и затем
возобновляет выполнение текущейпрограммы. Содержимое S сильно зависит от используемой
опе-рационной системы. В качестве тривиального примера, укажем,что на
системе UNIX строка SYSTEM(“DATE”);приводит к выполнению программы DATE, которая
печатает датуи время дня. 7.9.4. Управление памятьюФункция CALLOC весьма сходна
с функцией ALLOC, использо-ванной нами в предыдущих главах. В результате обращенияCALLOC(N,
SIZEOF(OBJCCT))возвращается либо указатель пространства, достаточного
дляразмещения N объектов указанного размера, либо NULL, еслизапрос не может
быть удволетворен. Отводимая память инициа-лизируется нулевыми значениями.Указатель
обладает нужным для рассматриваемых объектоввыравниванием, но ему следует
приписывать соответствующийтип, как в CHAR *CALLOC();INT *IP;IP=(INT*) CALLOC(N,SIZEOF(INT));Функция
CFREE(P) освобождает пространство, на котороеуказывает
“P”, причем указатель “P” певоначально должен бытьполучен в результате обращения
к CALLOC. Здесь нет никакихограничений на порядок освобождения пространства,
но будетнеприятнейшей ошибкой освободить что-нибудь, что не было по-лучено обращением
к CALLOC.Реализация программы распределения памяти, подобнойCALLOC, в
которой размещенные блоки могут освобождаться впроизвольном порядке, продемонстрирована
в главе 8.* 167 - 8. Интерфейс системы UNIXМатериал этой главы относится
к интерфейсу между с-прог-раммами и операционной системой UNIX. Так как большинствопользователей
языка “C” работают на системе UNIX, эта главаокажется полезной
для большинства читателей. даже если выиспользуете с-компилятор на другой
машине, изучение приводи-мых здесь примеров должно помочь вам глубже проникнуть
в ме-тоды программирования на языке “C”.Эта глава делится на три основные части:
ввод/вывод,система файлов и распределение памяти. Первые две частипредполагают
небольшое знакомство с внешними характеристика-ми системы UNIX.В главе 7 мы
имели дело с системным интерфейсом, которыйодинаков для всего многообразия операционных
систем. На каж-дой конкретной системе функции стандартной библиотеки
должныбыть написаны в терминах ввода-вывода, доступных на данноймашине. В следующих
нескольких разделах мы опишем основнуюсистему связанных с вводом и выводом
точек входа операцион-ной системы UNIX и проиллюстрируем, как с их помощью могутбыть
реализованы различные части стандартной библиотеки. 8.1. Дескрипторы файловВ
операционной системе UNIX весь ввод и вывод осуществ-ляется посредством чтения
файлов или их записи, потому чтовсе периферийные устройства, включая даже
терминал пользова-теля, являются файлами определенной файловой системы. Этоозначает,
что один однородный интерфейс управляет всеми свя-зями между программой и
периферийными устройствами.В наиболее общем случае перед чтением из файла или
за-писью в файл необходимо сообщить системе о вашем намерении;этот процесс называется
“открытием” файла. Система выясня-ет,имеете ли вы право поступать таким
образом (существует лиэтот файл? имеется ли у вас разрешение на обращение к не-му?),
и если все в порядке, возвращает в программу небольшоеположительное целое
число, называемое дескриптором файла.всякий раз, когда этот файл используется
для ввода или выво-да, для идентификации файла употребляется дескриптор файла,а
не его имя. (Здесь существует примерная аналогия с исполь-зованием READ (5,...)
и WRITE (6,...) в фортране). Вся ин-формация об открытом файле содержится в системе;
программапользователя обращается к файлу только через дескриптор фай-ла.Для
удобства выполнения обычных операций ввода и выводас помощью терминала пользователя
существуют специальные сог-лашения. Когда интерпретатор команд (“SHELL”)
прогоняетпрограмму, он открывает три файла, называемые стандартнымвводом, стандартным
выводом и стандартным выводом ошибок,которые имеют соответственно числа
0, 1 и 2 в качестве деск-рипторов этих файлов. В нормальном состоянии все они
связаныс терминалом, так что если программа читает с дескрипторомфайла 0 и пишет
с дескрипторами файлов 1 и 2, то она можетосуществлять ввод и вывод с помощью
терминала, не заботясьоб открытии соответствующих файлов. * 168 - Пользователь
программы может перенаправлять ввод и выводна файлы, используя операции командного
интерпретатора SHELL“” :PROG OUTFILEВ этом случае интерпретатор команд
SHELL изменит присваива-ние по умолчанию дескрипторов файлов 0 и 1 с терминала
науказанные файлы. Нормально дескриптор файла 2 остается свя-занным с терминалом,
так что сообщения об ошибках могут пос-тупать туда. Подобные замечания справедливы
и тогда, когдаввод и вывод связан с каналом. Следует отметить, что во всехслучаях
прикрепления файлов изменяются интерпретаторомSHELL, а не программой.
Сама программа, пока она используетфайл 0 для ввода и файлы 1 и 2 для вывода,
не знает ни отку-да приходит ее ввод, ни куда поступает ее выдача. 8.2. Низкоуровневый
ввод/вывод - операторы READ и WRITE.Самый низкий уровень ввода/вывода в
системе UNIX не пре-дусматривает ни какой-либо буферизации, ни какого-либо дру-гого
сервиса; он по существу является непосредственным вхо-дом в операционную
систему. Весь ввод и вывод осуществляетсядвумя функциями: READ и WRITE. Первым
аргументом обеих функ-ций является дескриптор файла. Вторым аргументом являетсябуфер
в вашей программе, откуда или куда должны поступатьданные. Третий аргумент
- это число подлежащих пересылкебайтов. Обращения к этим функциям имеют вид:
N_READ=READ(FD,BUF,N);N_WRITTEN=WRITE(FD,BUF,N);При каждом обращении возвращается
счетчик байтов, указываю-щий фактическое число переданных байтов. При чтении
возвра-щенное число байтов может оказаться меньше, чем запрошенноечисло. Возвращенное
нулевое число байтов означает конец фай-ла, а “-1” указывает на наличие
какой-либо ошибки. При запи-си возвращенное значение равно числу фактически записанныхбайтов;
несовпадение этого числа с числом байтов, котороепредполагалось
записать, обычно свидетельствует об ошибке.Количество байтов, подлежащих чтению
или записи, можетбыть совершенно произвольным. Двумя самыми распространеннымивеличинами
являются “1”, которая означает передачу одногосимвола за обращение (т.е.
Без использования буфера), и“512”, которая соответствует физическому размеру
блока намногих периферийных устройствах. Этот последний размер будетнаиболее
эффективным, но даже ввод или вывод по одному сим-волу за обращение не будет необыкновенно
дорогим.Объединив все эти факты, мы написали простую программудля
копирования ввода на вывод, эквивалентную программе ко-пировки файлов, написанной
в главе 1. На системе UNIX этапрограмма будет копировать что угодно куда угодно,
потомучто ввод и вывод могут быть перенаправлены на любой файл илиустройство.
* 169 - #DEFINE BUFSIZE 512 /*BEST SIZE FOR PDP-11 UNIX*/MAIN() /*COPY INPUT
TO OUTPUT*/ \(CHAR BUF[BUFSIZE];INT N;WHILE((N=READ(0,BUF,BUFSIZE))>0)WRITE(1,BUF,N);
\) Если размер файла не будет кратен BUFSIZE, то при некоторомобращении
к READ будет возвращено меньшее число байтов, ко-торые затем записываются с помощью
WRITE; при следующем пос-ле этого обращении к READ будет возвращен нуль.Поучительно
разобраться, как можно использовать функцииREAD и WRITE для построения
процедур более высокого уровня,таких как GETCHAR, PUTCHAR и т.д. Вот, например,
вариантфункции GETCHAR, осуществляющий ввод без использования буфе-ра.#DEFINE
CMASK 0377 /*FOR MAKING CHAR'S > 0*/GETCHAR() /*UNBUFFERED SINGLE CHARACTER
INPUT*/ \(CHAR C;RETURN((READ(0,C,1)>07 CMASK : EOF); \) Переменная “C” должна
быть описана как CHAR, потому что фун-кция READ принимает указатель на символы.
Возвращаемый сим-вол должен быть маскирован числом 0377 для гарантии его по-ложительности;
в противном случае знаковый разряд может сде-лать его значение отрицательным.
(Константа 0377 подходитдля эвм PDP-11, но не обязательно для других
машин).Второй вариант функции GETCHAR осуществляет ввод больши-ми порциями,
а выдает символы по одному за обращение.#DEFINE CMASK 0377 /*FOR MAKING CHAR'S>0*/#DEFINE
BUFSIZE 512GETCHAR() /*BUFFERED VERSION*/ \(STATIC CHAR BUF[BUFSIZE];STATIC
CHAR *BUFP = BUF;STATIC INT N = 0;IF (N==0) \( /*BUFFER IS EMPTY*/N=READ(0,BUF,BUFSIZE);BUFP
= BUF; \)RETURN((--N>=0) ? *BUFP++ CMASK : EOF); \) 8.3. Открытие,
создание, закрытие и расцепление(UNLINK).Кроме случая, когда по умолчанию
определены стандартныефайлы ввода, вывода и ошибок, вы должны явно открывать
фай-лы, чтобы затем читать из них или писать в них. Для этой це-ли существуют
две точки входа: OPEN и CREAT. * 170 - Функция OPEN весьма сходна с функцией
FOPEN, рассмотрен-ной в главе 7, за исключением того, что вместо возвращенияуказателя
файла она возвращает дескриптор файла, который яв-ляется просто целым типа
INT. INT FD;FD=OPEN(NAME,RWMODE);Как и в случае FOPEN, аргумент NAME является
символьнойстрокой, соответствующей внешнему имени файла. Однако аргу-мент, определяющий
режим доступа, отличен: RWMODE равно: 0 -для чтения, 1 - для записи,
2 - для чтения и записи. Еслипроисходит какая-то ошибка, функция OPEN возвращает
“-1”; впротивном случае она возвращает действительный дескрипторфайла.Попытка
открыть файл, который не существует, являетсяошибкой. Точка входа CREAT предоставляет
возможность созда-ния новых файлов или перезаписи старых. В результате
обраще-ния FD=CREAT(NAME,PMODE);возвращает дескриптор файла, если оказалось возможным
соз-дать файл с именем NAME, и “-1” в противном случае. Еслифайл с таким
именем уже существует, CREAT усечет его до ну-левой длины; создание файла, который
уже существует, не яв-ляется ошибкой.Если файл является совершенно новым,
то CREAT создаетего с определенным режимом защиты, специфицируемым аргумен-том
PMODE. В системе файлов на UNIX с файлом связываются де-вять битов защиты информации,
которые управляют разрешениемна чтение, запись и выполнение для владельца
файла, длягруппы владельцев и для всех остальных пользователей. Такимобразом,
трехзначное восьмеричное число наиболее удобно дляспецификации разрешений. Например,
число 0755 свидетельству-ет о разрешении на чтение, запись и выполнение
для владельцаи о разрешении на чтение и выполнение для группы и всех ос-тальных.Для
иллюстрации ниже приводится программа копированияодного файла в другой, являющаяся
упрощенным вариантом ути-литы CP системы UNIX. (Основное упрощение заключается
в том,что наш вариант копирует только один файл и что второй аргу-мент
не должен быть справочником). #DEFINE NULL 0#DEFINE BUFSIZE 512#DEFINE PMODE
0644/*RW FOR OWNER,R FOR GROUP,OTHERS*/MAIN(ARGC,ARGV) /*CP: COPY F1 TO F2*/INT
ARGC;CHAR *ARGV[]; \(INT F1, F2, N;CHAR BUF[BUFSIZE];* 171 - IF (ARGC ! = 3)ERROR(“USAGE:CP
FROM TO”, NULL);IF ((F1=OPEN(ARGV[1],0))== -1)ERROR(“CP:CAN'T OPEN
%S”, ARGV[1]);IF ((F2=CREAT(ARGV[2],PMODE))== -1)ERROR(“CP: CAN'T CREATE %S”,
ARGV[2]);WHILE ((N=READ(F1,BUF,BUFSIZE))>0)IF (WRITE(F2,BUF,N) !=N)ERROR(“CP:
WRITE ERROR”, NULL);EXIT(0); \)ERROR(S1,S2) /*PRINT ERROR MESSAGE AND DIE*/CHAR
*S1, S2; \(PRINTF(S1,S2);PRINTF(“\N”);EXIT(1); \) Существует ограничение (обычно
15 - 25) на количествофайлов, которые программа может иметь открытыми одновремен-но.
В соответствии с этим любая программа, собирающаяся ра-ботать со многими
файлами, должна быть подготовлена к пов-торному использованию дескрипторов файлов.
Процедура CLOSEпрерывает связь между дескриптором файла и открытым файлом
иосвобождает дескриптор файла для использования с некоторымдругим файлом. Завершение
выполнения программы через EXITили в результате возврата из ведущей программы
приводит кзакрытию всех открытых файлов.Функция расцепления UNLINK (FILENAME)
удаляет из системыфайлов файл с именем FILENAME ( из данного справочного
фай-ла. Файл может быть сцеплен с другим справочником, возможно,под другим именем
- примеч.переводчика).Упражнение 8-1.Перепишите программу CAT из главы 7, используя
функцииREAD, WRITE, OPEN и CLOSE вместо их эквивалентов из стандар-тной
библиотеки. Проведите эксперименты для определения от-носительной скорости работы
этих двух вариантов. 8.4. Произвольный доступ - SEEK и LSEEK.Нормально при
работе с файлами ввод и вывод осуществля-ется последовательно: при каждом обращении
к функциям READ иWRITE чтение или запись начинаются с позиции, непосредствен-но
следующей за предыдущей обработанной. Но при необходимос-ти файл может читаться
или записываться в любом произвольномпорядке. Обращение к системе с помощью
функции LSEEK позво-ляет передвигаться по файлу, не производя фактического
чте-ния или записи. В результате обращения LSEEK(FD,OFFSET,ORIGIN);* 172 - текущая
позиция в файле с дескриптором FD передвигается напозицию OFFSET (смещение),
которая отсчитывается от места,указываемого аргументом ORIGIN (начало отсчета).
Последующеечтение или запись будут теперь начинаться с этой позиции.Аргумент
OFFSET имеет тип LONG; FD и ORIGIN имеют тип INT.Аргумент ORIGIN может принимать
значения 0,1 или 2, указываяна то, что величина OFFSET должна отсчитываться
соответст-венно от начала файла, от текущей позиции или от конца фай-ла. Например,
чтобы дополнить файл, следует перед записьюнайти его конец: LSEEK(FD,0L,2);чтобы
вернуться к началу (“перемотать обратно”), можно напи-сать: LSEEK(FD,0L,0);обратите
внимание на аргумент 0L; его можно было бы записатьи в виде (LONG) 0.Функция
LSEEK позволяет обращаться с файлами примернотак же, как с большими массивами,
правда ценой более медлен-ного доступа. следующая простая функция, например,
считываетлюбое количество байтов, начиная с произвольного места вфайле.
GET(FD,POS,BUF,N) /*READ N BYTES FROM POSITION POS*/INT FD, N;LONG POS;CHAR *BUF;
\(LSEEK(FD,POS,0); /*GET TO POS*/RETURN(READ(FD,BUF,N)); \) В более ранних редакциях,
чем редакция 7 системы UNIX,основная точка входа в систему ввода-вывода
называется SEEK.Функция SEEK идентична функции LSEEK, за исключением того,что
аргумент OFFSET имеет тип INT, а не LONG. в соответствиис этим, поскольку на
PDP-11 целые имеют только 16 битов, ар-гумент OFFSET, указываемый функции SEEK,
ограничен величиной65535; по этой причине аргумент ORIGIN может иметь значения3,
4, 5, которые заставляют функцию SEEK умножить заданноезначение OFFSET на 512
(количество байтов в одном физическомблоке) и затем интерпретировать ORIGIN,
как если это 0, 1или 2 соответственно. Следовательно, чтобы достичь произ-вольного
места в большом файле, нужно два обращения к SEEK:сначала одно, которое выделяет
нужный блок, а затем второе,где ORIGIN имеет значение 1 и которое осуществляет
передви-жение на желаемый байт внутри блока.Упражнение 8-2.Очевидно, что
SEEK может быть написана в терминалахLSEEK и наоборот. напишите каждую функцию
через другую.* 173 - 8.5. Пример - реализация функций FOPEN и GETC.Давайте теперь
на примере реализации функций FOPEN иGETC из стандартной библиотеки подпрограмм
продемонстрируем,как некоторые из описанных элементов объединяются вместе.Напомним,
что в стандартной библиотеке файлы описыватсяпосредством указателей файлов,
а не дескрипторов. Указательфайла является указателем на структуру, которая
содержитнесколько элементов информации о файле: указатель буфера,чтобы файл
мог читаться большими порциями; счетчик числасимволов, оставшихся в буфере; указатель
следующей позициисимвола в буфере; некоторые признаки, указывающие режим
чте-ния или записи и т.д.; дескриптор файла.Описывающая файл структура данных
содержится в файлеSTDIO.H, который должен включаться (посредством #INCLUDE) влюбой
исходный файл, в котором используются функции из стан-дартной библиотеки. Он
также включается функциями этой биб-лиотеки. В приводимой ниже выдержке из файла
STDIO.H имена,предназначаемые только для использования функциями библиоте-ки,
начинаются с подчеркивания, с тем чтобы уменьшить веро-ятность совпадения с
именами в программе пользователя. DEFINE _BUFSIZE 512DEFINE _NFILE 20 /*FILES THAT
CAN BE HANDLED*/TYPEDEF STRUCT _IOBUF \( CHAR *_PTR; /*NEXT CHARACTER POSITION*/
INT _CNT; /*NUMBER OF CHARACTERS LEFT*/CHAR *_BASE; /*LOCATION OF BUFFER*/
INT _FLAG; /*MODE OF FILE ACCESS*/ INT _FD; /*FILE DESCRIPTOR*/) FILE;XTERN FILE
_IOB[_NFILE]; DEFINE STDIN (_IOB[0])
DEFINE STDOUT (_IOB[1])
DEFINE STDERR
(_IOB[2])
DEFINE _READ 01 /* FILE OPEN FOR READING */ DEFINE _WRITE 02 /* FILE
OPEN FOR WRITING */ DEFINE _UNBUF 04 /* FILE IS UNBUFFERED */ DEFINE _BIGBUF 010
/* BIG BUFFER ALLOCATED */ DEFINE _EOF 020 /* EOF HAS OCCURRED ON THIS FILE */
DEFINE _ERR 040 /* ERROR HAS OCCURRED ON THIS FILE */ DEFINE NULL 0 DEFINE EOF
(-1) DEFINE GETC(P) (--(P)->_CNT >= 0 \? *(P)->_PTR++ 0377 : _FILEBUF(P)) DEFINE
GETCHAR() GETC(STDIN) DEFINE PUTC(X,P) (--(P)->_CNT >= 0 \? *(P)->_PTR++ =
(X) : _FLUSHBUF((X),P))DEFINE PUTCHAR(X) PUTC(X,STDOUT)* 174 - В нормальном состоянии
макрос GETC просто уменьшаетсчетчик, передвигает указатель и возвращает
символ. (Еслиопределение #DEFINE слишком длинное, то оно продолжается спомощью
обратной косой черты). Если однако счетчик становит-ся отрицательным, то GETC вызывает
функцию _FILEBUF, котораяснова заполняет буфер, реинициализирует содержимое
структурыи возвращает символ. Функция может предоставлять переносимыйинтерфейс
и в то же время содержать непереносимые конструк-ции: GETC маскирует символ
числом 0377, которое подавляетзнаковое расширение, осуществляемое на PDP-11,
и тем самымгарантирует положительность всех символов.Хотя мы не собираемся обсуждать
какие-либо детали, мывсе же включили сюда определение макроса PUTC, для того
что-бы показать, что она работает в основном точно также, как иGETC, обращаясь
при заполнении буфера к функции _FLUSHBUF.Теперь может быть написана функция
FOPEN. Большая частьпрограммы функции FOPEN связана с открыванием файла и распо-ложением
его в нужном месте, а также с установлением битовпризнаков таким образом,
чтобы они указывали нужное состоя-ние. Функция FOPEN не выделяет какой-либо
буферной памяти;это делается функцией _FILEBUF при первом чтении из файла.
#INCLUDE #DEFINE PMODE 0644 /*R/W FOR OWNER;R FOR OTHERS*/FILE *FOPEN(NAME,MODE)
/*OPEN FILE,RETURN FILE PTR*/REGISTER CHAR *NAME, *MODE; \(REGISTER INT FD;REGISTER
FILE *FP;IF(*MODE !='R'&*MODE!='W'&*MODE!='A') \(FPRINTF(STDERR,”ILLEGAL
MODE %S OPENING %S\N”,MODE,NAME);EXIT(1); \)FOR (FP=_IOB;FP_FLAG (_READ \! _WRITE))==0)BREAK;
/*FOUND FREE SLOT*/IF(FP>=_IOB+_NFILE) /*NO FREE SLOTS*/RETURN(NULL);IF(*MODE=='W')
/*ACCESS FILE*/FD=CREAT(NAME,PMODE);ELSE IF(*MODE=='A') \(IF((FD=OPEN(NAME,1))==-1)FD=CREAT(NAME,PMODE);LSEEK(FD,OL,2);\)
ELSEFD=OPEN(NAME,0);IF(FD==-1)
/*COULDN'T ACCESS NAME*/RETURN(NULL);FP->_FD=FD;FP->_CNT=0;FP->_BASE=NULL;FP->_FLAG
=(_READ\! _WRITE);FP->_FLAG \!=(*MODE=='R') ? _READ : _WRITE;RETURN(FP);
\) * 175 - Функция _FILEBUF несколько более сложная. Основная труд-ность
заключается в том, что _FILEBUF стремится разрешитьдоступ к файлу и в
том случае, когда может не оказаться дос-таточно места в памяти для буферизации
ввода или вывода. ес-ли пространство для нового буфера может быть получено обра-щением
к функции CALLOC, то все отлично; если же нет, то_FILEBUF осуществляет
небуферизованный ввод/ вывод, исполь-зуя отдельный символ, помещенный в локальном
массиве. #INCLUDE _FILLBUF(FP) /*ALLOCATE AND FILL INPUT BUFFER*/REGISTER FILE
*FP; (STATIC CHAR SMALLBUF(NFILE);/*FOR UNBUFFERED 1/0*/CHAR *CALLOC();IF((FR->_FLAG&READ)==0\!\!(FP->_FLAG&EOF\!_ERR))\!=0RETURN(EOF);WHILE(FP->_BASE==NULL)
/*FIND BUFFER SPACE*/IF(FP->_FLAG _UNBUF) /*UNBUFFERED*/FP->_BASE=&MALLBUF[FP->_FD];ELSE
IF((FP->_BASE=CALLOC(_BUFSIZE,1))==NULL)FP->_FLAG \!=_UNBUF; /*CAN'T
GET BIG BUF*/ELSEFP->_FLAG \!=_BIGBUF; /*GOT BIG ONE*/FP->_PTR=FP->_BASE;FP->_CNT=READ(FP->_FD,
FP->_PTR,FP->_FLAG _UNBUF ? 1 : _BUFSIZE);FF(--FP->_CNT_CNT==
-1)FP->_FLAG \! = _EOF;ELSEFP->_FLAG \! = _ ERR;FP->_CNT = 0;RETURN(EOF); \)RETURN(*FP->_PTR++
0377); /*MAKE CHAR POSITIVE*/ ) При первом обращении к GETC
для конкретного файла счетчикоказывается равным нулю, что приводит к обращению
к_FILEBUF. Если функция _FILEBUF найдет, что этот файл не от-крыт для чтения, она
немедленно возвращает EOF. В противномслучае она пытается выделить большой буфер,
а если ей это неудается, то буфер из одного символа. При этом она заносит
в_FLAG соответствующую информацию о буферизации.Раз буфер уже создан, функция
_FILEBUF просто вызываетфункцию READ для его заполнения, устанавливает счетчик
иуказатели и возвращает символ из начала буфера.Единственный оставшийся невыясненным
вопрос состоит втом, как все начинается. Массив _IOB должен быть определен
иинициализирован для STDIN, STDOUT и STDERR:* 176 - FILE _IOB[NFILE] = \((NULL,0,_READ,0),
/*STDIN*/(NULL,0,NULL,1), /*STDOUT*/(NULL,0,NULL,_WRITE \! _UNBUF,2)
/*STDERR*/ ); Из инициализации части _FLAG этого массива структур видно,что
файл STDIN предназначен для чтения, файл STDOUT - длязаписи и файл STDERR - для
записи без использования буфера.Упражнение 8-3.Перепишите функции FOPEN и _FILEBUF,
используя полявместо явных побитовых операций.Упражнение 8-4.Разработайте
и напишите функции _FLUSHBUF и FCLOSE.Упражнение 8-5.Стандартная библиотека содержит
функциюFSEEK(FP, OFFSET, ORIGIN)которая идентична функции LSEEK, исключая
то, что FP являет-ся указателем файла, а не дескриптором файла. НапишитеFSEEK.
Убедитесь, что ваша FSEEK правильно согласуется с бу-феризацией, сделанной для
других функций библиотеки. 8.6. Пример - распечатка справочниковИногда требуется
другой вид взаимодействия с системойфайлов - определение информации о файле,
а не того, что внем содержится. Примером может служить команда LS (“списоксправочника”)
системы UNIX. По этой команде распечатываютсяимена файлов из справочника
и, необязательно, другая инфор-мация, такая как размеры, разрешения и т.д.Поскольку,
по крайней мере, на системе UNIX справочникявляется просто файлом,
то в такой команде, как LS нет ниче-го особенного; она читает файл и выделяет нужные
части изнаходящейся там информации. Однако формат информации опреде-ляется
системой, так что LS должна знать, в каком виде всепредставляется в системе.Мы
это частично проиллюстрируем при написании программыFSIZE. Программа FSIZE представляет
собой специальную формуLS, которая печатает размеры всех файлов, указанных
в спискеее аргументов. Если один из файлов является справочником, тодля
обработки этого справочника программа FSIZE обращаетсясама к себе рекурсивно. если
же аргументы вообще отсутству-ют, то обрабатывается текущий справочник.Для
начала дадим краткий обзор структуры системы файлов.Справочник - это файл, который
содержит список имен файлов инекоторое указание о том, где они размещаются.
Фактическиэто указание является индексом для другой таблицы, которуюназывают “I
- узловой таблицей”. Для файла I-узел - это то, * 177 - где содержится вся информация
о файле, за исключением егоимени. Запись в справочнике состоит только
из двух элемен-тов: номера I-узла и имени файла. Точная спецификация посту-пает
при включении файла SYS/DIR.H, который содержит #DEFINE DIRSIZ 14 /*MAX LENGTH
OF FILE NAME*/STRUCT DIRECT /*STRUCTURE OF DIRECTORY ENTRY*/ \(INO_T_INO /*INODE
NUMBER*/CHAR &NAME[DIRSIZ]; /*FILE NAME*/ \); “Тип” INO_T - это определяемый
посредством TYPEDEF тип,который описывает индекс I-узловой таблицы. На PDP-11
UNIXэтим типом оказывается UNSIGNED, но это не тот сорт информа-ции, который помещают
внутрь программы: на разных системахэтот тип может быть различным. Поэтому
и следует использо-вать TYPEDEF. Полный набор “системных” типов находится вфайле
SYS/TUPES.H.Функция STAT берет имя файла и возвращает всю содержащу-юся в
I-ом узле информацию об этом файле (или -1, если име-ется ошибка). Таким образом,
в результате STRUCT STAT STBUF;CHAR *NAME;STAT(NAME,STBUF)структура STBUF наполняется
информацией из I-го узла о файле с именем NAME. Структура, описывающая
возвращаемую функцией STAT информацию, находится в файле SYS/STAT.H и выглядит
следующим образом:STRUCT STAT /*STRUCTURE RETURNED BY STAT*/ \( DEV_T ST_DEV;
/* DEVICE OF INODE */ INO_T ST_INO; /* INODE NUMBER */ SHORT ST_MODE /* MODE BITS
*/SHORT ST_NLINK; / *NUMBER OF LINKS TO FILE */ SHORT ST_UID; /* OWNER'S USER
ID */ SHORT ST_GID; /* OWNER'S GROUP ID */DEV_T ST_RDEV; /* FOR SPECIAL FILES
*/OFF_T ST_SIZE; /* FILE SIZE IN CHARACTERS */TIME_T ST_ATIME; /* TIME LAST ACCESSED
*/TIME_T ST_MTIME; /* TIME LAST MODIFIED */TIME_T ST_CTIME; /* TIME ORIGINALLY
CREATED */ \) Большая часть этой информации объясняется в комментариях.Элемент
ST.MODE содержит набор флагов, описывающих файл; дляудобства определения
флагов также находятся в файлеSYS/STAT.H.* 178 - #DEFINE S_IFMT 0160000 /* TYPE
OF FILE */ #DEFINE S_IFDIR 0040000 /* DIRECTORY */ #DEFINE S_IFCHR 0020000 /*
CHARACTER SPECIAL */ #DEFINE S_IFBLK 0060000 /* BLOCK SPECIAL */ #DEFINE S_IFREG
0100000 /* REGULAR */ #DEFINE S_ISUID 04000 /* SET USER ID ON EXECUTION */
#DEFINE S_ISGID 02000 /* SET GROUP ID ON EXECUTION */ #DEFINE S_ISVTX 01000 /*SAVE
SWAPPED TEXT AFTER USE*/ #DEFINE S_IREAD 0400 /* READ PERMISSION */ #DEFINE
S_IWRITE 0200 /* WRITE PERMISSION */ #DEFINE S_IEXEC 0100 /* EXECUTE PERMISSION
*/ Теперь мы в состоянии написать программу FSIZE. Если по-лученный от функции
STAT режим указывает, что файл не явля-ется справочником, то его размер уже под
рукой и может бытьнапечатан непосредственно. Если же он оказывается справочни-ком,
то мы должны обрабатывать этот справочник отдельно длякаждого файла; так
как справочник может в свою очередь со-держать подсправочники, этот процесс обработки
является ре-курсивным.Как обычно, ведущая программа главным образом имеет
делос командной строкой аргументов; она передает каждый аргументфункции FSIZE
в большой буфер.#INCLUDE #INCLUDE /*TYPEDEFS*/#INCLUDE /*DIRECTORY ENTRY STRUCTURE*/#INCLUDE
/*STRUCTURE RETURNED BY STAT*/#DEFINE BUFSIZE 256MAIN(ARGC,ARGV)
/*FSIZE:PRINT FILE SIZES*/CHAR *ARGV[]; \(CHAR BUF[BUFSIZE];IF(ARGC==1) \( /*DEFAULT:CURRENT
DIRECTORY*/ATRCPY(BUF,”.”);FSIZE(BUF);\) ELSEWHILE(--ARGC>0) \(STRCPY(BUF,*++ARGV);FSIZE(BUF);
\) \) Функция FSIZE печатает размер файла. Если
однако файлоказывается справочником, то FSIZE сначала вызывает функциюDIRECTORY
для обработки всех указанных в нем файлов. Обрати-те внимание на использование
имен флагов S_IFMT и _IFDIR изфайла STAT.H. * 179 - FSIZE(NAME) /*PRINT SIZE
FOR NAME*/CHAR *NAME; \(STRUCT STAT STBUF;IF(STAT(NAME,STBUF)==-1) \(FPRINTF(STDERR,”FSIZE:CAN'T
FIND %S\N”,NAME);RETURN; \)IF((STBUF.ST_MODE S_IFMT)==S_IFDIR)DIRECTORY(NAME);PRINTF(“%8LD
%S\N”,STBUF.ST_SIZE,NAME); \)Функция DIRECTORY является
самой сложной. Однако значи-тельная ее часть связана с созданием для обрабатываемого
вданный момент файла его полного имени, по которому можновосстановить
путь в дереве. DIRECTORY(NAME) /*FSIZE FOR ALL FILES IN NAME*/CHAR *NAME;
(STRUCT DIRECT DIRBUF;CHAR *NBP, *NEP;INT I, FD;NBP=NAME+STRLEN(NAME);*NBP++='/';
/*ADD SLASH TO DIRECTORY NAME*/IF(NBP+DIRSIZ+2>=NAME+BUFSIZE) /*NAME TOO LONG*/RETURN;IF((FD=OPEN(NAME,0))==
-1)RETURN;WHILE(READ(FD,(CHAR *)&IRBUF,SIZEOF(DIRBUF))>0)
\(IF(DIRBUF.D_INO==0) /*SLOT NOT IN USE*/CONTINUE;IF(STRCMP (DIRBUF.D_NAME,”.”)==0\!\!
STRCMP(DIRBUF.D_NAME,”..”)==0CONTINUE; /*SKIP SELF AND PARENT*/FOR
(I=0,NEP=NBP;IS.PTR; ; G=P, P=P->S.PTR) \(IF (P->S.SIZE>=NUNITS) \( /*BIG
ENOUGH*/IF (P->S.SIZE==NUNITS) /*EXACTLY*/G->S.PTR=P->S.PTR;ELSE \( /*ALLOCATE
TAIL END*/P->S.SIZE-=NUNITS;P+=P->S.SIZE;P->S.SIZE=NUNITS; \)ALLOCP=G;RETURN((CHAR
*)(P+1)); \)IF(P==ALLOCP) /*WRAPPED AROUND FREE LIST*/IF((P=MORECORE(NUNITS))==NULL)RETURN(NULL);
/*NONE LEFT*/ \) \) Переменная BASE используется для начала
работы. ЕслиALLOCP имеет значение NULL, как в случае первого обращения кALLOC,
то создается вырожденный свободный список: он состоитиз свободного блока
размера нуль и указателя на самого себя.В любом случае затем исследуется свободный
список. Поисксвободного блока подходящего размера начинается с того места(ALLOCP),
где был найден последний блок; такая стратегия по-могает сохранить однородность
диска. Если найден слишкомбольшой блок, то пользователю предлагается его
хвостоваячасть; это приводит к тому, что в заголовке исходного блоканужно изменить
только его размер. Во всех случаях возвращае-мый пользователю указатель
указывает на действительно сво-бодную область, лежащую на единицу дальше заголовка.
Обрати-те внимание на то, что функция ALLOC перед возвращением “P”преобразует
его в указатель на символы.Функция MORECORE получает память от операционной
систе-мы. Детали того, как это осуществляется, меняются, конечно,от системы к
системе. На системе UNIX точка входа SBRK(N)возвращает указатель на “N” дополнительных
байтов памя-ти.(указатель удволетворяет всем ограничениям на выравнива-ние).
Так как запрос к системе на выделение памяти являетсясравнительно дорогой
операцией, мы не хотим делать это прикаждом обращении к функции ALLOC. Поэтому
функция MORECOREокругляет затребованное число единиц до большего значения;этот
больший блок будет затем разделен так, как необходимо.Масштабирующая величина
является параметром, который можетбыть подобран в соответствии с необходимостью.
* 183 - #DEFINE NALLOC 128 /*#UNITS TO ALLOCATE AT ONCE*/STATIC HEADER *MORECORE(NU)
/*ASK SYSTEM FOR MEMORY*/UNSIGNED NU; \(CHAR *SBRK();REGISTER CHAR *CP;REGISTER
HEADER *UP;REGISTER INT RNU;RNU=NALLOC*((NU+NALLOC-1)/NALLOC);CP=SBRK(RNU*SIZEOF(HEADER));IF
((INT)CP==-1) /*NO SPACE AT ALL*/RETURN(NULL);UP=(HEADER
*)CP;UP->S.SIZE=RNU;FREE((CHAR *)(UP+1));RETURN(ALLOCP); \) Если больше не осталось
свободного пространства, то фун-кция SBRK возвращает “-1”, хотя NULL был
бы лучшим выбором.Для надежности сравнения “-1” должна быть преобразована ктипу
INT. Снова приходится многократно использовать явныепреобразования (перевод) типов,
чтобы обеспечить определен-ную независимость функций от деталей представления
указате-лей на различных машинах.И последнее - сама функция FREE. Начиная
с ALLOCP, онапросто просматривает свободный список в поиске места длявведения свободного
блока. Это место находится либо междудвумя существующими блоками, либо
в одном из концов списка.В любом случае, если освободившийся блок примыкает к
одномуиз соседних, смежные блоки объединяются. Следить нужно толь-ко затем, чтобы
указатели указывали на то, что нужно, и что-бы размеры были установлены правильно.
FREE(AP) /*PUT BLOCKE AP IN FREE LIST*/CHAR *AP; \(REGISTER HEADER *P,
*G;P=(HEADER*)AP-1; /*POINT TO HEADER*/FOR (G=ALLOCP; !(P>G &P>G->S.PTR);G=G->S.PTR)IF
(G>=G->S.PTR &(P>G \!\! PS.PTR))BREAK; /*AT ONE END OR OTHER*/IF (P+P->S.SIZE==G->S.PTR)\(/*JOIN
TO UPPER NBR*/P->S.SIZE += G->S.PTR->S.SIZE;P->S.PTR
= G->S.PTR->S.PTR;\) ELSEP->S.PTR = G->S.PTR;IF (G+G->S.SIZE==P) \( /*JOIN TO LOWER
NBR*/G->S.SIZE+=P->S.SIZE;G->S.PTR=P->S.PTR;\) ELSEG->S.PTR=P;ALLOCP = G;
\) * 184 - Хотя распределение памяти по своей сути зависит от ис-пользуемой машины,
приведенная выше программа показывает,как эту зависимость можно регулировать
и ограничить весьманебольшой частью программы. Использование TYPEDEF и UNIONпозволяет
справиться с выравниванием (при условии, что функ-ция SBRK обеспечивает
подходящий указатель). Переводы типоворганизуют выполнение явного преобразования
типов и дажесправляются с неудачно разработанным системным интерфейсом.И хотя
рассмотренные здесь подробности связаны с распределе-нием памяти, общий подход
равным образом применим и к другимситуациям.Упражнение 8-6.Функция из стандартной
библиотеки CALLOC(N,SIZE) возвра-щает указатель на “N” объектов размера
SIZE, причем соответ-ствующая память инициализируется на нуль. напишите программудля
CALLOC, используя функцию ALLOC либо в качестве образца,либо как функцию,
к которой происходит обращение.Упражнение 8-7.Функция ALLOC принимает затребованный
размер, не прове-ряя его правдоподобности; функция FREE полагает, что тотблок,
который она должна освободить, содержит правильноезначение в поле размера.
Усовершенствуйте эти процедуры,затратив больше усилий на проверку ошибок.Упражнение
8-8.Напишите функцию BFREE(P,N), которая включает произволь-ный блок “P”
из “N” символов в список свободных блоков, уп-равляемый функциями ALLOC и FREE.
С помощью функции BFREEпользователь может в любое время добавлять в свободный
спи-сок статический или внешний массив.* 185 - 9. Приложение А: справочное руководство
по языку 'C' 9.1. Введение Это руководство описывает язык 'с' для компьютеров
DECPDP-11, HONEYWELL 6000, IBM система/370 и INTERDATA 8/32.там, где есть
расхождения, мы сосредотачиваемся на версиидля PDP-11, стремясь в то же время
указать детали, которыезависят от реализации. За малым исключением, эти расхождениянепосредственно
обусловлены основными свойствами используе-мого аппаратного
оборудования; различные компиляторы обычновполне совместимы. 10. Лексические
соглашенияИмеется шесть классов лексем: идентификаторы, ключевыеслова, константы,
строки, операции и другие разделители.Пробелы, табуляции , новые строки и комментарии
(совместно,“пустые промежутки”), как описано ниже, игнорируются, за
ис-ключением тех случаев, когда они служат разделителями лек-сем. Необходим какой-то
пустой промежуток для разделенияидентификаторов, ключевых слов и констант,
которые в против-ном случае сольются.Если сделан разбор входного потока на лексемы
вплоть доданного символа, то в качестве следующей лексемы берется са-мая
длинная строка символов, которая еще может представлятьсобой лексему.10.1. КомментарииКомментарий
открывается символами /* и заканчиваетсясимволами /*. Комментарии
не вкладываются друг в друга.10.2. Идентификаторы (имена)Идентификатор -
это последовательность букв и цифр; пер-вый символ должен быть буквой. Подчеркивание
_ считаетсябуквой. Буквы нижнего и верхнего регистров различаются. зна-чащими
являются не более, чем первые восемь символов, хотяможно использовать и больше.
На внешние идентификаторы, ко-торые используются различными ассемблерами
и загрузчиками,накладыватся более жесткие ограничения: DEC PDP-11 7 символов, 2
регистра HONEYWELL 6000 6 символов, 1 регистр IBM 360/370 7 символов, 1 регистр
INTERDATA 8/32 8 символов, 2 регистра * 186 - 10.3. Ключевые слова Следующие
идентификаторы зарезервированы для использова-ния в качестве ключевых слов и не
могут использоваться инымобразом: INT EXTERN ELSE CHAR REGISTER FOR FLOAT TYPEDEF
DO DOUBLE STATIC WHILE STRUCT GOTO SWITCH UNION RETURN CASE LONG SIZEOF DEFAULT
SHORT BREAK ENTRY UNSIGNED CONTINUE *AUTO IF Ключевое слово ENTRY в настоящее
время не используется ка-ким-либо компилятором; оно зарезервировано для использованияв
будущем. В некоторых реализациях резервируется также словаFORTRAN и
ASM10.4. КонстантыИмеется несколько видов констант, которые перечислены ниже.В
пункте 10.6 резюмируются характеристики аппаратных сред-ств, которые влияют на
размеры.10.4.1. Целые константы Целая константа, состоящая из последовательности
цифр,считается восьмеричной, если она начинается с 0 (цифрануль), и десятичной
в противном случае. Цифры 8 и 9 имеютвосьмеричные значения 10 и 11 соответственно.
Последователь-ность цифр, которой предшествуют символы 0х (нуль, х-малень-кое)
или 0х (нуль х-большое), рассматривается как шестнадца-тиричное целое. Шестнадцатиричные
цифры включают буквы от а(маленькое) или а (большое) до F (маленькое)
или F (большое)со значениями от 10 до 15. Десятичная константа, величинакоторой
превышает наибольшее машинное целое со знаком, счи-тается длинной; восмеричная
или шестнадцатиричная константа,которое превышает наибольшее машинное
целое без знака, такжесчитается длинной.10.4.2. Явные длинные константыДесятичная,
восмеричная или шестнадцатиричная константа,за которой непосредственно следует
L (эль-маленькое) или L(эль-большое), является длинной константой. Как обсуждаетсяниже,
на некоторых машинах целые и длинные значения могутрассматриваться
как идентичные.10.4.3. Символьные константыСимвольная константа - это символ,
заключенный в одиноч-ные кавычки, как, например, 'X'. Значением символьной конс-танты
является численное значение этого символа в машинномпредставлении набора
символов. * 187 - Некоторые неграфические символы, одиночная кавычка ' иобратная
косая черта \ могут быть представлены в соответст-вии со следующей таблицей
условных последовательностей: новая строка NL/LF/ \N горизонтальная табуляция
HT \T символ возврата на одну позицию BS \B возврат каретки CR \R переход на новую
страницу FF \F обратная косая черта \ \\ одиночная кавычка ' \' комбинация битов
DDD \DDD Условная последовательность \DDD состоит из обратной ко-сой черты,
за которой следуют 1,2 или 3 восмеричных цифры,которые рассмативаются как задающие
значение желаемого сим-вола. Специальным случаем этой конструкции является
последо-вательность \0 (за нулем не следует цифра), которая опреде-ляет символ
NUL. если следующий за обратной косой чертойсимвол не совпадает с одним из указанных,
то обратная косаячерта игнорируется.10.4.4. Плавающие константыПлавающая
константа состоит из целой части, десятичнойточки, дробной части, буквы E (маленькая)
или E (большая) ицелой экспоненты с необязательным знаком. Как целая,
так идробная часть являются последовательностью цифр. Либо целая,либо дробная
часть (но не обе) может отсутствовать; либо де-сятичная точка, либо е (маленькая)
и экспонента (но не то идругое одновременно) может отсутствовать. Каждая плавающаяконстанта
считается имеющей двойную точность.10.5. СтрокиСтрока - это последовательность
символов, заключенная вдвойные кавычки, как, наприимер,”...”. Строка
имеет тип“массив массивов” и класс памяти STATIC (см. Пункт 4 ниже).Строка
инициализирована указанными в ней символами. Всестроки, даже идентично записанные,
считаются различными.Компилятор помещает в конец каждой строки нулевой байт
\0, стем чтобы просматривающая строку программа могла определитьее конец. Перед
стоящим внутри строки символом двойной ка-вычки “ должен быть поставлен символ
обратной косой черты \;кроме того, могут использоваться те же условия последова-тельности,
что и в символьных константах. И последнее, об-ратная косая черта
\, за которой непосредственно следуетсимвол новой строки, игнорируется. * 188
- 10.6. Характеристики аппаратных средствСледующая ниже таблица суммирует некоторые
свойства ап-паратного оборудования, которые меняются от машины к машине.Хотя
они и влияют на переносимость программ, на практике онипредставляют маленькую
проблему, чем это может казаться за-ранее.Таблица 1 DEC PDP-11 HONEYWELL IBM
370 INTERDATA 8/32 ASCII ASCII EBCDIC ASCII CHAR 8 BITS 9 BITS 8 BITS 8 BITS
INT 16 36 32 32 SHORT 16 36 16 16 LONG 32 36 32 32 FLOAT 32 36 32 32 DOUBLE 64
72 64 64 RANGE -38/+38 -38/+38 -76/+76 -76/+7611. Синтаксическая нотация В используемой
в этом руководстве синтаксической нотациисинтаксические категории выделяются
курсивом (прим. перев.:в настоящее время синтексические категории вместо
курсивомвыделяются подчеркиванием), а литерные слова и символы -жирным шрифтом.
Альтернативные категории перечисляются наотдельных строчках. Необязательный символ,
терминальный илинетерминальный, указывается индексом “необ”, так что \(
выражение--------- необ \) указывает на необязательное выражение, заключенное в
фигур-ных скобках. Синтаксис суммируется в пункте 18. 12. Что в имене тебе моем?Язык
“C” основывает интерпретацию идентификатора на двухпризнаках идентификатора:
его классе памяти и его типе.Класс памяти определяет место и время хранения
памяти, свя-занной с идентификатором; тип определяет смысл величин, на-ходящихся
в памяти, определенной под идентификатором.Имеются четыре класса памяти: автоматическая,
статичес-кая, внешняя и регистровая. Автоматические переменные явля-ются
локальными для каждого вызова блока и исчезают при вы-ходе из этого блока.
Статические переменные являются локаль-ными, но сохраняют свои значения для
следующего входа в блокдаже после того, как управление передается за пределы блока.Внешние
переменные существуют и сохраняют свои значения втечение выполнения
всей программы и могут использоваться длясвязи между функциями, в том числе и
между независимо ском-пилированными функциями. Регистровые переменные хранятся(ели
это возможно) в быстрых регистрах машины; подобно авто-матическим переменным
они являются локальными для каждогоблока и исчезают при выходе из этого блока.
* 189 - В языке “C” предусмотрено несколько основных типовобъектов:объекты,
написанные как символы (CHAR), достаточно вели-ки, чтобы хранить любой член из
соответствующего данной реа-лизации внутреннего набора символов, и если действительныйсимвол
из этого набора символов хранится в символьной пере-менной, то ее
значение эквивалентно целому коду этого симво-ла. В символьных переменных можно
хранить и другие величины,но реализация будет машинно-зависимой.Можно использовать
до трех размеров целых, описываемыхкак SHORT INT, INT и LONG INT. Длинные
целые занимают неменьше памяти, чем короткие, но в конкретной реализации мо-жет
оказаться, что либо короткие целые, либо длинные целые,либо те и другие будут
эквивалентны простым целым. “Простые”целые имеют естественный размер, предусматриваемый
архиитек-турой используемой машины; другие размеры вводятся для удво-летворения
специальных потребностей.Целые без знака, описываемые как UNSIGNED,
подчиняютсязаконам арифметики по модулю 2**N, где N - число битов в ихпредставлении.
(На PDP-11 длинные величины без знака не пре-дусмотрены).Плавающие одинарной
точности (FLOAT) и плавающие двойнойточности (DOUBLE) в некоторых реализациях
могут быть синони-мами.Поскольку объекты упомянутых выше типов могут быть ра-зумно
интерпретированы как числа, эти типы будут называтьсяарифметическими. типы
CHAR и INT всех размеров совместно бу-дут называться целочисленными. Типы FLOAT
и DOUBLE совместнобудут называться плавающими типами.Кроме основных арифметических
типов существует концепту-ально бесконечный класс производных типов, которые
образуют-ся из основных типов следующим образом:массивы объектов большинства
типов;функции, которые возвращают объекты заданного типа;указатели на объекты
данного типа;структуры, содержащие последовательность объектовразличных типов;объединения,
способные содержать один из несколькихобъектов различных типов.Вообще
говоря, эти методы построения объектов могут при-меняться рекурсивно.13. Объекты
и L-значенияОбъект является доступным обработке участком памяти;L-значение
- это выражение, ссылающееся на объект. Очевиднымпримером выражения L-значения
является идентификатор. Сущес-твуют операции, результатом которых являются
L-значения; ес-ли, например, E - выражение указанного типа, то *E являетсявыражением
L-значения, ссылающимся на объект E. Название“L-значение” происходит от выражения
присваивания E1=E2, вкотором левая часть должна быть выражением L-значения.
Припоследующем обсуждении каждой операции будет указываться,ожидает ли она
операндов L-значения и выдает ли она L-значе-ние. * 190 - 14. Преобразования
Ряд операций может в зависимости от своих операндов вы-зывать преобразование значение
операнда из одного типа вдругой. В этом разделе объясняются результаты,
которые сле-дует ожидать от таких преобразований. В п. 14.6 Подводятсяитоги преобразований,
требуемые большинством обычных опера-ций; эти сведения дополняются
необходимым образом при обсуж-дении каждой операции. 14.1. Символы и целыеСимвол
или короткое целое можно использовать всюду, гдеможно использовать целое. Во
всех случаях значение преобра-зуется к целому. Преобразование более короткого
целого к бо-лее длинному всегда сопровождается знаковым расширением; це-лые являются
величинами со знаком. Осуществляется или нетзнаковое расширение для символов,
зависит от используемоймашины, но гарантируется, что член стандартного набора
сим-волов неотрицателен. из всех машин, рассматриваемых в этомруководстве,
только PDP-11 осуществляет знаковое расширение.область значений символьных переменных
на PDP-11 меняется от* 128 до 127; символы из набора ASC11 имеют положительныезначения.
Символьная константа, заданная с помощью восьме-ричной условной
последовательности, подвергается знаковомурасширению и может оказаться отрицательной;
например, '\377'имеет значение -1.Когда более длинное целое преобразуется
в более короткоеили в CHAR, оно обрезается слева; лишние биты просто отбра-сываются.14.2. Типы
FLOAT и DOUBLEВся плавающая арифметика в “C” выполняется с двойнойточностью
каждый раз, когда объект типа FLOAT появляется ввыражении, он удлиняется
до DOUBLE посредством добавлениянулей в его дробную часть. когда объект
типа DOUBLE долженбыть преобразован к типу FLOAT, например, при присваивании,перед
усечением DOUBLE округляется до длины FLOAT.14.3. Плавающие и целочисленные
величиныПреобразование плавающих значений к целочисленному типуимеет тенденцию
быть до некоторой степени машинно-зависимым;в частности направление усечения
отрицательных чисел меняет-ся от машине к машине. Результат не определен, если
значениене помещается в предоставляемое пространство.Преобразование целочисленных
значений в плавающие выпол-няется без осложнений. Может произойти некоторая
потеря точ-ности, если для результата не содержится достаточного коли-чества
битов.14.4. Указатели и целыеЦелое или длинное целое может быть прибавлено к указате-лю
или вычтено из него; в этом случае первая величина преоб-разуется так,
как указывается в разделе описания операциисложения. * 191 - Два указателя на
объекты одинакового типа могут бытьвычтены; в этом случае результат преобразуется
к целому, какуказывается в разделе описания операции вычитания.14.5. Целое без
знакаВсякий раз, когда целое без знака объединяется с простымцелым, простое
целое преобразуется в целое без знака и ре-зультат оказывается целым без знака.
Значением является наи-меньшее целое без знака, соответствующее целому со знаком(по
модулю 2**размер слова). В двоичном дополнительном пред-ставлении это преобразование
является чисто умозрительным ине изменяет фактическую комбинацию битов.Когда
целое без знака преобразуется к типу LONG, значе-ние результата совпадает
со значением целого без знака. Та-ким образом, это преобразование сводится
к добавлению нулейслева.14.6. Арифметические преобразованияПодавляющее большинство
операций вызывает преобразованиеи определяет типы результата аналогичным образом.
Приводимаяниже схема в дальнейшем будет называться “обычными арифмети-ческими
преобразованиями”.Сначала любые операнды типа CHAR или SHORT преобразуются
вINT, а любые операнды типа FLOAT преобразуются в DOUBLE.Затем, если какой-либо
операнд имеет тип DOUBLE, то другойпреобразуется к типу DOUBLE, и это будет
типом результата.В противном случае, если какой-либо операнд имеет тип LONG,то
другой операнд преобразуется к типу LONG, и это и будеттипом результата.В противном
случае, если какой-либо операнд имеет типUNSIGNED, то другой операнд преобразуется
к типу UNSIGNED,и это будет типом результата.В противном случае оба операнда
будут иметь тип INT, и этобудет типом результата.15. ВыраженияСтаршинство
операций в выражениях совпадает с порядкомследования основных подразделов настоящего
раздела, начинаяс самого высокого уровня старшинства. Так, например, выраже-ниями,
указываемыми в качестве операндов операции +(п.15.4), Являются выражения,
определенные в п.п.15.1-15.3.Внутри каждого подраздела операции имеет одинаковое
старшин-ство. В каждом подразделе для описываемых там операций ука-зывается
их ассоциативность слева или справа. Старшинство иассоциативность всех операций
в выражениях резюмируются вграмматической сводке в п.18.В противном случае
порядок вычислений выражений не опре-делен. В частности, компилятор считает
себя в праве вычис-лять подвыражения в том порядке, который он находит наиболееэффективным,
даже если эти подвыражения приводят к побочнымэффектам. Порядок,
в котором происходят побочные эффекты, неспецифицируется. Выражения, включающие
коммутативные и ассо-циативные операции ( *,+,,!,^), могут быть переупорядоченыпроизвольным
образом даже при наличии круглых скобок; чтобывынудить определенный
порядок вычислений, в этом случае не-обходимо использовать явные промежуточные
переменные. * 192 - При вычислении выражений обработка переполнения и про-верка
при делении являются машинно-зависимыми. Все существу-ющие реализации языка
“C” игнорируют переполнение целых; об-работка ситуаций при делении на 0 и при
всех особых случаяхс плавающими числами меняется от машины к машине и обычновыполняется
с помощью библиотечной функции.15.1. Первичные выраженияПервичные выражения,
включающие ., ->, индексацию и об-ращения к функциям, группируются слева
направо.Первичное выражение:идентификаторконстантастрока(выражение)первичное-выражение
[выражение]первичное-выражение (список-выражений неопервичное-L-значение
. Идентификаторпервичное-выражение -> идентификаторсписок-выражений:выражениесписок-выражений,
выражение Идентификатор является первичным выражением при условии,
чтоон описан подходящим образом, как это обсуждается ниже. типидентификатора
определяется его описанием. Если, однако, ти-пом идентификатора является “массив
...”, то значением выра-жения, состоящего из этого идентификатора , является
указа-тель на первый объект в этом массиве, а типом выражения бу-дет “указатель
на ...”. Более того, идентификатор массива неявляется выражением L-значения.
подобным образом идентифика-тор, который описан как “функция, возвращающая
...”, за иск-лючением того случая, когда он используется в позиции именифункции
при обращении, преобразуется в “указатель на функ-цию, которая возвращает ...”.Константа
является первичным выражением. В зависимостиот ее формы типом константы
может быть INT, LONG или DOUBLE.Строка является первичным выражением. Исходным
ее типомявляется “массив символов”; но следуя тем же самым правилам,которые
приведены выше для идентификаторов, он модифицирует-ся в “указатель на символы”,
и результатом является указа-тель на первый символ строки. (имеется исключение
в некото-рых инициализаторах; см. П. 16.6.)Выражение в круглых скобках является
первичным выражени-ем, тип и значение которого идентичны типу и значению этоговыражения
без скобок. Наличие круглых скобок не влияет нато, является ли выражение
L-значением или нет. * 193 - Первичное выражение, за которым следует выражение
вквадратных скобках, является первичным выражением. Интуитив-но ясно, что
это выражение с индексом. Обычно первичное вы-ражение имеет тип “указатель на
...”, индексное выражениеимеет тип INT, а типом результата является “...”. ВыражениеE1[E2]
по определению идентично выражению * ((E1) + (E2)).Все, что необходимо
для понимания этой записи, содержится вэтом разделе; вопросы, связанные с
понятием идентификаторови операций * и + рассматриваются в п.п. 15.1, 15.2 И 15.4соответственно;
выводы суммируются ниже в п. 22.3.Обращение к функции является
первичным выражением, закоторым следует заключенный в круглые скобки возможно
пустойсписок выражений, разделенных запятыми, которые и представ-ляют собой фактические
аргументы функции. Первичное выраже-ние должно быть типа “функция, возвращающая
...”, а резуль-тат обращения к функции имеет тип “...”. Как указывается
ни-же, ранее не встречавщийся идентификатор, за которым непос-редственно следует
левая круглая скобка, считается описаннымпо контексту, как представляющий
функцию, возвращающую це-лое; следовательно чаще всего встречающийся случай функции,возвращающей
целое значение, не нуждается в описании.Перед обращением любые
фактические аргументы типа FLOATпреобразуются к типу DOUBLE, любые аргументы
типа CHAR илиSHORT преобразуются к типу INT, и, как обычно, имена масси-вов преобразуются
в указатели. Никакие другие преобразованияне выполняются автоматически;
в частности, не сравнивает ти-пы фактических аргументов с типами формальных
аргументов.Если преобразование необходимо, используйте явный переводтипа (CAST);
см. П.п. 15.2, 16.7.При подготовке к вызову функции делается копия каждогофактического
параметра; таким образом, все передачи аргумен-тов в языке “C” осуществляются
строго по значению. функцияможет изменять значения своих формальных
параметров, но этиизменения не влияют на значения фактических параметров. Сдругой
строны имеется возможность передавать указатель притаком условии, что функция
может изменять значение объекта,на который этот указатель указывает. Порядок
вычисления ар-гументов в языке не определен; обратите внимание на то, чторазличные
компиляторы вычисляют по разному.Допускаются рекурсивные обращения к любой
функции.Первичное выражение, за которым следует точка и иденти-фикатор, является
выражением. Первое выражение должно бытьL-значением, именующим структуру или
объединение, а иденти-фикатор должен быть именем члена структуры или объединения.Результатом
является L-значение, ссылающееся на поименован-ный член структуры
или объединения.Первичное выражение, за которым следует стрелка (состав-ленная
из знаков - и >) и идентификатор, является выражени-ем. первое выражение должно
быть указателем на структуру илиобъединение, а идентификатор должен именовать
член этойструктуры или объединения. Результатом является L-значение,ссылающееся
на поименованный член структуры или объединения,на который указывает указательное
выражение.Следовательно, выражение E1->MOS является тем же самым,что и выражение
(*E1).MOS. Структуры и объединения рассмат-риваются в п. 16.5. Приведенные
здесь правила использованияструктур и объединений не навязываются строго, для
того что-бы иметь возможность обойти механизм типов. См. П. 22.1. * 194 - 15.2. Унарные
операцииВыражение с унарными операциями группируется справо на-лево.Унарное-выражение:*
выражение L-значение* выражение! Выражение\^ выражение++ L-значение*
L-значениеL-значение ++L-значение—(имя-типа) выражениеSIZEOF выражениеSIZEOF
имя-типа Унарная операция * означает косвенную адресацию: выраже-ние должно
быть указателем, а результатом является L-значе-ние, ссылающееся на тот объект,
на который указывает выраже-ние. Если типом выражения является “указатель
на...”, то ти-пом результата будет “...”.Результатом унарной операции является
указатель наобъект, к которому ссылается L-значение. Если L-значениеимеет тип
“...”, то типом результата будет “указатель на ...”.Результатом унарной операции
- (минус) является ее опе-ранд, взятый с противоположным знаком. Для величины
типаUNSIGNED результат получается вычитанием ее значения из 2**N(два в степени
N), где N-число битов в INT. Унарной операции+ (плюс) не существует.Результатом
операции логического отрицания ! Является 1,если значение ее операнда равно 0,
и 0, если значение ееоперанда отлично от нуля. Результат имеет тип INT. Эта опе-рация
применима к любому арифметическому типу или указате-лям.Операция \^ дает
обратный код, или дополнение до едини-цы, своего операнда. Выполняются обычные
арифметические пре-образования. Операнд должен быть целочисленного типа.Объект,
на который ссылается операнд L-значения префикс-ной операции ++, увеличивается.
значением является новоезначение операнда, но это не L-значение. Выражение
++х экви-валентно х+=1. Информацию о преобразованиях смотри в разбореоперации
сложения (п. 15.4) и операции присваивания (п.15.14).Префиксная операция—аналогична
префиксной операции++, но приводит к уменьшению своего операнда L-значения.При
применении постфиксной операции ++ к L-значению ре-зультатом является значение
объекта, на который ссылаетсяL-значение. После того, как результат принят
к сведению,объект увеличивается точно таким же образом, как и в случаепрефиксной
операции ++. Результат имеет тот же тип, что ивыражение L-значения. * 195 -
При применении постфиксной операции—к L-значению ре-зультатом является значение
объекта, на который ссылаетсяL-значение. После того, как результат принят к сведению,объект
уменьшается точно таким же образом, как и в случаепрефиксной операции
--. Результат имеет тот же тип, что ивыражение L-значения.Заключенное в круглые
скобки имя типа данных,стоящее пе-ред выражением , вызывает преобразование
значения этого вы-ражения к указанному типу. Эта конструкция называется пере-вод
(CAST). Имена типов описываются в п. 16.7.Операция SIZEOF выдает размер своего
операнда в байтах.(Понятие байт в языке не определено, разве только как значе-ние
операции SIZEOF. Однако во всех существующих реализацияхбайтом является пространство,
необходимое для храненияобъекта типа CHAR). При применении к массиву
результатом яв-ляется полное число байтов в массиве. Размер определяется изописаний
объектов в выражении. Это выражение семантическиявляется целой константой
и может быть использовано в любомместе, где требуется константа. Основное применение
эта опе-рация находит при связях с процедурами, подобным распредели-телям
памяти, и в системах ввода- вывода.Операция SIZEOF может быть также применена
и к заключен-ному в круглые скобки имени типа. В этом случае она выдаетразмер
в байтах объекта указанного типа.Конструкция SIZEOF (тип) рассматривается как
целое, такчто выражение SIZEOF (тип) - 2 эквивалентно выражению(SIZEOF (тип)9
- 2.15.3. Мультипликативные операцииМультипликативные операции *, /, и % группируются
слеванаправо. Выполняются обычные арифметические преобразования.Мультипликативное-выражение:выражение
* выражениевыражение / выражениевыражение % выражениеБинарная
операция * означает умножение. Операция * ассо-циативна, и выражения
с несколькими умножениями на одном итом же уровне могут быть перегруппированы
компилятором.Бинарная операция / означает деление. При делении поло-жительных
целых осуществляется усечение по направлению к ну-лю, но если один из операндов
отрицателен, то форма усечениязависит от используемой машины. На всех машинах,
охватывае-мых настоящим руководством, остаток имеет тот же знак , чтои делимое.
Всегда справедливо, что (A/B)*B+A%B равно A (еслиB не равно 0).Бинарная операция
% выдает остаток от деления первоговыражения на второе. Выполняются обычные
арифметические пре-образования. Операнды не должны быть типа FLOAT.15.4. Аддитивные
операцииАддитивные операции + и - группируются слева направо.выполняются
обычные арифметические преобразования. Для каж-дой операции имеются некоторые
дополнительные возможности,связанные с типами операндов. * 196 - Аддитивное-выражение:выражение
+ выражениевыражение - выражение Результатом операции + является
сумма операндов. Можно скла-дывать указатель на объект в массиве и значение
любого цело-численного типа. во всех случаях последнее преобразуется вадресное
смещение посредством умножения его на длину объек-та, на который указывает этот
указатель. Результатом являет-ся указатель того же самого типа, что и исходный
указатель,который указывает на другой объект в том же массиве, смещен-ный соответствующим
образом относительно первоначальногообъекта. Таким образом, если P
является указателем объекта вмассиве, то выражение P+1 является указателем на следующийобъект
в этом массиве.Никакие другие комбинации типов для указателей не
разре-шаются.Операция + ассоциативна, и выражение с несколькими сло-жениями на
том же самом уровне могут быть переупорядоченыкомпилятором.Результатом операции
- является разность операндов. Вы-полняются обычные арифметические преобразования.
Кроме того,из указателя может быть вычтено значение любого целочислен-ного
типа, причем, проводятся те же самые преобразования,что и при операции сложения.Если
вычитаются два указателя на объекты одинакового ти-па, то результат преобразуется
(делением на длину объекта) ктипу INT, представляя собой число объектов,
разделяющих ука-зываемые объекты. Если эти указатели не на объекты из одногои
того же массива, то такое преобразование, вообще говоря,даст неожиданные результаты,
потому что даже указатели наобъекты одинакового типа не обязаны отличаться
на величину,кратную длине объекта.15.5. Операции сдвигаОперации сдвига >
группируются слева направо. Дляобеих операций проводятся обычные арифметические
преобразо-вания их операндов, каждый из которых должен быть целочис-ленного типа.
Затем правый операнд преобразуется к типу INT;результат имеет тип левого операнда.
Результат не определен,если правый операнд отрицателен или больше или равен,
чемдлина объекта в битах.Выражение-сдвига:выражение > выражение Значением
выражения E1>E2 яв-ляется E1, сдвинутое вправо на E2 битовых позиций. Если E1имеет
тип UNSIGNE, то сдвиг вправо гарантированно будет ло-гическим (заполнение
нулем); в противном случае сдвиг можетбыть (и так и есть на PDP-11) арифметическим
(освобождающие-ся биты заполняются копией знакового бита). * 197 - 15.6. Операции
отношенияОперации отношения группируются слева направо, но этотфакт не очень
полезен; выражение A выражениевыражение = выражение Операции (больше), =(больше
или равно) все дают 0, если указанное отношение лож-но, и 1, если оно истинно.
Результат имеет тип ITN. Выполня-ются обычные арифметические преобразования.
Могут сравни-ваться два указателя; результат зависит от относительногорасположения
указываемых объектов в адресном пространстве.Сравнение указателей переносимо
только в том случае, еслиуказатели указывают на объекты из одного и того
же массива.15.7. Операции равенства Выражение-равенства:выражение == выражениевыражение
!= выражение Операции == (равно) и != (не равно) в точности аналогичныоперациям
отношения, за исключением того, что они имеют бо-лее низкий уровень старшинства.
(Поэтому значение выраженияA>= выражениеL-значение COUNTссылается к
полю COUNT структуры, на которую указывает SP;выражение S.LEFTссылается на указатель
левого поддерева в структуре S, а вы-ражение S.RIGHT->TWORD[0]ссылается
на первый символ члена TWORD правого поддерева изS. * 207 -16.6. ИнициализацияОписатель
может указывать начальное значение описываемо-го идентификатора. Инициализатор
состоит из выражения илизаключенного в фигурные скобки списка значений,
перед кото-рыми ставится знак =. Инициализатор:= выражение= \(список-инициализатора\)=
\(список-инициализатора,\)список-инициализатора:выражениесписок-инициализатора,список-инициализатора\(список-инициализатора\)
Все выражения, входящие
в инициализатор статической иливнешней переменной, должны быть либо константными
выражения-ми, описываемыми в п. 23, Либо выражениями, которые сводятсяк адресу
ранее описанной переменной, возможно смещенному наконстантное выражение. Автоматические
и регистровые перемен-ные могут быть инициализированы произвольными
выражениями,включающими константы и ранее описанные переменные и функ-ции.Гарантируется,
что неинициализированные статические ивнешние переменные получают в
качестве начальных значений0;неинициализированные автоматические и регистровые
перемен-ные в качестве начальных значений содержат мусор.Когда инициализатор применяется
к скаляру (указателю илиобъекту арифметического типа), то он состоит
из одного выра-жения, возможно заключенного в фигурные скобки. Начальноезначение
объекта находится из выражения; выполняются те жесамые преобразования, что и
при присваивании.Когда описываемая переменная является агрегатом (струк-турой
или массивом ), то инициализатор состоит из заключен-ного в фигурные скобки и разделенного
запятыми списка иници-ализаторов для членов агрегата. Этот список составляется
впорядке возрастания индекса или в соответствии с порядкомчленов. Если
агрегат содержит подагрегаты, то это правилоприменяется рекурсивно к членам
агрегата. Если количествоинициализаторов в списке оказывается меньше числа членов
аг-регата, то оставшиеся члены агрегата заполняются нулями.Запрещается инициализировать
объединения или автоматическиеагрегаты.Фигурные скобки могут быть
опущены следующим образом.Если инициализатор начинается с левой фигурной скобки,
топоследующий разделенный запятыми список инициализаторов ини-циализирует члены
агрегата; будет ошибкой, если в спискеокажется больше инициализаторов, чем членов
агрегата. Еслиоднако инициализатор не начинается с левой фигурной скобки,то
из списка берется только нужное для членов данного агре-гата число элементов;
оставшиеся элементы используются дляинициализации следующего члена агрегата,
частью которого яв-ляется настоящий агрегат. * 208 - Последнее сокращение допускает
возможность инициализациимассива типа CHAR с помощью строки. В этом случае
члены мас-сива последовательно инициализируются символами строки.Например,INT
X[] = \(1,3,5\);описывает и инициализирует X как одномерный массив; посколь-ку
размер массива не специфицирован, а список инициализиторасодержит три элемента,
считается, что массив состоит из трехчленов.Вот пример инициализации с полным
использованием фигур-ных скобок:FLOAT *Y[4][3] = \(( 1, 3, 5 ),( 2, 4, 6 ),( 3,
5, 7 ), \); Здесь 1, 3 и 5 инициализируют первую строку массива Y[0], аименно
Y[0][0], Y[0][1] и Y[0][2]. Аналогичным образом сле-дующие две строчки инициализируют
Y[1] и Y[2]. Инициализаторзаканчивается преждевременно, и, следовательно
массив Y[3]инициализируется нулями. В точности такого же эффекта можнобыло бы
достичь, написав FLOAT Y[4][3] = \(1, 3, 5, 2, 4, 6, 3, 5, 7 \); Инициализатор
для Y начинается с левой фигурной скобки, ноинициализатора для Y[0] нет. Поэтому
используется 3 элементаиз списка. Аналогично следующие три элемента используютсяпоследовательно
для Y[1] и Y[2]. следующее описание FLOAT Y[4][3] = \((1), (2),
(3), (4) \); инициализирует первый столбец Y (если его рассматривать какдвумерный
массив), а остальные элементы заполняются нулями.И наконец, описаниеCHAR
MSG[] = “SYNTAX ERROR ON LINE %S\N”;демонстрирует инициализацию элементов символьного
массива спомощью строки.16.7. Имена типовВ двух случаях (для явного указания
типа преобразованияв конструкции перевода и для аргументов операции SIZEOF)
же-лательно иметь возможность задавать имя типа данных. Этоосуществляется с
помощью “имени типа”, которое по существуявляется описанием объекта такого типа
, в котором опущеноимя самого объекта. * 209 - Имя типа:спецификатор-типа абстрактный-описательабстрактный-описатель:пусто(абстрактный-описатель)*
абстрактный
описатель абстрактный-описатель () абстрактный-описатель [константное выражениенеоб]Во
избежании двусмысленности в конструкции(абстрактный описатель)требуется,
чтобы абстрактный-описатель был непуст. При этомограничении возможно однозначено
определить то место в абст-рактном-описателе, где бы появился идентификатор,
если быэта конструкция была описателем в описании. Именованный типсовпадает
тогда с типом гипотетического идентификатора. Нап-ример, имена типов INTINT *INT
*[3]INT (*)[3]INT *()INT (*)()именуют соответственно типы “целый”, “указатель
на целое”,“массив из трех указателей на целое”, “указатель на массивиз трех целых”,
“ функция, возвращающая указатель на целое”и “указатель на функцию, возвращающую
целое”.16.8. TYPEDEFОписания, в которых “класс памяти”специфицирован какTYPEDEF,
не вызывают выделения памяти. вместо этого они оп-ределяют идентификаторы
,которые позднее можно использоватьтак, словно они являются ключевыми словами,
имеющими основ-ные или производные типы.Определяющее-тип-имяидентификатор
В пределах области действия описания со спецификаторомTYPEDEF каждый идентификатор,
являющийся частью любого опи-сателя в этом описании, становится синтаксически
эквивалент-ным ключевому слову, имеющему тот тип , который ассоциируетс идентификатором
в описанном в п. 16.4 Смысле. Например,после описаний TYPEDEF INT
MILES, >KLICKSP;TYPEDEF STRUCT ( DOUBLE RE, IM; ) COMPLEX;конструкцииMILES DISTANCE;EXTERN
KLICKSP METRICP;COMPLEX Z, *ZP;* 210 - становятся законными описаниями;
при этом типом DISTANCE яв-ляется INT, типом METRICP - “указатель на INT”,
типом Z -специфицированная структура и типом ZP - указатель на такуюструктуру.Спецификатор
TYPEDEF не вводит каких-либо совершенно но-вых типов, а только определяет
синонимы для типов, которыеможно было бы специфицировать и другим способом.
Так в при-веденном выше примере переменная DISTANCE считается имеющейточно
такой же тип, что и любой другой объект, описанный вINT.17. ОператорыЗа исключением
особо оговариваемых случаев, операторывыполняются последовательно.17.1. Операторное
выражениеБольшинство операторов являются операторными выражения-ми,
которые имеют формувыражение;обычно операторные выражения являются присваиваниями
или об-ращениями к функциям.17.2. Составной оператор (или блок)С тем чтобы
допустить возможность использования несколь-ких операторов там, где ожидается присутствие
только одного,предусматривается составной оператор (который также и
экви-валентно называют “блоком”): составной оператор:\(список-описаний список-операторовнеоб
необ\)список-описаний:описаниеописание список-описанийсписок-операторов:оператороператор
список-операторов Если какой-либо идентификатор из списка-описаний
был описанранее, то во время выполнения блока внешнее описание подав-ляется
и снова вступает в силу после выхода из блока.Любая инициализация автоматических
и регистрационных пе-ременных проводится при каждом входе в блок через
его нача-ло. В настоящее время разрешается (но это плохая практика)передавать
управление внутрь блока; в таком случае эти ини-циализации не выполняются. Инициализации
статических пере-менных проводятся только один раз, когда начинается
выполне-ние программы. Находящиеся внутри блока внешние описания не резервируют
памяти, так что их инициализация неразрешается.* 211 - 17.3. Условные операторы
Имеются две формы условных операторов: IF (выражение) операторIF (выражение)
оператор ELSE оператор В обоих случаях вычасляется выражение и, если оно отлично
отнуля, то выполняется первый подоператор. Во втором случае,если выражение
равно нулю, выпалняется второй подоператор.Как обычно, двусмысленность “ELSE” разрешается
связываениемELSE с последним встречающимся IF, у которого нет ELSE.17.4. Оператор
WHILEОператор WHILE имеет формуWHILE (выражение) операторПодоператор
выполняется повторно до тех пор, пока значениевыражения остается отличным от
нуля. проверка производитсяперед каждым выполнением оператора.17.5. Оператор
DOОператор DO имеет формуDO оператор WHILE (выражения)Оператор выполняется повторно
до тех пор, пока значение выражения не станет равным нулю. Проверка производится
после каждого выполнения оператора.17.6. Оператор FORОператор FOR имеет
форму(выражение-1 ; выражение-2 ; выражение-3 )операторнеоб необ необОператор FOR
эквивалентен следующемувыражение-1;WHILE (выражение-2) \(операторвыражение-3
\) Таким образом, первое выражение определяет инициализациюцикла; второе специфиуирует
проверку, выполняемую перед каж-дой итерацией, так что выход из цикла
происходит тогда, ког-да значение выражения становится нулем; третье выражениечасто
задает приращение параметра, которое проводится послекаждой итерации.Любое
выражение или даже все они могут быть опущены. Ес-ли отсутствует второе выражение,
то предложение с WHILE счи-тается эквивалентным WHILE(1); другие отсутствующие
выраже-ния просто опускаются из приведенного выше расширения. * 212 - 17.7. Оператор
SWITCHОператор SWITCH (переключатель), вызывает передачу уп-равления
к одному из нескольких операторов, в зависимости отзначения выражения. Оператор
имеет форму SWITCH (выражение) операторВ выражении проводятся обычные арифметические
преобразова-ния, но результат должен иметь тип INT. Оператор обычно яв-ляется
составным. Любой оператор внутри этого оператора мо-жет быть помечен одним
или более вариантным префиксом CASE,имеющим форму: CASE констанстное выражение:где
константное выражение должно иметь тип INT. Никакие двевариантные константы
в одном и том же переключателе не могутиметь одинаковое значение. точное определение
константноговыражения приводится в п. 23.Кроме того, может присутствовать
самое большее один опе-раторный префикс видаDEFAULT:При выполнении оператора
SWITCH вычисляется входящее внего выражение и сравнивается с каждой вариантной
констан-той. Если одна из вариантных констант оказывается равнойзначению этого
выражения, то управление передается операто-ру, который следует за совпадающим
вариантным префиксом. Ес-ли ни одна из вариантных констант не совпадает со значениемвыражения
и если при этом присутствует префикс DEFAULT, тоуправление передается
оператору, помеченному этим префиксом.если ни один из вариантов не подходит
и префикс DEFAULT от-сутствует, то ни один из операторов в переключателе
не вы-полняется.Сами по себе префиксы CASE и DEFAULT не изменяют потокуправления,
которое беспрепятсвенно проходит через такиепрефиксы. Для выхода из переключателя
смотрите операторBREAK, п. 17.8.Обычно оператор, который входит в переключатель,
являет-ся составным. Описания могут появляться в начале этого опе-ратора,
но инициализации автоматических и регистровых пере-менных будут неэффективными.17.8. Оператор
BREAKОператорBREAK;вызывает завершение выполнения наименьшего
охватывающегоэтот оператор оператора WHILE, DO, FOR или SWITCH; управле-ние передается
оператору, следующему за завершенным операто-ром. * 213 - 17.9. Оператор
CONTINUE ОператорCONTINUE;приводит к передаче управления на продолжающую цикл
частьнаименьшего охватывающего этот оператор оператора WHILE, DOили FOR; то есть
на конец цикла. Более точно, в каждом изоператоров WHILE(...) \( DO \( FOR(...)
\( ... ... ... CONTIN: ; CONTIN: ; CONTIN: ; \) \) WHILE(...); \) Оператор
CONTINUE эквивалентен оператору GOTO CONTIN. (ЗаCONTIN: следует пустой оператор;
см. П. 17.13.).17.10. Оператор возвратаВозвращение из функции в вызывающую программу
осуществ-ляется с помощью оператора RETURN, который имеет одну изследующих
форм RETURN;RETURN выражение;В первом случае возвращаемое значение неопределено.
Во вто-ром случае в вызывающую функцию возвращается значение выра-жения.
Если требуется, выражение преобразуется к типу функ-ции, в которой оно появляется,
как в случае присваивания.Попадание на конец функции эквивалентно возврату
без возвра-щаемого значения.17.11. Оператор GOTOУправление можно передавать безусловно
с помощью опера-тораGOTO идентификатор1идентификатор должен быть меткой
(п. 9.12), Локализованной вданной функции.17.12. Помеченный операторПеред любым
оператором может стоять помеченный префиксвидаидентификатор:который служит для
описания идентификатора в качестве метки.Метки используются только для указания
места, куда передает-ся управление оператором GOTO. Областью действия метки
явля-ется данная функция, за исключением любых подблоков, в кото-рых тот же идентификатор
описан снова. Смотри п. 19. * 214 - 17.13. Пустой оператор Пустой оператор
имеет форму: ; Пустой оператор оказывается полезным, так как он позволяетпоставить
метку перед закрывающей скобкой \) составного опе-ратора или указать
пустое тело в операторах цикла, таких какWHILE.18. Внешние определенияC-программа
представляет собой последовательность внеш-них определений. Внешнее определение
описывает идентификаторкак имеющий класс памяти EXTERN (по умолчанию), или
возможноSTATIC, и специфицированный тип. Спецификатор типа (п. 16.2)Также может
быть пустым; в этом случае считается, что типявляется типом INT. Область действия
внешних определенийраспространяется до конца файла, в котором они приведены,точно
так же , как влияние описаний простирается до концаблока. Синтаксис внешних
определений не отличается от син-таксиса описаний, за исключением того, что
только на этомуровне можно приводить текст функций.18.1. Внешнее определение
функцииОпределение функции имеет формуопределение-функции:спецификаторы-описания
описатель-функциитело-функциинеобЕдинственными спецификаторами класса памяти,
допускаемыми вкачестве спецификаторов-описания, являются EXTERN илиSTATIC; о
различии между ними смотри п. 19.2. Описатель фун-кции подобен описателю для “функции,
возвращающей...”, заисключением того, что он перечисляет формальные параметрыопределяемой
функции. Оисатель-функции:описатель (список-параметровнеоб)список
параметров:идентификаторидентификатор, список-параметров тело-функции имеет
формутело-функции:список-описаний составной-оператор* 215 - Идентификаторы из
списка параметров и только они могутбыть описаны в списке описаний. Любой идентификатор,
тип ко-торого не указан, считается имеющим тип INT. Единственнымдопустимым
здесь спецификатором класса памяти являетсяREGISTER; если такой класс памяти
специфицирован, то в нача-ле выполнения функции соответствующий фактический
параметркопируется, если это возможно, в регистр.Вот простой пример полного
определения функции:INT MAX(A, B, C)INT A, B, C; \(INT M;M = (A>B) ? A:B;RETURN((M>C)
? M:C); \) Здесь INT - спецификатор-типа, MAX(A,B,C) - описатель-функ-ции,
INT A,B,C; - список-описаний формальных параметров, \(... \) - Блок, содержащий
текст оператора.В языке “C” все фактические параметры типа FLOAT преоб-разуются
к типу DOUBLE, так что описания формальных парамет-ров, объявленных как FLOAT,
приспособлены прочесть параметрытипа DOUBLE. Аналогично, поскольку ссылка
на массив в любомконтексте (в частности в фактическом параметре) рассматрива-ется
как указатель на первый элемент массива, описания фор-мальных параметров вила
“массив ...” приспособлены прочесть: “указатель на ...”. И наконец, поскольку
структуры,объединения и функции не могут быть переданы функции, бесс-мысленно
описывать формальный параметр как структуру,объединение или функцию (указатели
на такие объекты, конеч-но, допускаются).18.2. Внешние определения данных Внешнее
определение данных имеет формуопределение-данных:описаниеКлассом памяти таких
данных может быть EXTERN (в частности,по умолчанию) или STATIC, но не AUTO или
REGISTER.19. Правила, определяющие область действияВся C-программа необязательно
компилируется одновремен-но; исходный текст программы может храниться в несколькихфайлах
и ранее скомпилированные процедуры могут загружатьсяиз библиотек.
Связь между функциями может осуществляться какчерез явные обращения, так и в результате
манипулирования свнешними данными.Поэтому следует рассмотреть два вида
областей действия:во-первых, ту, которая может быть названа лексической об-ластью
действия идентификатора и которая по существу являет-ся той областью в программе,
где этот идентификатор можноиспользовать, не вызывая диагностического сообщения
“неопре-деленный идентификатор”; и во-вторых, область действия, ко-торая
связана с внешними идентификаторами и которая характе-ризуется правилом, что
ссылки на один и тот же внешний иден-тификатор являются ссылками на один и тот
же объект. * 216 - 19.1. Лексическая область действияЛексическая область действия
идентификаторов, описанныхво внешних определениях, простирается от определения
до кон-ца исходного файла, в котором он находится. Лексическая об-ласть действия
идентификаторов, являющихся формальными пара-метрами, распространяется на
ту функцию, к которой они отно-сятся. Лексическая область действия идентификаторов,
описан-ных в начале блока, простирается до конца этого блока. Лек-сической
областью действия меток является та функция, в ко-торой они находятся.Поскольку
все обращения на один и тот же внешний иденти-фикатор обращаются к одному и
тому же объекту (см. П. 19.2),Компилятор проверяет все описания одного и того же
внешнегоидентификатора на совместимость; в действительности их об-ласть действия
распространяется на весь файл, в котором онинаходятся.Во всех случаях, однако,
есть некоторый идентификатор,явным образом описан в начале блока, включая и
блок, которыйобразует функцию, то действие любого описания этого иденти-фикатора
вне блока приостанавливается до конца этого блока.Напомним также (п. 16.5),
Что идентификаторы, соответст-вующие обычным переменным, с одной стороны, и идентификато-ры,
соответствующие членам и ярлыкам структур и объединений,с другой
стороны, формируют два непересекающихся класса, ко-торые не вступают в противоречие.
Члены и ярлыки подчиняютсятем же самым правилам определения областей действия,
как идругие идентификаторы. Имена, специфицируемые с помощьюTYPEDEF, входят
в тот же класс, что и обычные идентификато-ры. Они могут быть переопределены
во внутренних блоках, ново внутреннем описании тип должен быть указан явно: TYPEDEF
FLOAT DISTANCE; ... \(AUTO INT DISTANCE; ... Во втором описании спецификатор
типа INT должен присутство-вать, так как в противном случае это описание будет
принятоза описание без описателей с типом DISTANCE (прим. Автора:согласитесь,
что лед здесь тонок.).19.2. Область действия внешних идентификаторовЕсли функция
ссылается на идентификатор, описанный какEXTERN, то где-то среди файлов или
библиотек, образующихполную программу, должно содержаться внешнее определениеэтого
идентификатора. Все функции данной программы, которыессылаются на один и
тот же внешний идентификатор, ссылаютсяна один и тот же объект, так что следует
позаботиться, чтобыспецифицированные в этом определении тип и размер были сов-местимы
с типом и размером, указываемыми в каждой функции,которая ссылается на
эти данные. * 217 - Появление ключевого слова EBTERN во внешнем определенииуказывает
на то, что память для описанных в нем идентифика-торов будет выделена в другом
файле. Следовательно, в состо-ящей из многих файлов программе внешнее определение
иденти-фикатора, не содержащее спецификатора EXTERN, должно появ-ляться
ровно в одном из этих файлов. любые другие файлы, ко-торые желают дать внешнее
определение этого идентификатора,должны включать в это определение слово EXTERN.
Идентифика-тор может быть инициализирован только в том описании, кото-рое приводит
к выделению памяти.Идентификаторы, внешнее определение которых начинаетсясо
слова STATIC, недоступны из других файлов. Функции могутбыть описаны как STATIC.20. Строки
управления компиляторомКомпилятор языка “C” содержит препроцессор,
который поз-воляет осуществлять макроподстановки, условную компиляцию ивключение
именованных файлов. Строки, начинающиеся с #, об-щаются с этим препроцессором.
Синтаксис этих строк не связанс остальным языком; они могут появляться в
любом месте и ихвлияние распространяется (независимо от области действия) доконца
исходного программного файла.20.1. Замена лексемУправляющая компилятором строка
вида#DEFINE идентификатор строка-лексем(Обратите внимание на отсутствие в
конце точки с запя-той) приводит к тому, что препроцессор заменяет последующиевхождения
этого идентификатора на указанную строку лексем.Строка вида #DEFINE идентификатор(идентификатор,...,идентификатор)строка
лексем где между первым идентификатором
и открывающейся скобкой (нет пробела, представляет собой макроопределение
с аргумен-тами. Последующее вхождение первого идентификатора, за кото-рым
следует открывающая скобка '(', последовательность раз-деленных запятыми лексем
и закрывающая скобка ')', заменяют-ся строкой лексем из определения. каждое
вхождение идентифи-катора, упомянутого в списке формальных параметров в опреде-лении
, заменяется соответствующей строкой лексем из обраще-ния. Фактическими аргументами
в обращении являются строкилексем, разделенные запятыми; однако запятые,
входящие в за-кавыченные строки или заключенные в круглые скобки, не раз-деляют
аргументов. Количество формальных и фактических пара-метров должно совпадать.
Текст внутри строки или символьнойконстанты не подлежит замене.В обоих случаях
замененная строка просматривается сновас целью обнаружения других определенных
идентификаторов. Вобоих случаях слишком длинная строка определения может бытьпродолжена
на другой строке, если поместить в конце продол-жаемой строки обратную
косую черту \ . * 218 - Описываемая возможность особенно полезна для определения“объявляемых
констант”, как, например,#DEFINE TABSIZE 100INT TABLE[TABSIZE];
Управляющая строка вида#UNDEF идентификаторприводит к отмене препроцессорного
определения данного иден-тификатора.20.2. Включение файловСтрока управления
компилятором вида#INCLUDE “FILENAME”приводит к замене этой строки на все содержимое
файла с име-нем FILENAME. Файл с этим именем сначала ищется в справочни-ке
начального исходного файла, а затем в последовательностистандартных мест. В отличие
от этого управляющая строка вида #INCLUDE ищет файл только в стандартных
местах и не просматриваетсправочник исходного файла.Строки #INCLUDE могут быть
вложенными.20.3. Условная компиляцияСтрока управления компилятором вида#IF константное
выражениепроверяет, отлично ли от нуля значение константного выраже-ния
(см. П. 15). Управляющая строка вида #IF DEF идентификаторпроверяет, определен
ли этот идентификатор в настоящий мо-мент в препроцессоре, т.е. Определен ли
этот идентификатор спомощью управляющей строки #DEFINE.21. Неявные описанияНе всегда
является необходимым специфицировать и класспамяти и тип идентификатора в
описании. Во внешних определе-ниях и описаниях формальных параметров и членов
структуркласс памяти определяется по контексту. Если в находящемсявнутри функции
описании не указан тип, а только класс памя-ти, то предполагается, что идентификатор
имеет тип INT; еслине указан класс памяти, а только тип, то идентификатор
пред-полагается описанным как AUTO. Исключение из последнего пра-вила дается
для функций, потому что спецификатор AUTO дляфункций является бессмысленным (язык
“C” не в состоянии ком-пилировать программу в стек); если идентификатор имеет
тип“функция, возвращающая ...”, то он предполагается неявноописанным как EXTERN.
* 219 - Входящий в выражение и неописанный ранее идентификатор,за которым
следует скобка ( , считается описанным по контек-сту как “функция, возвращающая
INT”.22. Снова о типахВ этом разделе обобщаются сведения об операциях, которыеможно
применять только к объектам определенных типов.22.1. Структуры и объединенияТолько
две вещи можно сделать со структурой или объеди-нением: назвать один
из их членов (с помощью операции) илиизвлечь их адрес ( с помощью унарной операции
).Другиеоперации, такие как присваивание им или из них и передача ихв качестве
параметров, приводят к сообщению об ошибке. В бу-дущем ожидается, что эти операции,
но не обязательно ка-кие-либо другие, будут разрешены.В п. 15.1 Говорится,
что при прямой или косвенной ссылкена структуру (с помощью . Или ->) имя справа
должно бытьчленом структуры, названной или указанной выражением слева.Это
ограничение не навязывается строго компилятором, чтобыдать возможность обойти
правила типов. В действительностиперед '.' допускается любое L-значение и затем
предполагает-ся, что это L-значение имеет форму структуры, для которойстоящее
справа имя является членом. Таким же образом, от вы-ражения, стоящего перед '->',
требуется только быть указате-лем или целым. В случае указателя предполагается,
что онуказывает на структуру, для которой стоящее справа имя явля-ется членом.
В случае целого оно рассматривается как абсо-лютный адрес соответствующей структуры,
заданный в единицахмашинной памяти.Такие структуры не являются переносимыми.22.2. ФункцииТолько
две вещи можно сделать с функцией: вызвать ее илиизвлечь
ее адрес. Если имя функции входит в выражение не впозиции имени функции, соответствующей
обращению к ней, тогенерируется указатель на эту функцию. Следовательно,
чтобыпередать одну функцию другой, можно написать INT F(); ...G(F);Тогда
определение функции G могло бы выглядеть так:G(FUNCP)INT(*FUNCP)(); \( ...(*FUNCP)();
... \) Обратите внимание, что в вызывающей процедуре функция F дол-жна
быть описана явно, потому что за ее появлением в G(F) неследует скобка ( . *
220 - 22.3. Массивы, указатели и индексацияКаждый раз, когда идентификатор, имеющий
тип массива,появляется в выражении, он преобразуется в указатель на пер-вый
член этого массива. Из-за этого преобразования массивыне являются L-значениями.
По определению операция индексация[] интерпретируется таким образом, что E1[E2]
считаетсяидентичным выражению *((е1)+(е2)). Согласно правилам преоб-разований,
применяемым при операции +, если E1 - массив, ае2 - целое, то е1[е2] ссылается
на е2-й член массива е1. По-этому несмотря на несимметричный вид операция индексации
яв-ляется коммутативной.В случае многомерных массивов применяется последователь-ное
правило. Если е является N-мерным массивом размераI*J*...*K, то
при появлении в выражении е преобразуется вуказатель на (N-1)-мерный массив размера
J*...*K. Если опе-рация * либо явно, либо неявно, как результат индексации,применяется
к этому указателю, то результатом операции будетуказанный (N-1)-мерный
массив, который сам немедленно преоб-разуется в указатель.Рассмотрим, например,
описаниеINT X[3][5];Здесь X массив целых размера 3*5. При появлении в выраженииX
преобразуется в указатель на первый из трех массивов из 5целых. В выражении
X[I], которое эквивалентно *(X+I), снача-ла X преобразуется в указатель так,
как описано выше; затемI преобразуется к типу X, что вызывает умножение I на
длинуобъекта, на который указывает указатель, а именно на 5 целыхобъектов. Результаты
складываются, и применение косвеннойадресации дает массив (из 5 целых),
который в свою очередьпреобразуется в указатель на первое из этих целых. Если
ввыражение входит и другой индекс, то таже самая аргументацияприменяется снова;
результатом на этот раз будет целое.Из всего этого следует, что массивы в языке
“C” хранятсяпострочно ( последний индекс изменяется быстрее всего) и чтопервый
индекс в описании помогает определить общее количест-во памяти, требуемое для
хранения массива, но не играет ни-какой другой роли в вычислениях, связанных с
индексацией.22.4. Явные преобразования указателейРазрешаются определенные преобразования,
с использовани-ем указателей , но они имеют некоторые зависящие от
конкрет-ной реализации аспекты. Все эти преобразования задаются спомощью операции
явного преобразования типа; см. П. 15.2 и16.7.Указатель может быть преобразован
в любой из целочислен-ных типов, достаточно большой для его хранения. Требуется
липри этом INT или LONG, зависит от конкретной машины. Преоб-разующая функция
также является машинно-зависимой, но онабудет вполне естественной для тех,
кто знает структуру адре-сации в машине. Детали для некоторых конкретных машин
приво-дятся ниже.Объект целочисленного типа может быть явным образом пре-образован
в указатель. такое преобразование всегда переводитпреобразованное из указателя
целое в тот же самый указатель,но в других случаях оно будет машинно-зависимым.
* 221 - Указатель на один тип может быть преобразован в указа-тель на другой
тип. Если преобразуемый указатель не указыва-ет на объекты, которые подходящим
образом выравнены в памя-ти, то результирующий указатель может при использовании
вы-зывать ошибки адресации. Гарантируется, что указатель наобъект заданного
размера может быть преобразован в указательна объект меньшего размера и снова
обратно, не претерпев приэтом изменения.Например, процедура распределения памяти
могла бы прини-мать запрос на размер выделяемого объекта в байтах, а возв-ращать
указатель на символы; это можно было бы использоватьследующим образом. EXTERN
CHAR *ALLOC();DOUBLE *DP;DP=(DOUBLE*) ALLOC(SIZEOF(DOUBLE));*DP=22.0/7.0;Функция
ALLOC должна обеспечивать (машинно-зависимым спосо-бом), что возвращаемое
ею значение будет подходящим для пре-образования в указатель на DOUBLE; в таком
случае использо-вание этой функции будет переносимым.Представление указателя
на PDP-11 соответствует 16-бито-вому целому и измеряется в байтах. Объекты типа
CHAR не име-ют никаких ограничений на выравнивание; все остальные объек-ты должны
иметь четные адреса.На HONEYWELL 6000 указатель соответствует 36-битовомуцелому;
слову соответствует 18 левых битов и два непосредст-венно примыкающих к ним
справа бита, которые выделяют символв слове. Таким образом, указатели на символы
измеряются вединицах 2 в степени 16 байтов; все остальное измеряется вединицах
2 в степени 18 машинных слов. Величины типа DOUBLEи содержащие их агрегаты
должны выравниваться по четным ад-ресам слов (0 по модулю 2 в степени 19). Эвм
IBM 370 иINTERDATA 8/32 сходны между собой. На обеих машинах адресаизмеряются
в байтах; элементарные объекты должны быть выров-нены по границе, равной их длине,
так что указатели на SHORTдолжны быть кратны двум, на INT и FLOAT - четырем
и наDOUBLE - восьми. Агрегаты выравниваются по самой строгойгранице, требуемой
каким-либо из их элементов.23. Константные выраженияВ нескольких местах в языке
“C” требуются выражения, ко-торые после вычисления становятся константами: после
вариан-тного префикса CASE, в качестве границ массивов и в инициа-лизаторах.
В первых двух случаях выражение может содержатьтолько целые константы, символьные
константы и выраженияSIZEOF, возможно связанные либо бинарными операциями
+ - * / . % \! Ч > == 1= =либо унарными операциями - \^ либо тернарной операцией
?:* 222 - Круглые скобки могут использоваться для группировки, но недля обращения
к функциям. В случае инициализаторов допускается большая (ударениена букву
о) свобода; кроме перечисленных выше константныхвыражений можно также применять
унарную операцию к внешнимили статическим объектам и к внешним или статическим
масси-вам, имеющим в качестве индексов константное выражение.Унарная операция
может быть также применена неявно, в ре-зультате появления неиндексированных массивов
и функций. Ос-новное правило заключается в том, что после вычисления ини-циализатор
должен становится либо константой, либо адресомранее описанного внешнего
или статического объекта плюс илиминус константа.24. Соображения о переносимостиНекоторые
части языка “C” по своей сути машинно-зависи-мы. Следующие ниже
перечисление потенциальных трудностей хо-тя и не являются всеобъемлющими, но
выделяет основные изних.Как показала практика, вопросы, целиком связанные с ап-паратным
оборудованием, такие как размер слова, свойстваплавающей арифметики и
целого деления, не представляют осо-бенных затруднений. Другие аспекты аппаратных
средств нахо-дят свое отражение в различных реализациях. Некоторые изних, в
частности, знаковое расширение (преобразующее отрица-тельный символ в отрицательное
целое) и порядок, в которомпомещаются байты в слове, представляют собой неприятность,которая
должна тщательно отслеживаться. Большинство из ос-тальных проблем
этого типа не вызывает сколько-нибудь значи-тельных затруднений.Число переменных
типа REGISTER, которое фактически можетбыть помещено в регистры, меняется
от машины к машине, такжекак и набор допустимых для них типов. Тем не менее
все ком-пиляторы на своих машинах работают надлежащим образом; лиш-ние или недопустимые
регистровые описания игнорируются.Некоторые трудности возникают только
при использованиисомнительной практики программирования. Писать программы,которые
зависят от каких- либо этих свойств, является чрез-вычайно неразумным.Языком
не указывается порядок вычисления аргументов фун-кций; они вычисляются справа
налево на PDP-11 и VAX-11 ислева направо на остальных машинах. порядок, в котором
про-исходят побочные эффекты, также не специфицируется.Так как символьные
константы в действительности являютсяобъектами типа INT, допускается использование
символьныхконстант, состоящих из нескольких символов. Однако, посколь-ку порядок,
в котором символы приписываются к слову, меняет-ся от машины к машине, конкретная
реализация оказываетсявесьма машинно-зависимой.Присваивание полей к словам
и символов к целым осуществ-ляется справо налево на PDP-11 и VAX-11 и слева
направо надругих машинах. эти различия незаметны для изолированныхпрограмм,
в которых не разрешено смешивать типы (преобразуя,например, указатель на INT в
указатель на CHAR и затем про-веряя указываемую память), но должны учитываться
при согла-совании с накладываемыми извне схемами памяти. * 223 - Язык, принятый
на различных компиляторах, отличаетсятолько незначительными деталями. Самое заметное
отличие сос-тоит в том, что используемый в настоящее время компилятор наPDP-11
не инициализирует структуры, которые содержат полябитов, и не допускает
некоторые операции присваивания в оп-ределенных контекстах, связанных с использованием
значенияприсваивания.25. АнахронизмыТак как язык “C” является развивающимся
языком, в старыхпрограммах можно встретить некоторые устаревшие конструкции.Хотя
большинство версий компилятора поддерживает такие анах-ронизмы, они в конце
концов исчезнут, оставив за собой толь-ко проблемы переносимости.В ранних версиях
“C” для проблем присваивания использо-валась форма =ON, а не ON=, приводя
к двусмысленностям, ти-пичным примером которых является X = -1где X фактически
уменьшается, поскольку операции = и - при-мыкают друг к другу, но что вполне
могло рассматриваться икак присваивание -1 к X.Синтаксис инициализаторов изменился:
раньше знак равенс-тва, с которого начинается инициализатор, отсутствовал,
такчто вместо INT X = 1;использовалосьINT X 1;изменение было внесено из-за инициализацииINT
F (1+2)которая достаточно сильно напоминает определение функции,чтобы
смутить компиляторы.26. Сводка синтаксических правилЭта сводка синтаксиса
языка “C” предназначена скорее дляоблегчения понимания и не является точной формулировкой
язы-ка.26.1. Выражения Основными выражениями являются следующие: выражение:первичное-выражение*
выражениевыражение* выражение! Выражение\^ выражение++
L-значение* L-значениеL-значение ++L-значение— * 224 - SIZEOF выражение(имя
типа) выражениевыражение бинарная-операция выражениевыражение ? Выражение : выражениеL-значение
операция-присваивания выражениевыражение , выражениепервичное
выражение:идентификаторконстантастрока(выражение)первичное-выражение (список выраженийнеоб)первичное-выражение
[выражение]L-значение . Идентификаторпервичное
выражение -> идентификаторL-значение:идентификаторпервичное-выражение [выражение]L-значение
. Идентификаторпервичное-выражение -> идентификатор* выражение(L-значение)Операции
первичных выражений () [] . -> имеют самый высокий приоритет
и группируются слеванаправо. Унарные операции * - ! \^ ++ -- SIZEOF(Имя типа)
имеют более низкий приоритет, чем операции первичных выраже-ний, но более высокий,
чем приоритет любой бинарной опера-ции. Эти операции группируются справа налево.
Все бинарныеоперации и условная операция (прим. Перевод.: условная опе-рация
группируется справа налево; это изменение внесено вязык в 1978 г.) группируются
слева направо и их приоритетубывает в следующем порядке: Бинарные операции:
* / % + - >> = == !=
\^ \! &
\!\! ?: * 225 - Все операции присваивания имеют
одинаковый приоритет и груп-пируются справа налево.Операции присваивания: = +=
-= *= ?= %= >>= #IF константное-выражение#IFDEF идентификатор#IFNDEF идентификатор#ELSE#ENDIF#LINE
константа идентификатор Последние изменения языка “C” (15 ноября
1978 г.)27. Присваивание структурыСтруктуры могут быть присвоены, переданы
функциям в ка-честве аргументов и возвращены функциям. Типы участвующихоперандов
должны оставаться теми же самыми. Другие правдопо-добные операторы, такие как
сравнение на равенство, не былиреализованы.В реализации возвращения структур
функциями на PDP-11имеется коварный дефект: если во время возврата происходитпрерывание
и та же самая функция пеентерабельно вызываетсяво время этого прерывания,
то значение возвращаемое из пер-вого вызова, может быть испорчено. Эта трудность
может воз-никнуть только при наличии истинного прерывания, как из опе-рационной
системы, так и из программы пользователя, прерыва-ния, которое существенно
для использования сигналов; обычныерекурсивные вызовы совершенно безопасны.28. Тип
перечисленияВведен новый тип данных,аналогичный скалярным типам язы-ка
паскаль. К спецификатору-типа в его синтаксическом описа-нии в разделе 8.2. Приложения
а следует добавить спецификатор-перечисления с синтаксисом пецификатор-перечисления:ENUM
список-перечисленияENUM идентификатор список-перечисления -------------
-------------------ENUM идентификатор * 230 - cписок-перечисления:перечисляемоесписок-перечисления,
перечисляемое ------------------- -------------перечисляемое:идентификаторидентификатор
= константное выражение -------------
--------------------- Роль идентификатора в спецификаторе-перечисления пол-ностью
аналогична роли ярлыка структуры в спецификато-ре-структуры; идентификатор обозначает
определенное перечис-ление. Например, описание ENUM COLOR \(RED, WHITE,
BLACK, BLUE \); . . .ENUM COLOR *CP, COL; Объявляет идентификатор COLOR ярлыком
перечисления типа,описывающего различные цвета и затем объявляет CP указателемна
объект этого типа, а COL - объектом этого типа.Идентификаторы в списке-перечисления
описываются какконстанты и могут появиться там, где требуются (по контекс-ту)
константы. Если не используется вторая форма перечисляе-мого (с равеством
=), то величины констант начинаются с 0 ивозрастают на 1 в соответствии с
прочтением их описания сле-ва на право. Перечисляемое с присвоением = придает соответс-твующему
идентификатору указанную величину; последующиеидентификаторы продолжают
прогрессию от приписанной величи-ны.Все ярлыки перечисления и константы
могут быть различны-ми и непохожими на ярлыки и члены структур даже при условиииспользования
одного и того же множества идентификаторов.Объекты данного типа
перечисления рассматриваются какобъекты, имеющие тип, отличный от любых типов
и контролирую-щая программа LINT сообщает об ошибках несоответствия типов.В реализации
на PDP-11 со всеми перечисляемыми переменнымиоперируют так, как если бы
они имели тип INT.29. Таблица изображений непечатных символов языка “C”.В данной
таблице приведены изображения некоторых симво-лов (фигурные скобки и т.д.)
языка “C”, которых может неоказаться в знаковом наборе дисплея или печатающего
устройс-тва. * 231 - ! Значение ! Изображение ** ! ! ! В тексте ! ! Фигурная открывающаяся
! ! ! Скобка ! \( ! ! ! ! ! Фигурная закрывающаяся ! ! ! Скобка !
\) ! ! ! ! ! Вертикальная ! ! ! Черта ! \! ! ! ! ! ! ! ! ! Апостороф ! \' ! ! !
! ! Волнистая ! ! ! Черта ! \^ ! ! ! ! ** П_р_и_м_е_ч_а_н_и_е:Изображения приведены
для операционой системы UNIX. Приработе компилятора “C” под управлением любой
другой операци-онной системы, необходимо воспользоваться соответствующимруководством
для данной системы.