Язык программирования C [2-е издание] 9785845908919

Классическая книга по языку C, написанная самими разработчиками этого языка и выдержавшая в США уже 34 переиздания! Книг

131 62 4MB

Russian Pages 304 [290] Year 2009

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Язык программирования C [2-е издание]
 9785845908919

Table of contents :
Содержание
Предисловие
Предисловие к первому изданию
Введение
Глава 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.6. Массивы
1.7. Функции
1.8. Аргументы: передача по значению
1.9. Массивы символов
1.10. Внешние переменные
Глава 2. Типы данных, операции и выражения
2.1. Имена переменных
2.2. Типы данных и их размеры
2.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. Оператор switch
3.5. Циклы - while и for
3.6. Циклы - do-while
3.7. Операторы break и continue
3.8. Оператор goto и метки
Глава 4. Функции и структура программы
4.1. Основы создания функций
4.2. Функции, возвращающие нецелые значения
4.3. Внешние переменные
4.4. Область действия
4.5. Заголовочные файлы
4.6. Статические переменные
4.7. Регистровые переменные
4.8. Блочная структура
4.9. Инициализация
4.10. Рекурсия
4.11. Препроцессор C
4.11.1. Включение файлов
4.11.2. Макроподстановки
4.11.3. Условное включение
Глава 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. Форматированный вывод и функция printf
7.3. Списки аргументов переменной длины
7.4. Форматированный ввод и функция scanf
7.5. Доступ к файлам
7.6. Обработка ошибок. Поток stderr и функция exit
7.7. Ввод-вывод строк
7.8. Различные функции
7.8.1. Операции со строками
7.8.2. Анализ, классификация и преобразование символов
7.8.3. Функция ungetc
7.8.4. Выполнение команд
7.8.5. Управление памятью
7.8.6. Математические функции
7.8.7. Генерирование случайных чисел
Глава 8. Интерфейс системы Unix
8.1. Дескрипторы файлов
8.2. Ввод-вывод низкого уровня - функции read и write
8.3. Функции open, creat, close, unlink
8.4. Прямой доступ к файлу и функция lseek
8.5. Пример реализации функций fopen и getc
8.6. Пример получения списка файлов в каталоге
8.7. Пример распределения памяти
Приложение А. Справочное руководство по языку C
А.1. Введение
А.2. Лексические соглашения
А.2.1. Лексемы
А.2.2. Комментарии
А.2.3. Идентификаторы
А.2.4. Ключевые слова
А.2.5. Константы
А.2.5.1. Целочисленные константы
А.2.5.2. Символьные константы
А.2.5.3. Вещественные константы с плавающей точкой
А.2.5.4. Константы перечислимых типов
А.З. Система синтаксических обозначений
А.4. Употребление идентификаторов
А.4.1. Классы памяти
А.4.2. Базовые типы
А.4.3. Производные типы
А.4.4. Модификаторы типов
А.5. Объекты и именующие выражения
А.6. Преобразования типов
А.6.1. Расширение целочисленных типов
А.6.2. Преобразование целочисленных типов
А.6.3. Преобразование целых чисел в вещественные и наоборот
А.6.4. Вещественные типы
А.6.5. Арифметические преобразования
А.6.6. Связь указателей и целых чисел
А.6.7. Тип void
А.6.8. Указатели на void
А.7. Выражения
А.7.1. Генерирование указателей
А.7.2. Первичные выражения
А.7.3. Постфиксные выражения
А.7.3.1. Обращение к элементам массивов
А.7.3.2. Вызовы функций
А.7.3.3. Обращение к структурам
А.7.3.4. Постфиксное инкрементирование
А.7.4. Одноместные операции
А.7.4.1. Префиксные операции инкрементирования
А.7.4.2. Операция взятия адреса
А.7.4.3. Операция разыменования (ссылки по указателю)
А.7.4.4. Операция "одноместный плюс"
А.7.4.5. Операция "одноместный минус"
А.7.4.6. Операция вычисления дополнения до единицы (поразрядного отрицания)
А.7.4.7. Операция логического отрицания
А.7.4.8. Операция вычисления размера sizeof
А.7.5. Приведение типов
А.7.6. Мулыипликативные операции
А.7.7. Аддитивные операции
А.7.8. Операции сдвига
А.7.9. Операции отношения (сравнения)
А.7.10. Операции проверки равенства
А.7.11. Операция поразрядного и
А.7.12. Операция поразрядного исключающего или
А.7.13. Операция поразрядного включающего или
А.7.14. Операция логического и
А.7.15. Операция логического или
А.7.16. Операция выбора по условию
А.7.17. Выражения с присваиванием
А.7.18. Операция "запятая"
А.7.19. Константные выражения
А.8. Объявления
А.8.1. Спецификаторы класса памяти
А.8.2. Спецификаторы типа
А.8.3. Объявления структур и объединений
А.8.4. Перечислимые типы (перечисления)
А.8.5. Описатели
А.8.6. Смысл и содержание описателей
А.8.6.1. Описатели указателей
A.8.6.2. Описатели массивов
А.8.6.3. Описатели функций
А.8.7. Инициализация
А.8.8. Имена типов
А.8.9. Объявление typedef
А.8.10. Эквивалентность типов
А.9. Операторы
А.9.1. Операторы с метками
А.9.2. Операторы-выражения
А.9.3. Составные операторы
А.9.4. Операторы выбора
А.9.5. Операторы цикла
А.9.6. Операторы перехода
А.10. Внешние объявления
А.10.1. Определения функций
А.10.2. Внешние объявления
А.11. Область действия и связывание
А.11.1. Лексическая область действия
А.11.2. Связывание
А.12. Препроцессор
А.12.1. Комбинации из трех символов
А.12.2. Слияние строк
А.12.3. Макроопределения и их раскрытие
А.12.4. Включение файлов
А.12.5. Условная компиляция
А.12.6. Нумерация строк
А.12.7. Генерирование сообщений об ошибках
А.12.8. Директива #pragma
А.12.9. Пустая директива
А.12.10. Заранее определенные имена
А.13. Грамматика
Приложение Б. Стандартная библиотека
Б.1. Ввод-вывод:
Б.1.1. Файловые операции
Б.1.2. Форматированный вывод
Б.1.3. Форматированный ввод
Б.1.4. Функции ввода-вывода символов
Б.1.5. Функции прямого ввода-вывода
Б.1.6. Функции позиционирования в файлах
Б.1.7. Функции обработки ошибок
Б.2. Анализ и классификация символов:
Б.З. Функции для работы со строками:
Б.4. Математические функции:
Б.5. Вспомогательные функции:
Б.6. Диагностика:
Б.7. Переменные списки аргументов:
Б.8. Нелокальные переходы:
Б.9. Сигналы:
Б.10. Функции даты и времени:
Б.11. Системно-зависимые константы: и
Приложение В. Сводка изменений
Предметный указатель

Citation preview

ВТОРОЕ ИЗДАНИЕ

ЯЗЬII< ПРОГРАММИРОВАНИЯ

БРАЙАН КЕРНИГАН ДЕННИС РИТЧИ Серия книг по программированию от Prentice Hall



11

ЯЗЬII< ПРОГРАММИРОВАНИЯ

c

второе издание, переработанное и дополненное

БРАЙАН КЕРНИГАН ДЕННИС РИТЧИ

• Издательский дом "Вильяме" Москва• Санкт-Петербург• Киев

2009

c ТНЕ

PROGRAММING LANGUAGE Second Edition

BRIAN W KERNIGHAN DENNIS М. RITCHIE Bell Laboratories Murray Hill, New Jersey

АТ & Т



Prentice Hall PTR, Upper Saddle River, New Jersey 07458

ББК 32.973.26-018.2.75 К36 УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией С.Н. Тригуб Перевод с английского и редакция В.Л. Бродового По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу:[email protected]Ь!ishing.com,http://www.williamspuЫishing.com

Керниган, Брайан У., Ритчи, Деяние М.

К36

Язык программирования С, 2-е издание. : Пер . с англ. "Вильяме", 2009. - 304 с.: ил. - Парал. тит. англ.

-

М.

: Издательский дом

ISBN 978-5-8459- 0891-9 (рус.) Классическая книга п о языку С , написанная самими разработчиками этого языка и выдержав­ шая в США уже 34 переиздания! Книга является как практически исчерпывающим справочником, так

и

учебным пособием по самому распространенному языку программирования. Предлагаемое

второе издание книги было существенно переработано по сравнению с первым в связи с появлени­ ем стандарта ANSI С, для которого она частично послужила основой. Книга не рекомендуется для чтения новичкам; для своего изучения она требует знания основ программирования и вычисли­ тельной техники. Книга предназначена для широкого круга программистов и компьютерных специалистов. Мо­ жет использоваться как учебное пособие для вузов.

ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответству­ ющих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирова­ ние и запись на магнитный носитель, если на это нет письменного разрешения издательства Prentice Hall, lnc. Authorized translation from the English language edition puЫished Ьу Prentice Hall, lnc" Copyright © 1988, 1978 Ьу Bell Telephone Laboratories, lnc. All rights reserved. No part of this book may Ье reproduced or transmitted in any form or Ьу any means, electronic or mechanical, including photocopying, recording or Ьу any information storage retrieval system, without permission from the PuЫisher. 34th Printing. Russian language edition puЫished Ьу Williams PuЫishing House according to the Agreement with R&I Enterprises lntemational, Copyright © 2009

ISBN 978-5-8459- 0891-9 (рус.)

©Издательский дом "Вильяме", 2009

ISBN 0-13-110362-8 (англ.)

© Ьу Bell Telephone Laboratories, Inc" 1988, 1978

Огл а вл ен и е Предисловие

11

Предисловие к первому изданию

13

Введение

15

Глава 1 . Вводный урок

19

Глава 2. Типы данных, операции и выражения

49

Глава 3. Управляющие конструкции

69

Глава 4. Функции и структура программы

81

Глава 5. Указатели и массивы

1 05

Глава 6. Структуры

1 39

Глава 7. Ввод-вывод

1 63

Глава 8. Интерф ейс системы Unix

181

Приложение А. Справочное руководство по языку С

20 1

Приложение Б. Стандартная библиотека

259

Приложение В. Сводка изменений

28 1

Предметный указатель

284

С о д е ржа н и е Предисловие

11

Предисловие к первому изданию

13

Введение

15

Глава

1. Вводный урок

1.1. 1 .2. 1 .3 . 1 .4. 1 .5 .

Первые шаги Переменные и арифметические выражения Оператор for Символические константы Символьный ввод-вывод 1 .5 . 1 . Копирование файлов 1 .5.2. Подсчет символов 1 .5 . 3 . Подсчет строк 1 .5 .4. Подсчет слов 1 .6. Массивы 1 . 7. Функции 1 . 8. Аргументы: передача по значению 1 .9. Массивы символов 1 . 10. Внешние переменные Глава

2. Типы данных, операции и выражения

2. 1 . Имена переменных 2.2. Типы данных и их размеры 2.3. Константы 2.4. Объявления 2.5. Арифметические операции 2.6. Операции отношения и логические операции 2. 7. Преобразование типов 2.8. Операции инкрементирования и декрементирования 2.9. Поразрядные операции 2. 1 0 . Операции с присваиванием и выражения с ними 2. 1 1 . Условные выражения 2. 1 2. Приоритет и порядок вычисления Глава

3. 1 . 3.2. 3.3. 3 .4. 3.5. 3.6. 3.7. 3.8.

3. Управляющие конструкции

Операторы и блоки Оператор if-else Конструкция else-if Оператор switch Циклы - while и for Циклы - do-while Операторы break и continue Оператор goto и метки

19 19 22 27 28 29 29 31 32 33 35 37 40 41 44 49 49 50 51 54 55 55 56 60 62 63 65 66 69 69 69 71 72 74 77 78 79

Глава 4. Функции и структура программы

4. 1 . Основы создания функций 4.2. Функции, возвращающие нецелые значения 4.3. Внешние переменные 4.4. Область действия 4.5. Заголовочные файлы 4.6. Статические переменные 4.7. Регистровые переменные 4.8. Блочная структура 4.9. Инициализация 4. 1 О. Рекурсия 4. l l. Препроцессор С 4. ll. l. Включение файлов 4. 1 1 .2. Макроподстановки 4.l l.3. Условное включение Глава

5. Указатели и массивы

5 .l. Указатели и адреса 5.2. Указатели и аргументы функций 5.3. Указатели и массивы 5 .4. Адресная арифметика 5.5. Символьные указатели и функции 5.6. Массивы указателей и указатели на указатели 5.7. Многомерные массивы 5.8. Инициализация массивов указателей 5.9. Указатели и многомерные массивы 5 . 1 0. Аргументы командной строки 5. l l. Указатели на функции 5 . 1 2. Сложные объявления Глава

6. 1 . 6.2. 6.3. 6.4. 6.5. 6.6. 6. 7. 6.8. 6.9. Глава

6. Структуры

Основы работы со структурами Структуры и функции Массивы структур Указатели на структуры Структуры со ссылками на себя Поиск по таблице Определение новых типов Объединения Битовые поля 7. Ввод-вывод

7.l. Стандартные средства ввода-вывода 7 .2. Форматированный вывод и функция printf 7.3. Списки аргументов переменной длины 7.4. Форматированный ввод и функция scanf 7.5. Доступ к файлам 7.6. Обработка ошибок. Поток stderr и функция exit 7.7. Ввод-вывод строк СОДЕРЖАНИ Е

81 81 85 87 93 95 96 97 97 98 99 101 101 1 02 1 04 1 05 1 05 1 07 1 09 1 12 1 15 1 18 1 22 1 24 1 24 1 25 1 29 1 32 1 39 1 39 141 144 1 47 1 49 1 54 1 56 1 58 1 59 1 63 1 63 1 65 1 67 1 69 17 2 1 74 1 76

7

7 .8. Различные функции 7.8.1. Операции со строками 7 .8.2. Анализ, классификация и преобразование символов 7.8.3. Функция ungetc 7.8.4. Выполнение команд 7.8.5. Управление памятью 7.8.6. Математические функции 7.8.7. Генерирование случайных чисел

177 177 178 178 178 1 79 1 79 180

Unix

1 81 181 182 184 186 187 190 196

Глава 8. Интерфейс системы

8.1. 8.2. 8.3. 8.4. 8.5. 8.6. 8.7.

Дескрипторы файлов Ввод-вывод низкого уровня - функции read и write Функции open, creat, close, unlink Прямой доступ к файлу и функция lseek Пример реализации функций fopen и getc Пример получения списка файлов в каталоге Пример распределения памяти

Приложение А. Справочное руководство по языку

С

А.1. Введение А.2. Лексические соглашения А.2.1. Лексемы А.2.2. Комментарии А.2.3. Идентификаторы А.2.4. Ключевые слова А.2.5. Константы А.2.5.1. Целочисленные константы А.2.5.2. Символьные константы А.2.5.3. Вещественные константы с плавающей точкой А.2.5.4. Константы перечислимых типов А.2.6. Строковые литералы (константы) А.3. Система синтаксических обозначений А.4. Употребление идентификаторов А.4.1. Классы памяти А.4.2. Базовые типы А.4. 3 . Производные типы А.4.4. Модификаторы типов А.5. Объекты и именующие выражения А.6. Преобразования типов А.6.1. Расширение целочисленных типов А.6.2. Преобразование целочисленных типов А.6.3. Преобразование целых чисел в вещественные и наоборот А.6.4. Вещественные типы А.6.5. Арифметические преобразования А.6.6. Связь указателей и целых чисел А.6.7. Тип void А.6.8. Указатели на void А.7. Выражения А. 7 .1. Генерирование указателей

8

201 201 201 201 202 202 202 203 203 203 204 205 205 205 206 206 206 207 208 208 208 209 209 209 209 210 210 21 1 2 12 212 213 СОДЕРЖАНИЕ

А.7.2. Первичные выражения А.7.3. Постфиксные выражения А.7.3.1. Обращение к элементам массивов А.7.3.2. Вызовы функций А.7.3.3. Обращение к структурам А.7.3.4. Постфиксное инкрементирование А.7.4. Одноместные операции А. 7.4.1. Префиксные операции инкрементирования А.7.4.2. Операция взятия адреса А.7.4.3. Операция разыменования (ссылки по указателю) А. 7.4.4. Операция "одноместный плюс" А.7.4.5. Операция "одноместный минус" А.7.4.6. Операция вычисления дополнения до единицы (поразрядного отрицания) А .7.4.7 . Операция логического отрицания А.7.4.8. Операция вычисления размера sizeof А.7.5. Приведение типов А.7.6. Мулыипликативные операции А.7.7 . Адд итивные операции А.7.8. Операции сдвига А.7.9. Операции отношения (сравнения) А.7.10. Операции проверки равенства А. 7 .11. Операция поразрядного И А. 7 .12. Операция поразрядного исключающего ИЛИ А.7.13. Операция поразрядного включающего ИЛИ А.7.14. Операция логического И А.7.15. Операция логического ИЛИ А. 7 .16. Операция выбора по условию А. 7 .17 . Выражения с присваиванием А.7.18. Операция "запятая" А.7 .19. Константные выражения А.8. Объявления А.8.1. Спецификаторы класса памяти А.8.2. Спецификаторы типа А.8.3. Объявления структур и объединений А.8.4. Перечислимые типы (перечисления) А.8.5. Описатели А.8.6. Смысл и содержание описателей А.8.6.1. Описатели указателей А.8.6.2. Описатели массивов А.8.6.3. Описатели функций А.8.7. Инициализация А.8.8. Имена типов А.8.9. Объявление typedef А.8.10. Эквивалентность типов А.9. Операторы А.9.1. Операторы с метками СОДЕРЖАНИЕ

213 213 214 214 215 216 216 216 216 216 217 217 217 217 217 218 218 218 219 219 220 220 221 221 221 22 1 222 222 223 223 224 224 225 226 230 230 231 231 232 233 234 236 237 238 238 238 9

А.9.2. О ператоры-выражения А.9.3. Составные операторы А.9.4. Операторы выбора А.9.5. Операторы цикла А.9.6. Операторы перехода А.10 . Внеш ние объявления А .10.1. Определения функций А.10.2. Внешние объявления А.11. Область действия и связывание А.11.1. Лексическая область действия А.11.2. Связывание А.12. Препроцессор А.12.1. Комбинации из трех символов А.12.2. Слияние строк А.12 .3. Макроопределения и их раскрытие А.12.4. Включение файлов А.12.5. Условная компиляция А.12.6. Нумерация строк А .12.7. Генерирование сообщений об ошибках А.12.8. Директива #pragma А.12.9. Пустая директива А.12.1 О. Заранее определенные имена А.13. Грамматика Приложение

Б.

С тандартная библиотека

Б.1. Ввод-вывод: Б.1.1. Файловые операции Б.1.2. Форматированный вывод Б.1.3. Форматированный ввод Б.1.4. Функции ввода-вывода символов Б.1.5. Функции прямого ввода-вывода Б.1.6. Функции позиционирования в файлах Б.1. 7. Функции обработки ошибок Б.2. Анализ и классификация символов: Б.3. Функции для работы со строками: Б.4. Математические функции: Б.5. Вспомогательные функции: Б.6. Диагностика: Б.7. Переменные списки аргументов: Б.8. Нелокальные переходы: Б.9. Сигналы: Б.10. Функции даты и времени: Б.11. Системно-зависимые константы: и

239 239 239 240 241 242 242 243 244 245 245 246 247 247 247 249 250 251 251 251 251 252 252 259 259 260 261 263 265 266 267 267 268 269 270 271 274 275 275 276 277 279

Приложение В. С водка изменений

281

Предметный указатель

284

10

СОДЕРЖАНИ Е

Предисловие С момента выхода книги Язык программирования С в 1978 году в мире вычислитель­ ных технологий произошли революционные изменения. Большие компьютеры стали еще больше, а персональные приобрели такую мощь, что могут посоревноваться с суперком­ пьютерами предыдущего десятилетия. За это время язык С также изменился, хотя и в не­ большой степени. А главное - он вышел далеко за пределы своего первоначального уз­ кого применения в виде языка операционной системы Unix. Постоянно возрастающая популярность С, накопившиеся за годы изменения в языке, разработка компиляторов независимыми группами, которые не участвовали в его созда­ нии, - все это вместе показало, что назрела необходимость дать более точное и совре­ менное описание языка, чем то, которое содержалось в первом издании этой книги. В 1983 году Американский национальный институт стандартов (ANSI - American National Staпdards Iпstitute) создал комитет, целью которого была разработка "четко сформулированного и системно-независимого определения языка С" при сохранении са­ мого духа и стиля этого языка. Результатом стало появление стандарта ANSI языка С. В этом стандарте даны формальные определения конструкций, которые лишь очер­ чивались, но не описывались строго в первом издании книги. В частности, это присваи­ вание структур и перечислимые типы. В стандарте задается новая форма объявления функций, которая позволяет выполнить перекрестную проверку правильности объявле­ ния и фактического вызова. В нем определяется состав стандартной библиотеки функ­ ций, содержащей обширный набор средств для ввода-вывода, управления памятью, опе­ раций со строками и т.п. Стандарт дает точное определение тех средств, для которых в первоначальном описании языка такого определения дано не было, и в то же время ука­ зывает, какие аспекты языка остались системно-зависимыми. Во втором издании книги Язык программирования С описывается язык С в том виде, в каком его определяет стандарт ANSI. Хотя мы и отмечаем те места языка, которые эволюционировали со временем, но все же мы решили писать почти исключительно о современном состоянии С. Как правило, это не очень существенно; наиболее заметное изменение - это новая форма объявления и определения функций. Современные компи­ ляторы уже поддерживают большую часть нового стандарта. Мы постарались сохранить краткость первого издания. Язык С невелик по объему, и нет большого смысла писать о нем толстые книги. Мы улучшили изложение ключевых вопросов - таких как указатели, являющиеся центральным моментом в программирова­ нии на С. Мы доработали первоначальные примеры, а также добавили новые в некото­ рые из глав. Например, рассказ о сложных объявлениях дополнен программой преобра­ зования деклараций в текстовые описания и наоборот. Как и раньше, все примеры про­ тестированы

и

отлажены

непосредственно

из

текста,

который

подготовлен

в

электронном виде. Приложение А является справочником по языку, но не описанием стандарта, хотя мы и попытались изложить все основные элементы стандарта в сжатом виде. Справочник задумывался как легкодоступное пособие для программистов, но отнюдь не как строгое определение языка для разработчиков компиляторов - эта роль по праву принадлежит собственно стандарту. Приложение Б содержит краткое описание средств стандартной библиотеки. Оно также задумывалось как справочник для программистов, но не для раз-

работчиков компиляторов. Приложение В содержит список изменений, внесенных в язык С со времени первого издания книги. Как говорилось в предисловии к первому изданию, язык С "становится все удобнее по мере того, как приобретается опыт работы с ним". После нескольких десятилетий ра­ боты мы не изменили своего мнения. Надеемся, что эта книга поможет вам изучить С и пользоваться им как можно эффективнее. Мы глубоко признательны нашим друзьям, без которых второе издание книги не вы­ шло бы в свет. Джон Бентли (Jon Bentley), Дуг Гвин (Doug Gwyn), Дуг Макилрой (Doug Mcllroy), Питер Нельсон (Peter Nelson) и Роб Пайк (Rob Pike) высказали вдумчивые за­ мечания почти по каждой странице исходного текста. Мы также благодарны за тщатель­ ное прочтение рукописи таким людям, как Эл Ахо (Al Aho), Деннис Эллисон (Dennis Allison), Джо Кэмпбелл (Joe Campbell), Дж. Р. Эмлин (G. R . Emlin), Карен Фортганг (Karen Fortgang), Аллен Холуб (Allen Holub), Эндрю Хьюм (Andrew Hume), Дэйв Кри­ стол (Dave Кristol), Джон Линдерман (John Linderman), Дэйв Проссер (Dave Prosser), Джин Спаффорд (Gene Spafford) и Крис Ван Вик (Chrys Van Wyk) . Ряд ценных предло­ жений выдвинули Билл Чезвик (Bill Cheswick), Марк Керниган (Mark Kemighan), Энди Кёниг (Andy Koenig), Робин Лэйк (Robln Lake), Том Ланден (Tom London), Джим Ридз (Jim Reeds), Кловис Тондо (Clovis Tondo) и Питер Вайнбергер (Peter Weinberger). Дэйв Проссер (Dave Prosser) самым подробным образом ответил на множество вопросов, ка­ сающихся стандарта ANSI. Для текущей компиляции и тестирования наших программ мы широко использовали транслятор С++ Бьерна Страуструпа (Bjame Stroustrup), а для окончательного тестирования Дэйв Кристол (Dave Кristol) предоставил компилятор ANSI С. Большую помощь в типографском оформлении текста оказал Рич Дрекслер (Rich Drechsler). Выражаем всем нашу искреннюю благодарность. Брайан В. Керниган Деннис М. Ритчи

П р еди сл о в и е к пе р в о м у изда н и ю С представляет собой язык программирования общего наз начения, характеризую­ щийся краткостью выражений, современными управляющими конструкциями и структу­ рами данных, а также богатым набором операций. С - это язык не слишком высокого уровня, не слишком объемный и не приспособленный специально к какой-либо конкрет­ ной области приложений. Но зато отсутствие в нем каких-либо ограничений и большая общность делают его более удобным и эффективным для решения многих задач, чем языки, даже считающиеся по разным причинам более мощными . Первоначально язык С был разработан для операционной системы Unix на ЭВМ DEC PDP-11 и реализован в этой системе Деннисом Ритчи (Dennis Ritchie). Операционная система, компилятор С и практически все основные прикладные программы для Unix (в том числе те, с помощью которых готовилась эта книга) написаны на С. Существуют профессиональные компиляторы и для друтих систем, в частности IBM System/370, Honeywell 6000 и Interdata 8/32. Тем не менее язык С не привязан к какой-либо конкрет­ ной аппаратной или системной платформе, и на нем легко писать программы, которые будут выполняться безо всяких изменений в любой системе, поддерживающей этот язык. Эта книга предназначена для того, чтобы помочь читателю научиться программиро­ вать на С. Первая глава книги называется "Вводный урок", благодаря которой читатель может сразу взяться за дело, а затем следуют главы с подробным изложением всех ос­ новных средств языка, завершает книгу справочное руководство. Изложение большей частью построено на чтении, написании и исправлении примеров программ, а не на про­ стом описании правил и конструкций. В основном рассматриваются примеры закончен­ ных и реально работающих программ, а не отдельные искусственные фрагменты. Все примеры были протестированы непосредственно из исходного текста, который подго­ товлен в электронном виде. Кроме демонстрации эффективного применения языка, мы постарались по возможности дать понятие о ряде полезных алгоритмов и принципов хо­ рошего стиля и разумного написания программ. Эта книга не представляет собой элементарное введение в программирование. Пред­ полагается, что читатель знаком с основными понятиями программирования - такими как переменные, операторы присваивания, циклы и функции. Тем не менее даже начи­ нающий программист скорее всего окажется в состоянии читать примеры и извлекать из них уроки языка, хотя помощь более знающего коллеги будет не лишней. Судя по нашему опыту, язык С оказался легким в применении, выразительным и дос­ таточно универсальным для целого ряда программных приложений. Он прост в изучении и по мере накопления опыта работы с ним становится все удобнее. Мы надеемся, что эта книга поможет вам эффективно работать с языком С. Вдумчивая критика и предложения многих наших друзей и коллег очень помогли нам в написании этой книги и сделали процесс работы над ней очень приятным. В частности, Майк Бьянки (Mike Bianchi), Джим Блю (Jim Blue), Стью Фельдман (Stu Feldman), Дуг Макилрой (Doug Mcllroy), Билл Рум (Bill Roome), Боб Розин (ВоЬ Rosin) и Ларри Рослер (Larry Rosler) прочитали несколько версий книги самым тщательным образом. Мы глу-

боко признательны Элу Ахо (Al Aho), Стиву Борну (Steve Bourne), Дэну Дворак (Dan Dvorak), Чаку Хэйли (Chuck Haley), Дебби Хэйли (Debble Haley), Марион Харрис (Marion Harris), Рику Холту (Rick Holt), Стиву Джонсону (Steve Johnson), Джону Мэши (John Mashey), Бобу Митцу (ВоЬ Mitze), Ральфу Мухе (Ralph Muha), Питеру Нельсону (Peter Nelson), Эллиоту Пинсону (Elliot Pinson), Билл у Плоджеру (Bill Plauger), Джерри Спи­ ваку (Jerry Spivack), Кену Томпсону (Ken Thornpson) и Питеру Вайнбергеру (Peter WeinЬerger) за полезные замечания на разных этапах подготовки книги, а также Майку Леску (Мike Lesk) и Джо Оссанне (Joe Ossanna) за неоценимую помощь при макетировании. Брайан В. Керниган Деннис М. Ричи

Ждем ваш и х отзы в о в! Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые дру­ гие замечания, которые вам хотелось бы высказать в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бу­ мажное или электронное письмо либо просто посетить наш WеЬ-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нра­ вится вам эта книга или нет, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обя­ зательно учтем его при отборе и подготовке к изданию последующих книг. Наши ко­ ординаты: [email protected] l l i amspuЫ i sh i ng . c om E-mail: ht tp://www . wi l l i amspuЬ l i s hing . com WWW: Адреса для писем из : России: Украины:

127055, Москва, ул. Лесная, д. 43, стр. 1 03150, К иев, а/я 152

Издательство выражает признательность Крупенько Никите Анатольевичу за при­ сланные замечания и пожелания.

В в ед е н и е С является языком программирования общего назначения. Он всегда находился в са­ мой тесной связи с операционной системой Unix, в которой и для которой он и разраба­ тывался, поскольку как сама система, так и большинство работающих в ней программ написаны именно на С. Тем не менее сам язык не привязан жестко к одной определенной системе или аппаратной платформе. И хотя С называют "языком системного программи­ рования", поскольку на нем удобно писать компиляторы и операционные системы, он столь же удобен и для написания больших прикладных программ в самых разных облас­ тях применения. Многие ключевые идеи С пришли из языка BCPL, разработанного Мартином Ричар­ дсом (Martin Richards). BCPL оказал влияние на С опосредованно - через язык В, раз­ работанный Кеном Томпсоном (Ken Thompson) в 1970 году для первой системы Unix на ЭВМ DEC PDP-7. В языках BCPL и В отсутствует типизация данных. В отличие от них язык С предла­ гает широкий ассортимент типов. Фундаментальные типы включают в себя символьный, а также целый и вещественный (с плавающей точкой) нескольких различных размеров. Кроме того, существует целая иерархия производных типов данных, создаваемых с по­ мощью указателей, массивов, структур и объединений. Из операндов и знаков операций формируются выражения; любое выражение, в том числе присваивание и вызов функ­ ции, может являться оператором. Благодаря указателям поддерживается механизм сис­ темно-независимой адресной арифметики. В языке С имеются все основные управляющие конструкции, необходимые для напи­ сания хорошо структурированной программы: группировка операторов в блоки, приня­ тие решения по условию ( i f - e l s e), выбор одного из нескольких возможных вариантов ( s w i t ch), циклы с проверкой условия завершения в начале (wh i l e, for ) и в конце (do), а также принудительный выход из цикла (break). Функции могут возвращать значения простых типов, структуры, объединения или указатели. Любую функцию можно вызывать рекурсивно. Локальные переменные функ­ ции обычно являются "автоматическими", т.е. создаются при каждом ее вызове. Опреде­ ления (тела) функций нельзя вкладывать друг в друга, однако переменные можно объяв­ лять в блочно-структурированном стиле. Функции программы на С могут находиться в отдельных файлах исходного кода, компилируемых также отдельно. Переменные могут быть внутренними по отношению к функции, внешними и при этом видимыми только в одном файле кода или же видимыми во всем пространстве программы . На этапе препроцессорной обработки в тексте программы выполняются макропод­ становки, включение дополнительных файлов исходного кода и условная компиляция. С это язык сравнительно "низкого" уровня. Ничего уничижительного в этом опре­ делении нет; это всего лишь значит, что язык С работает с теми же объектами, что и большинство компьютерных систем, а именно с символами, числами и адресами. Эти дан­ ные можно комбинировать разными способами с помощью арифметических и логических операций, которые реализованы реальными аппаратными и системными средствами. В языке С нет операций для прямого манипулирования составными объектами, на­ пример строками символов, множествами, списками или массивами. Не существует опе­ раций для непосредственной работы с целым массивом строк, хотя структуру можно -

скопировать как единое целое. Язык не содержит специальных средств распределения памяти - только статическое определение и стек, в котором хранятся локальные пере­ менные функций; не предусмотрена ни системная куча (heap), ни сборка мусора (garbage collection). Наконец, в самом языке нет и средств ввода-вывода наподобие операторов READ или WRI TE, а также отсутствуют встроенные механизмы обращения к файлам. Все эти операции высокого системного уровня выполняются путем явного вызова функций. В большинстве реализаций языка С имеется более или менее стандартный набор таких функций. Аналогично, в С имеются только несложные, однопоточные управляющие структуры: проверки условий, циклы, блоки и подпрограммы, однако отсутствует мультипрограм­ мирование, распараллеливание операций, синхронизация и сопрограммы. Хотя отсутствие некоторых из перечисленных средств может показаться очень серь­ езным недостатком ("То есть я должен вызывать функцию, чтобы просто сравнить две строки символов?!"), тем не менее в том, чтобы сохранять набор базовых конструкций языка относительно небольшим, есть и свои преимущества. Поскольку С невелик, его описание занимает немного места, а изучение отнимает немного времени. Каждый про­ граммист вполне может знать, понимать и регулярно использовать практически всю базу языка. Много лет определением языка служил справочник по нему, включенный в первое издание книги Язык программирован ия С. В 1983 году Американский национальный ин­ ститут стандартов (ANSI) основал комитет для создания полного и современного опре­ деления языка С. В результате к концу 1988 года было завершено создание стандарта ANSI С. Большинство средств и возможностей этого стандарта поддерживается совре­ менными компиляторами. Стандарт основан на первоначальном справочнике по языку. Сам язык изменился от­ носительно мало; одной из целей его стандартизации было обеспечить совместимость с большинством уже написанных программ или по крайней мере добиться от компилято­ ров корректных предупреждений, если там будут встречаться новые средства. Для большинства программистов самым важным новшеством оказался новый син­ таксис объявления и определения функций. Объявление функции теперь может содер­ жать описание ее аргументов; синтаксис определения должен соответствовать объявле­ нию. Дополнительная информация сильно облегчает компиляторам работу по выявле­ нию ошибок, причиной которых стала несогласованность типов аргументов. Судя по нашему опыту, это очень полезное дополнение к языку. Есть и другие небольшие изменения. Присваивание структур и перечислимые типы, которые давно уже встречались в разных реализациях, официально стали частью языка. Операции с плавающей точкой теперь можно выполнять с одинарной точностью. Более четко определены свойства арифметических операций, особенно над переменными без знака. Препроцессор стал более сложным и развитым. Однако большая часть изменений не очень повлияет на работу программистов. Второе существенное новшество, связанное с введением стандарта, - это определе­ ние стандартной библиотеки функций, включаемой в состав компилятора С. В эту биб­ лиотеку входят функции для обращения к операционной системе (например, для чтения и записи файлов), для форматированного ввода-вывода, распределения памяти, операций со строками и т.п. Совокупность стандартных заголовочных файлов обеспечивает еди­ нообразие обращения к объявлениям функций и типов данных. Программы, которые пользуются этой библиотекой для взаимодействия с операционной системой, являются 16

В ВЕД ЕНИЕ

гарантированно совместимыми с раз ными системными средами. Большая часть библио­ теки построена по принципам стандартной библиотеки ввода-вывода системы Unix. Эта библиотека описывалась еще в первом издании и с тех пор широко использовалась во многих других системах. Большинство программистов и здесь не столкнутся с сущест­ венными изменениями. Поскольку типы данных и управляющие структуры языка С поддерживаются многи­ ми компьютерами непосредственно на аппаратном уровне, библиотека функций, необхо­ димых для реализации автономных, полнофункциональных программ, имеет совсем крошечные размеры. Функции из стандартной библиотеки всегда вызываются только яв­ ным образом, так что их применения легко избежать, если это необходимо. Большинство функций написаны на С и полностью переносимы, за исключением некоторых подроб­ ностей работы операционных систем, которые скрыты на более низком уровне. Хотя язык С соответствует по своей структуре аппаратным возможностям многих компьютеров, он не привязан к какой-либо конкретной машинной архитектуре. При не­ котором усердии на нем легко написать полностью переносимую программу, т.е. про­ грамму, которая будет работать безо всяких изменений на самых разных компьютерах. В стандарте вопросы переносимости четко определены и выведены на явный уровень. Стандартом предписывается использование набора констант, характеризующего архи­ тектуру системы, в которой работает программа. Языку С не свойствен строгий контроль типов, хотя по мере его эволюции развива­ лись средства проверки соответствия типов. В исходной версии С компилятор выдавал предупреждения, но не запрещал взаимную замену указателей и целых переменных раз­ ных типов. Но от этого уже давно отказались, и теперь стандарт требует полной совмес­ тимости объявлений и явного выполнения преобразований типов. Хорошие компиляторы уже давно требовали от программиста того же самого. Еще одним шагом в этом направ­ лении как раз и является новый стиль объявления функций. Компиляторы выдают пре­ дупреждения по поводу большинства ошибок, связанных с несоответствием типов, и не выполняется никакого автоматического преобразования несовместимых типов данных. И все же в С сохраняется традиционное отношение к программисту, как к человеку, ко­ торый сам должен знать и решать, что ему делать; от него просто требуется выражать свои намерения в явной и четкой форме . Как и у любых языков, у С есть свои недостатки: некоторые операции имеют нело­ гичный приоритет; некоторые синтаксические конструкции можно было бы организо­ вать и получше. И тем не менее язык С доказал свою высочайшую эффективность и вы­ разительность для самого широкого круга приложений. Эта книга организована следующим образом. Глава 1 представляет собой краткое учебное пособие по основным конструкциям С. Цель этой главы - быстро ввести чита­ теля в курс дела. Мы убеждены, что лучший способ изучить новый язык - это сразу на­ чать писать программы на нем. В этом учебном пособии предполагается, что читатель имеет некоторые знания по основам программирования, поэтому мы не разжевываем, что такое компьютер, зачем нужен компилятор или что означает выражение n= n+l. Хо­ тя мы и старались везде, где это возможно, демонстрировать полезные приемы програм­ мирования, все-таки книга не является пособием по структурам данных и алгоритмам. Если необходимо было сделать подобный выбор, мы всегда сосредоточивались на во­ просах, связанных с самим языком. В главах 2-6 более подробно и строго, чем в главе 1 , рассматриваются различные ас­ пекты языка С, хотя главное место в изложении по-прежнему занимают примеры законВ ВЕД ЕНИЕ

17

ченных программ, а не изолированные фрагменты кода. В главе 2 рассматриваются ба­ зовые типы данных, операции и выражения. В главе 3 обсуждаются управляющие конст­ рукции: i f - e l s e , s w i t ch, whi l e , for и т.д. Глава 4 посвящена функциям и структуре программы: внешним переменным, области видимости, распределению кода по исход­ ным файлам и т.п. Также в ней рассматривается работа с препроцессором. В главе 5 опи­ саны указатели и операции адресной арифметики. В главе 6 рассматриваются структуры и объединения. Глава 7 содержит описание стандартной библиотеки функций, представляющей собой стандартный интерфейс для взаимодействия с операционной системой. Эта библиотека определена стандартом ANSI и должна поддерживаться во всех системах, в которых реа­ лизован язык С, так что программы, которые пользуются ее функциями для ввода, выво­ да и других операций на уровне операционной системы, должны обладать свойством пе­ реносимости на другие платформы безо всяких изменений. В главе 8 описывается взаимодействие программ на С и операционной системы Unix. Основное внимание уделяется вводу-выводу, файловой системе и распределению памя­ ти. Хотя частично эта глава содержит только информацию, специфическую для систем Unix, программисты, работающие с другими системами, все равно найдут здесь для себя много полезного. Среди прочего это взгляд изнутри на реализацию одной из версий стандартной библиотеки, а также рекомендации по переносимости программ. Приложение А является полным справочником по языку. Официальным документом по синтаксису и семантике языка С является сам стандарт ANSI. Однако стандарт как справочное пособие в основном предназначается для разработчиков компиляторов, в то время как справочник в этой книге описывает язык более доходчиво, кратко и без пере­ груженности официальными выражениями. В приложении Б содержится описание стан­ дартной библиотеки, опять-таки предназначенное скорее для программистов­ пользователей, чем для разработчиков компиляторов. В приложение В включена сводка изменений, внесенных в язык по сравнению с его исходной версией. Однако в случае возникновения каких-либо сомнений и разночтений программисту всегда следует опи­ раться на стандарт и имеющийся в наличии компилятор.

18

ВВЕДЕН ИЕ

Глава 1

Ввод ный урок Начнем изложение с быстрого введения в язык С. Нашей целью будет продемонстри­ ровать основные элементы языка в действии - в реально работающих программах - и при этом не утонуть в подробностях, правилах и исключениях. На этом этапе мы не пре­ тендуем на полноту и даже на строгость (хотя все примеры, насколько мы можем судить, написаны без ошибок). Мы намерены помочь читателю как можно быстрее преодолеть тот барьер, после которого уже можно писать практически полезные программы, и по­ этому наше внимание будет сосредоточено на фундаментальных понятиях: переменных и константах, математических операциях, управляющих конструкциях, функциях, а так­ же основах ввода-вывода. Те средства С, которые необходимы для написания больших программ, в этой главе пока не рассматриваются. Это указатели, структуры, большая часть богатого набора операций, несколько управляющих операторов и стандартная биб­ лиотека функций. В таком подходе есть и свои недостатки. Самый значительный из них - это то, что ни одно конкретное средство языка не описано здесь во всей полноте. Поэтому вводный урок из-за своей краткости может в чем-то и дезориентировать читателя. Поскольку в примерах не используются все возможности С, они написаны не столь кратко и изящно, как это можно было бы сделать. Мы пытались сделать так, чтобы подобных недостатков бьшо как можно меньше. Тем не менее они есть, и мы об этом предупреждаем. Еще один недостаток состоит в том, что в следующих главах вынужденно повторяется часть мате­ риала этой главы. Надеемся, что это повторение окажется скорее полезным, чем надоед­ ливым. Как бы то ни было, опытным программистам должно быть нетрудно перейти к своим практическим потребностям, отталкиваясь от материала этой главы. Начинающим сле­ дует воспользоваться изученным, чтобы написать небольшие учебные программы, ана­ логичные приведенным. И в том и в другом случае читатель может пользоваться мате­ риалом этой главы как основой, на которую можно наложить более подробные сведения, начинающиеся с главы 2.

1 . 1 . Первые ш аг и Самый лучший способ изучить новый язык программирования - это сразу начать писать на нем программы. Программировать на любом языке начинают с такой про­ граммы: Выв е сти слов а

he l l o , worl d

Это не так мало, как кажется. Чтобы преодолеть этот этап, необходимо уметь подго­ товить текст программы, успешно скомпилировать его, загрузить и запустить на выпол­ нение, а затем выяснить, куда и как был выведен результат ее работы. На языке С программа для вывода слов "he l l o , wor l d" выглядит так:

# include < s t d i o . h > ma in ( )

{

print f ( " he l l o , wor l d " ) ;

Процесс выполнения этой программы зависит от системы, в которой вы работаете. Например, в операционной системе Unix необходимо поместить программу в файл, имя которого имеет окончание . с, например hel l o. с. Затем ее следует скомпилировать с помощью следующей команды: се

he l l o . c

Если не было сделано никаких случайных ошибок (таких как пропуск буквы или опи­ ска в слове), то компиляция пройдет без сообщений, и в результате будет создан выпол­ няемый файл а. out . Теперь этот файл можно запустить на выполнение командой a . out В результате его выполнения будет выведена строка he l l o , wor l d В других системах эта процедура будет отличаться. Обратитесь к справочнику или специалисту за подробностями . Теперь рассмотрим саму программу. Программа на С независимо от ее размера со­ стоит из функций и переменных. Функция содержит операторы команды для выпол­ нения определенных вычислительных операций, а в переменных хранятся числа и другие данные, используемые в этих операциях. Функции С аналогичны подпрограммам и функциям языка Fortran или процедурам и функциям языка Pascal. В нашем примере функция имеет имя ma in. Обычно функциям можно давать любые имена по своему же­ ланию, но ma i n особый случай, поскольку программа начинает выполняться именно с начала этой функции. Это означает, что в любой программе на С обязательно должна присутствовать функция ma in. Обычно из ma i n для выполнения разных операций вызываются другие функции, не­ которые из которых программист пишет сам, а другие содержатся в стандартных биб­ лиотеках. В первой строчке программы выводится следующее: -

-

# inc lude < s t d i o . h >

Это указание компилятору включить в программу информацию о стандартной биб­ лиотеке ввода-вывода. Эта же строка имеется в начале многих файлов исходного кода С. Стандартная библиотека описывается в главе 7 и приложении Б. Один из методов передачи данных между функциями заключается в том, что одна функция вызывает другую и предоставляет ей список чисел, именуемых аргументами. Список заключается в круглые скобки и ставится после имени функции. В нашем приме­ ре ma i n является функцией без аргументов, на что указ ывает пустой список в скобках. Первая программа на С # include < s t d i o.h > ma i n ( )

{

20

в ключение информа ции о стандар тной библиотеке определение функции ma in без аргументов операторы ma in з а ключаются в ско бки

ГЛАВА 1

print f ( " he l l o , wor ld\n 11 ) ;

библио течна я функция pri n t f выводит с троку; \ n обознача е т конец с троки

Операторы функции заключаются в фигурные скобки. Функция ma i n в данном при­ мере содержит всего один оператор: p r i nt f ( 11 he l l o , wor l d \ n11 ) ;

Чтобы вызвать функцию, следует ввести ее имя, а затем список аргументов в круглых скобках. Так что здесь вызывается функция p r i nt f с аргументом " hel l o , wor l d\n " . Функция p r i n t f включена в стандартную библиотеку и выводит свои ар­ гументы на экран или на печать. В данном случае это один аргумент в виде строки текста в кавычках. Последовательность символов в двойных кавычках, такая как " hel l o , wo r l d\n " , называется символьной строкой или строковой константой. В данный момент единст­ венным применением таких строк для нас будет их передача в функцию p r i n t f и ана­ логичные функции вывода. Комбинация символов \n в символьной строке является условным обозначением языка С для символа конца строки, который переводит вывод данных на левый край но­ вой строки. Если выбросить \n (поэкспериментируйте для наглядности), то окажется, что после вывода символов переход на новую строку не произойдет. Чтобы включить та­ кой переход в аргумент p r i n t f , необходимо воспользоваться комбинацией \ n . Если попытаться использовать приведенную ниже конструкцию, компилятор С выдаст сооб­ щение об ошибке: print f 11 ) ;

( 1 1 he l l o , wor l d

Функция p r i n t f никогда не вставляет конец строки автоматически, поэтому одну строку вывода можно составить за несколько вызовов этой функции. Наша первая про­ грамма могла бы иметь и такой вид: # inc lude < s t d i o . h > ma i n ( )

{

p r i n t f ( 11 he l l o , 11 ) ; print f ( 11 world11 ) ; print f ( 11 \ n" ) ;

Результат ее работы был бы тот же. Обратите внимание, что комбинация \n представляет один символ. Для обозначения всех символов, которые трудно или невозможно изобразить на письме, используются управляющие последовательности (escape seqиeпces), аналогичные \ n. Среди прочих в С используются комбинации \ t для обозначения знака табуляции, \ Ь для возврата на один символ назад с затиранием (backspace), \ " для двойной кавычки, \ \ для обратной косой черты. В разделе 2.3 представлен полный список таких последовательностей. Упражнение 1 . 1 . Запустите программу " hel l o , wo r l d " на выполнение в вашей сис­ теме. Поэкспериментируйте с ней, выбрасывая определенные фрагменты программы и наблюдая за сообщениями об ошибках, которые будут появляться в связи с этим.

В В ОДНЫЙ УРОК

21

Упражнение 1.2. Попытайтесь выяснить экспериментально, что происходит при переда­ че в функцию print f строки, содержащей управляющую последовательность \с, где с некий символ, не входящий в вышеперечисленные комбинации. -

1.2. П е рем енные и ари ф мети ч еские выр ажен ия В следующей программе выводится таблица температур п о Фаренгейту и их соответ­ ствий по шкале Цельсия. Для этого используется формула 0С = (5/9) (°F 32). Результат работы программы имеет вид о - 17 20 -6 4 40 60 15 80 26 100 37 120 48 140 60 160 71 180 82 200 93 2 20 104 240 1 15 2 60 1 2 6 280 137 300 148 -

Сама программа, как и предыдущая, состоит из определения всего одной функции под именем ma i n. Она несколько длиннее, чем та, которая выводила на экран слова "hello , world", но ненамного сложнее. В ней появляется ряд новых конструкций, таких как комментарии, объявления, переменные, арифметические выражения, циклы и форматированный вывод. #include

/* вывод таблицы темпера тур по Фаренгейту и Цель сию для fahr = О , 20 , . . . , 3 0 0 * / main( )

{

int fahr, celsius; int lower, upper, step; lower upper

step

= =

О;

300 ; 20 ;

/ * нижняя граница температур в таблице * / / * верхняя граница * / / * величина шага * /

fahr lower; while (fahr < = upper) { celsius = 5 * ( f ahr -32 ) printf("%d\t%d\n", f ahr ,

22

/ 9; celsius);

ГЛАВА 1

fahr

fahr + s t ep ;

Следующие две строки программы называются комментарием: / * вывод таблицы температур по Фаренгейту и Цель сию для f ahr = О 2 О , ... , З О О * / ,

В данном случае комментарий содержит краткое описание того, что делает програм­ ма. Любые символы, заключенные между / * и * /, игнорируются компилятором, поэто­

му такую конструкцию можно использовать по своему усмотрению для придания про­ грамме удобочитаемости. Комментарий может стоять везде, где допускается наличие пустой строки, табуляции или пробела. В языке С все переменные необходимо объявить до их использования. Обычно это делается в начале функции до любых выполняемых операторов. Свойства переменных описываются в объявлении, которое состоит из наименования типа и списка переменных, например: int fahr , c e l s ius ; int lowe r , uppe r , s t ep ; Тип int означает, что перечисленные далее переменные являются целочисленными, в отличие от f l oat - вещественных чисел с плавающей точкой, которые могут содер­ жать дробную часть. Диапазон значений как типа int, так и типа f l oat зависит от ап­ паратной и системной платформы. Общеприняты как 1 6-разрядные ( 1 6-битовые) целые числа типа int с диапазоном от -32768 до +32767, так и 32-разрядные. Число типа float обычно представляется 32-битовой переменной, содержащей как минимум 6 зна­ чащих цифр и имеющей диапазон значений от 10-38 до 10+38• Помимо i nt и f l oat, в языке С имеется ряд других элементарных типов данных, таких как char - символ (один байт); s hort - короткое целое число; l ong - длинное целое число; douЫ e - вещественное число двойной точности. Длины этих типов данных также зависят от используемой системы. К тому же суще­ ствуют массивы, структуры и объединения элементарных типов, указатели на них, а также возвращающие их функции. Все это будет изучено в свое время. Непосредственные вычисления в программе перевода температур начинаются с опе­ раторов присваивания: lowe r = О ; upper = 300 ; step = 20 ; fahr = lower ; Эти операторы задают переменным их начальные значения. Операторы отделяются друг от друга точками с запятой. Каждая строка таблицы вычисляется одним и тем же образом, поэтому здесь исполь­ зуется цикл, который выполняется по одному разу на каждую строку результата. Для этого вводится цикл whi le : while (fahr < = upper ) {

ВВОДНЫ Й УРОК

23

Цикл whi l e работает следующим образом. Проверяется условие в скобках. Если оно справедливо (т.е. переменная f ahr меньше или равна uppe r ), то выполняется тело цик­ ла - три оператора, заключенные в фигурные скобки. Затем условие проверяется снова, и если оно по-прежнему справедливо, то тело цикла выполняется опять. Как только про­ верка дает отрицательный результат ( f ahr превосходит по величине upp e r ), цикл за­ канчивается и работа программы продолжается с того оператора, который стоит сразу после цикла. Поскольку в программе больше операторов нет, она на этом завершается. Тело цикла whi l e может состоять из одного или нескольких операторов, заключен­ ных в фигурные скобки, как это видно в программе перевода температур, или из одного оператора без скобок: whi l e ( i < j ) i = 2 * i; В любом случае оператор или операторы, выполняемые в цикле whi l e , выделяются отступом в виде табуляции (в данном случае он эквивалентен четырем пробелам), так что на глаз сразу видно, что именно входит в цикл. Отступы подчеркивают логическую структуру программы. Хотя компиляторам С безразличен внешний вид программы, рас­ становка отступов и пробелов очень важна для того, чтобы сделать программу удобочи­ таемой. Рекомендуется писать по одному оператору в строке, окружать их пробелами и выделять пустыми строками для подчеркивания группировки. Расположение фигурных скобок не так важно, хотя многие программисты лелеют привязанность к той или иной их расстановке. Мы выбрали один из нескольких популярных стилей. Рекомендуется придерживаться в этом вопросе единой системы: выберите один стиль раз и навсегда. Большая часть работы выполняется в теле цикла. Следующий оператор вычисляет температуру по Цельсию и помещает ее в переменную c e l s i u s : c e l s ius = 5 * ( fahr-3 2 ) / 9 ;

Причина того, что использовалось умножение н а 5 и деление н а 9 вместо простого умножения на 5/ 9, состоит вот в чем. В языке С, как и во многих других языках, при де­ лении целых чисел усекается (отбрасывается) дробная часть результата. Поскольку 5 и 9 являются целыми числами, частное 5/ 9 было бы усечено до нуля и все температуры по Цельсию также оказались бы равными нулю. В приведенном примере также продемонстрированы некоторые возможности функ­ ции print f . Это функция форматированного вывода самого широкого назначения. Подробнее о ней рассказывается в главе 7. Ее первый аргумент - это строка выводимых символов, причем каждый знак процента ( % ) обозначает место, куда при выводе следует подставить следующий (второй, третий и т.д.) аргумент функции, а также форму, в кото­ рой эти следующие аргументы следует выводить. Например, %d указывает на целочис­ ленный аргумент, так что следующий оператор осуществляет вывод двух целых значе­ ний переменных f ahr и ce l s i u s с символом табуляции ( \ t) между ними: printf ( " %d\ t %d\n" , fahr , cel s ius ) ; Каждая конструкция с символом % в первом строковом аргументе функции p r i ntf должна иметь соответствие: второй, третий и т.д. аргумент; их количество и типы долж­ ны быть согласованы, иначе будут выданы неверные ответы. Кстати говоря, функция pr int f не является частью определения языка С. В самом языке не определены стандартные конструкции ввода-вывода, так что print f - это просто полезная функция из стандартной библиотеки, к которой обычно имеют возмож­ ность обращаться программы на С. Однако поведение и свойства функции p r i ntf рег-

24

ГЛАВА 1

ламентированы в стандарте ANSI, поэтому она должна работать одинаково во всех биб­ лиотеках и компиляторах, соответствующих этому стандарту. Чтобы сосредоточить внимание на самом языке С, мы отложим рассмотрение вопро­ сов ввода-вывода до главы 7. В частности, пока не будем заниматься форматированным вводом. Если понадобится вводить числа, ознакомьтесь с описанием работы функции s c anf в разделе 7.4. Эта функция аналогична print f , но она считывает данные из по­ тока ввода, вместо того чтобы отправлять их в поток вывода. Программа преобразования температур содержит несколько недочетов. Простейший из них состоит в том, что результат выглядит не очень красиво, поскольку числа не вы­ ровнены по правому краю. Это легко исправить, добавив в каждый элемент % при вызове функции print f ширину поля вывода. В итоге каждое число будет выравниваться по правому краю отведенного ему пространства. Например, с помощью следующего опера­ тора первое число в каждой строке будет выводиться в поле шириной три цифры, тогда как второе - в поле шириной шесть цифр: print f ( " % 3d %6d\n " , fahr , c e l s ius ) ; Результат получится таким: -1 7 о -6 20 4 40 15 60 26 80 100 37 Более серьезная проблема состоит в том, что, поскольку используется целочисленная арифметика, температура по Цельсию вычисляется не очень точно. Например, 0°F соот­ ветствует -17 ,8°С, а не -17. Чтобы получить более точные ответы, следует прибегнуть к вещественной арифметике. Для этого в программу нужно внести некоторые изменения. Вот ее вторая версия: # inc lude < s tdio . h> / * выв од таблицы температур по Фаренгейту и Цель сию для fahr = О , 2 0 , 300 ;

версия с в еще с т в е нной арифметикой * / ma in ( )

{

f l oat fahr , c e l s ius ; int lowe r , upper , s t ep ; lower = О ; upper = 3 0 0 ; step 20;

/ * нижня я граница температур в таблице * / / * верхняя граница * / / * в еличина ша га * /

fahr lower ; whi l e ( fahr < = upper) { c e l s ius = ( 5 . 0 /9 . 0 ) * ( f ahr-3 2 . 0 ) ; print f ( " % 3 . 0 f % 6 . l f \ n " , fahr , cel s ius ) ; fahr = fahr + s t ep ;

Вводный УРОК

25

Эта версия программы в значительной мере совпадает с предыдущей, только пере­ менные fahr и c e ls ius в ней объявлены вещественными, а формула преобразования записана более естественно. Ранее выражение ( 5 / 9) нельзя было использовать из-за его усечения до нуля при целочисленном делении. А вот наличие десятичной точки в записи константы означает, что это вещественное число, поэтому 5 О/ 9 . о не усекается, буду­ чи частным от деления двух вещественных чисел. Если знак математической операции соединяет два целочисленных операнда, то вы­ полняется целочисленная операция. Однако если один из операндов вещественный, а другой - целый, то перед выполнением операции целое число будет преобразовано в вещественное. Если записать выражение f ahr - З 2 , константа З 2 будет автоматически преобразована к вещественному представлению. Но для удобства чтения программы все­ гда лучше записать вещественную константу с десятичной точкой, чтобы подчеркнуть ее вещественную природу, даже если она имеет целое значение без дробной части. Подробно о правилах, которым подчиняется преобразование целых чисел в вещест­ венные, речь пойдет в главе 2. А пока следует знать вот что. Кроме арифметических опе­ раций, мы видели переменные еще в операторах присваивания: fahr = lowe r ; .

Также они использовались в проверке, которая стоит в заголовке цикла: whi l e ( f ahr < = upp e r ) В обоих этих случаях применяется тот ж е принцип: значение типа i n t преобразуется во float перед непосредственным выполнением операции. Спецификация формата из функции print f , а именно %3 . O f, указывает, что следу­ ет вывести вещественное число (в данном случае f ahr) в поле шириной не менее 3 сим­ волов без десятичной точки и дробной части. Спецификация % 6 1 f задает вывод еще одного числа (ce l s ius), которое должно выводить в поле как минимум 6 символов шириной с одной цифрой после десятичной точки. Вот как будет выглядеть результат: о - 17 8 20 -6 . 7 40 4.4 .

.

Ширину и точность представления в спецификации можно по желанию опустить. Так, % 6f задает вывод в поле шириной не менее 6 символов; % . 2 f требует вывода двух цифр после точки, но не ограничивает ширину поля; % f задает вывод десятичного веще­ ственного числа без ограничений. %d

вывести арrумент как десятичное целое число.

%6d

вывести арrумент как десятичное целое число в поле шириной не менее 6 си мволов.

%f

вывести арrумент как вещественное число с плавающей точкой.

%6 f

вывести арrумент как вещественное число в поле шириной не менее 6 символов.

% . 2f

вывести арrумент как вещественное число с двумя цифрами после десятичной точки.

%6 . 2 f

вывести арrумент как вещественное число в поле не короче 6 символов и с двумя цифрами после точки.

В числе прочих спецификаций функция print f понимает % о - вывод восьмерично­ го числа; %х - шестнадцатеричного числа; % с - отдельного символа; % s - строки, а также % % - собственно знака процента.

26

ГЛАВА 1

Упражнение 1 .3. Модифицируйте программу преобразования температур так, чтобы она выводила заголовок над таблицей.

Напишите программу для перевода температур по Цельсию в шкалу Фаренгейта и вывода соответствующей таблицы.

Упражнение 1 .4.

1 . 3 . Оператор

f or

Для каждой конкретной задачи можно написать множество разных версий програм­ мы, которая эту задачу решает. Попробуем написать другой вариант программы преоб­ разования температур: # i nclude < stdio . h> / * выво д таблицы температур по Фаренгейту и Цель сию * /

ma in ( )

{

int fahr ; for ( f ahr = О ; fahr # de f ine # de f ine # de f i ne

LOWER UPPER STEP

о

300 20

/ * нижний предел диапазона * / / * верхний предел * / / * размер ш ага * /

/ * выв од таблицы темпера тур по Фарен гейту и Цел ь сию * / ma i n ( )

{

int f ahr ; for ( f ahr = LOWER ; f ahr < = UPPER ; f ahr = f ahr + STE P ) p r i n t f ( " % 3 d % 6 . l f \ n " , f ahr , ( 5 . 0 / 9 . 0 ) * ( f ahr - 3 2 ) ) ;

Величины LOWER, UPPER и STEP в этом случае являются символическими констан­ тами, а не переменными, поэтому они не фигурируют в объявлениях. Имена символиче­ ских констант обычно записывают прописными буквами, чтобы легко отличать их от

28

ГЛАВА 1

имен переменных в нижнем регистре. Обратите внимание, что строка #de f ine не за­ канчивается точкой с запятой.

1 . 5 . Символ ь н ый ввод- вывод В этом разделе мы рассмотрим семейство программ, выполняющих те или иные опе­ рации по обработке символьной информации. Со временем вы увидите, что многие про­ граммы являются всего лишь расширенными вариантами тех простых прототипов, кото­ рые будут приведены здесь. Модель ввода-вывода, поддерживаемая стандартной библиотекой функций, очень проста. Текстовый ввод-вывод, независимо от его физического источника или места на­ значения, выполняется над потоками символов. Поток символов - это последователь­ ность символов, разбитых на строки; каждая строка заканчивается специальным симво­ лом конца строки и может быть пустой или содержать некоторое количество символов. За то, чтобы привести каждый поток ввода или вывода в соответствие с этой моделью, отвечает стандартная библиотека, тогда как программисту на С, пользующемуся ей, нет нужды заботиться о том, как строки потока представляются вовне программы. В стандартной библиотеке имеется ряд функций для чтения или записи одного сим­ вола за одну операцию; простейшими из них являются get char и put c har. Каждый раз при вызове get char эта функция считывает следующий символ текстового потока ввода и возвращает его в качестве своего значения. Таким образом, после выполнения приведенного ниже оператора переменная с будет содержать следующий символ вход­ ного потока: с = getcha r ( ) ; Обычно символы поступают с клавиатуры. Ввод из файлов рассматривается в главе 7 . Функция putchar при каждом вызове выводит один символ: put char ( c ) ;

Данный оператор выводит значение целочисленной переменной с в виде символа как правило, на экран монитора. Вызовы put char и print f можно чередовать как угод­ но - выводимые данные будут следовать в том порядке, в каком выполняются вызовы.

1 . 5. 1 . Копирование файлов Имея в своем распоряжении функции ge t char и put c har, можно написать удиви­ тельно много полезного программного кода, больше ничего не зная о вводе-выводе. Простейшим примером является программа, копирующая входной поток в выходной по одному символу: прочита ть симв ол whi l e ( символ не является симв олом- конца - файла ) выв е с ти пр о чита нный симв ол про чита ть симв ол

Если все это перевести на С, получится вот что: # inc lude < S t d i o . h > / * копиров ание входного потока в выходной * /

Вводный УРОК

29

main ( )

{

int с ; с = get char ( ) ; whi l e ( с ! = EOF ) put char ( c ) ; с = get char ( ) ;

Знак ! = означает операцию отношения "не равно". Все, что на экране или клавиатуре выглядит как символ, в памяти компьютера, разу­ меется, представлено в виде набора битов. Для хранения таких символьных данных спе­ циально предназначен тип char, но можно использовать и любой другой целочислен­ ный тип. Мы выбрали для этой цели тип int по неочевидной, но важной причине. Сушествует проблема, как отличить конец входного потока от обычного символа данных. Функция g e t char решает ее, возвращая при окончании потока ввода особое значение, которое нельзя перепутать ни с каким настоящим символом. Это значение на­ зывается EOF сокращение от "end-of-file" (конец файла). Необходимо объявить пере­ менную с принадлежащей к типу достаточного объема, чтобы вместить любое значение, возвращаемое из g e t char. Тип char для этих целей не годится, поскольку переменная с должна вмещать как любой возможный символ, так и EOF. Вот почему мы используем тип int . Символическая константа EOF это целое число, определенное в файле < s t d i o . h > , но ее конкретное числовое значение не играет роли, коль скоро оно не сов­ падает ни с одним из возможных значений типа char. Использование символической константы гарантирует, что никакие компоненты программы не окажутся зависимыми от ее конкретного числового значения. Опытные программисты на С написали бы программу копирования потоков короче и экономнее. В языке С любое присваивание является выражением и имеет значение: с = get char ( ) -

-

Значением этого выражения является значение переменной слева от знака равенства после выполнения присваивания. Отсюда следует, что выражение с присваиванием мо­ жет фигурировать в составе других выражений. Если присваивание вводимого символа переменной с внести в проверку условия цикла wh i l e , программу копирования можно переписать в таком виде: # inc lude < s tdio . h> / * копирование входного потока в выходной ; 2 - я версия * / m a in ( )

{

int с ; whi le ( ( с = ge t char ( ) ) put char ( с ) ;

! = EOF )

В заголовке wh i l e вводится символ и присваивается переменной с, а затем выпол­ няется проверка, не обозначает ли он конец файла. Если это не так, выполняется тело

30

ГЛАВА 1

цикла, в котором выводится символ. Затем цикл wh i l e повторяется. Когда достигается конец потока, цикл, а вместе с ним и функция ma i n прекращают работу. В этой версии программы операции ввода концентрируются в одном месте, посколь­ ку остается всего один вызов get char, а объем кода уменьшается. Программа записы­ вается более компактно, и ее становится удобнее читать (при условии, что читатель зна­ ком с данной устойчивой конструкцией). Такой стиль будет встречаться часто. Конечно, им можно чрезмерно увлечься и написать совершенно неудобочитаемый код, но мы по­ стараемся избежать этой тенденции. Скобки вокруг присваивания в условии необходимы. Приоритет операции ! выше, чем операции =, т.е. в отсутствие скобок сравнение ! = выполнялось бы раньше, чем присваивание = . Таким образом, следующие два выражения будут эквивалентны: с = getchar ( ) ! = EOF с = ( get char ( ) ! = EOF ) =

При этом переменная с получает значение О или 1 в зависимости от того, встретился или нет конец файла при вызове g e t c har. В то же время от этого выражения ожидалось совсем другое. Более подробно поговорим об этом в главе 2. Упражнение

1.6. Проверьте, что выражение ge t char ( )

! = EOF действительно равно

1 или О. Упражнение

1.7. Напишите программу для вывода значения константы EOF.

1 .5. 2 . Подсчет символов Следующая программа подсчитывает символы; она похожа н а программу копирова­ ния потоков: # inc lude < S tdio . h> / * подсчет символов в о входном пот оке ; 1 - я версия * / main ( )

{

long nc ; nc = О ; whi l e ( ge t char ( ) ! = EOF ) + + nc ; print f ( " % l d\ n " , nc ) ;

Здесь использована новая операция + + , представляющая собой инкремент, или уве­ личение на единицу. Вместо нее можно было бы записать nc = nc + l , но + +nc пишет­ ся короче и, как правило, работает быстрее. Существует и противоположная операция декремента ( - - ) для уменьшения на единицу. Операции + + и - могут быть префиксными ( + +nc) или постфиксными (nc + + ). В выражениях эти два способа записи дают разные результаты, как будет показано в главе 2. Но в любом случае как + +nc, так и nc + + инкре­ ментируют переменную nc. Пока что будем придерживаться префиксной формы записи. Программа подсчета символов накапливает счет в переменной типа l ong вместо i nt. Переменные типа l ong имеют длину не менее 32 бит. Хотя в некоторых системах типы int и l ong имеют одинаковую длину, в других тип int является 1 6-разрядным и

Вводный УРОК

31

может

хранить числа не более 32 767 . Чтобы переполнить счетчик типа int , потребует­ ся не такой уж длинный поток. Спецификация вывода % l d сообщает функции p r int f , что соответствующий аргумент имеет тип l ong - длинное целое число. Можно работать со счетчиком даже большей емкости, если использовать для него тип douЫ e (аналог f l oa t , но с двойной точностью). Вместо whi l e можно применить цикл for, чтобы проиллюстрировать другой способ записи циклов: #include < S tdio . h>

/ * подсчет символов в о входном пот оке ; 2 - я версия * / main ( )

{

douЫ e nc ; =

for ( nc

О ; get char ( )

! = EOF ; + +nc )

print f ( " % . O f \ n " , nc ) ; В функции p r i n t f спецификация % f используется и для f l oa t , и для douЫ e ; конструкция % о f подавляет вывод десятичной точки и дробной части, которая равна нулю. Тело данного цикла f o r пусто, поскольку все операции выполняются при проверке условия и модификации переменных. Однако грамматика С требует, чтобы оператор f o r имел тело. Это требование удовлетворяется наличием пустого оператора точки с запятой. У нас в программе он ставится в отдельной строке, чтобы выделить тело цикла. Прежде чем закончить с программой подсчета символов, сделаем одно замечание. Если во входном потоке совсем нет символов, то проверка условия в whi l e или f o r да­ ет отрицательный результат при первом же вызове get char, и программа выдает нуль, который и является правильным ответом. Это важно. В операторах whi l e и f o r хоро­ шо именно то, что проверка выполняется в начале цикла перед входом в его тело. Если делать в цикле нечего, то ничего и не делается, поскольку до операторов тела управление так и не доходит. Программы должны действовать корректно, встретив поток ввода ну­ левой длины. Операторы wh i l e и end помогают безошибочно обрабатывать разного рода предельные случаи. •

-

1 .5.3 . Подсчет строк Следующая программа подсчитывает количество строк во входном потоке. Как уже упоминалось, за представление потока ввода в виде последовательности строк, заканчи­ вающихся специальными символами, отвечает стандартная библиотека. Таким образом, подсчет строк сводится к подсчету символов конца строки в потоке: #inc lude < s tdio . h >

/ * подсчет строк в о входном пот оке * / ma in ( )

{

int с , n l ; nl = О ;

32

ГЛАВА 1

whi l e ( ( с = getchar ( ) ) if ( с == \n ' ) + +nl ; p r i nt f ( " % d \ n " , nl ) ;

!

=

EOF )

'

Тело цикла whi l e в этом случае состоит из оператора i f , который отвечает за при­ ращение счетчика + + n l . В операторе i f проверяется условие, стоящее в скобках, и если оно истинно, то выполняется оператор или группа операторов в фигурных скобках после условия. И снова подчиненность или вложенность операторов в программе подчеркнута с помощью отступов. Двойной знак равенства ( = = ) в языке С используется для записи отношения "равно" (как = в языке Pascal или . E Q . в Fortran) . Этот символ используется для того, чтобы от­ личать проверку равенства от присваивания, которое в С выражается одним знаком ра­ венства ( = ) . Одно предостережение: новички в С часто пишут присваивание ( = ) , имея в виду равенство ( = = ) . Как будет показано в главе 2, сложность состоит в том, что в ре­ зультате обычно получается синтаксически правильное выражение, поэтому даже не бу­ дет выдано предупреждение. Символ, записанный в одинарных кавычках, представляет числовое значение, равное коду символа в символьном наборе системы. Такой символ называется символыюй кон­ стантой, хотя на самом деле это еще один способ записи небольшого целого числа. На­ пример, ' А ' является символьной константой; в символьном наборе ASCII ее код равен 65 , и это - внутреннее числовое представление символа А. Разумеется, запись ' А ' сле­ дует предпочесть записи 6 5: ее смысл более очевиден, и константа будет независимой от конкретного символьного набора. Управляющие комбинации, используемые в строковых константах, разрешаются также и в символьных константах, так что, например, ' \ n ' обозначает символ конца строки с АSСП-кодом, равным 1 0. Уясните себе важное различие: константа ' \ n ' - это один символ, и в выражениях она является просто целым числом, тогда как " \ n " - это строковая константа, которая по случайности содержит всего один символ. Тема строко­ вых и символьных констант рассматривается более подробно в главе 2. Напишите программу для подсчета пробелов, знаков табуляции и сим­ волов конца строки.

Упражнение 1.8.

Напишите программу для копирования входного потока в выходной с заменой каждой строки, состоящей из одного или нескольких пробелов, одним пробелом. Упражнен ие 1.9.

Напишите программу для копирования входного потока в выходной с заменой знаков табуляции на \ t , символов возврата назад (Backspace) на \ Ь , а обрат­ ных косых черт - на \ \ . Это сделает табуляции и символы возврата легко читаемыми в потоке. Упражнение 1 . 1 0 .

1 . 5 . 4 . Подсчет слов Четвертая программа и з нашей серии полезных утилит для обработки текстовых по­ токов подсчитывает строки, слова и символы. Используется нестрогое определение сло­ ва как последовательности символов, не содержащей пробелов, табуляций и символов новой строки. Это сильно урезанный "скелет" программы wc для системы Unix.

Вводный УРОК

33

# include < S tdio . h> #de f ine IN 1 #de f ine ОUТ О

/ * в нутри слов а * / / * снаружи слова * /

/ * подсчет с трок , слов и симв олов в о входном пот оке * / main ( )

{

int с , nl , nw , nc , state ; state = OUT ; nl = nw = nc = О ; whi l e ( ( с = get char ( ) ) ! = EOF ) { + +nc ; i f ( с = = ' \n ' ) + +nl ; if ( с == ' 1 1 с = = ' \n ' 1 1 с state = OUT ; e l s e i f ( s tate = = OUT ) { state = IN ; + + nw ;

1 \t 1 )

}

print f ( 11 % d % d %d\n " , nl , nw , nc ) ; Всякий раз, когда программа встречает первый символ слова, она прибавляет едини­ цу к счетчику слов. Переменная s t at e учитывает состояние потока: находимся ли мы внутри слова или нет. Вначале она устанавливается в состояние "вне слова", что соответ­ ствует значению OUT. Мы предпочитаем символические константы IN и OUT явным чи­ словым 1 и О, поскольку так программу удобнее читать. В маленьких программах напо­ добие приведенной это не так важно, однако в больших улучшение удобочитаемости, достигаемое за счет применения символических констант, более чем окупает те скром­ ные усилия, которые нужно приложить, чтобы с самого начала писать в этом стиле. Вы скоро убедитесь сами, что вносить глобальные изменения в программы гораздо легче, если числовые константы фигурируют в них только под символическими именами. Следующая строка обнуляет сразу три переменные: пl = nw = nc = О ;

Это не какая-нибудь специальная форма присваивания, а прямое следствие того фак­ та, что оператор присваивания является выражением, имеющим значение, и что при­ сваивание выполняется справа налево. Можно было бы записать и так: nl =

if

( nw =

( nc = О ) ) ;

Знак 1 1 обозначает логическую операцию ИЛИ: (с = = ' 1 1 с == ' \n ' 1 1 с = = ' \ t ' )

Поэтому данная строка означает "если с пробел, или с символ конца строки, или с табуляция". (Вспомните, что \ t является визуальным представлением невиди­ мого символа табуляции.) Существует также знак && для операции И; его приоритет вы­ ше, чем у 1 1 Выражения, в которых аргументы соединяются знаками && и 1 1 . вычис­ ляются слева направо, причем вычисление всегда прекращается в том месте, в котором гарантированно достигается значение П РАВДА или ЛОЖЬ . Так, если с пробел, то нет -

-

-

·

-

34

ГЛАВА 1

нужды выяснять, является ли этот символ еще и концом строки или табуляцией, и соот­ ветствующие сравнения просто не выполняются. Здесь это не особенно важно, а вот в более сложных ситуациях это имеет значение, как вы вскоре убедитесь. В приведенном примере фигурирует также конструкция e l se, которая задает аль­ тернативные операции на тот случай, если проверка условия в i f дает отрицательный результат. Вот общая форма оператора i f :

i f ( выражение) опера торl else опера тор2 Выполняется всегда только один оператор из двух, ассоциированных с конструкцией опера т ор l ; в против­ ном случае выполняется опера т ор2. Каждый из этих опера т оров может быть и бло­ ком операторов в фигурных скобках. В программе подсчета слов после e l se стоит еще один оператор i f , управляющий двумя операторами в фигурных скобках.

i f - e l s e . Если выражение равно П РАВДА (TRUE), выполняется

Упражнение 1 . 1 1 .

Как бы вы протестировали программу подсчета слов? Какого рода входной поток скорее всего выявит ошибки в программе (если таковые есть)?

Упражнение 1.12.

Напишите программу для вывода входного потока по одному слову в

строке.

1 . 6 . М ассивы Напишем программу, которая бы подсчитывала количество каждой из цифр, количе­ ство символов пустого пространства (пробелов, табуляций, переходов на новую строку) и количество всех остальных символов во входном потоке. Задача довольно искусствен­ ная, но позволяет проиллюстрировать несколько аспектов языка С в одной программе . Итак, у нас есть двенадцать возможных категорий входных данных. Удобно завести массив, чтобы держать в нем количество вхождений каждой цифры, вместо того чтобы объявлять десять отдельных переменных. Вот первая версия такой программы :

# include < s tdio . h> /* подсчет цифр , символов пусто го простран с т в а , о с тал ь ных */ main ( )

{

int с , i , nwhi t e , nothe r ; int ndigit [ l O ] ; nwhite = nother = О ; for ( i = О ; i < 1 0 ; + + i ) ndigi t [ i ] = О ; whi l e ( ( с = getchar ( ) ) ! = EOF ) i f ( с >= ' 0 ' && с < = ' 9 ' ) ++ndigi t [ с - ' О ' ] ; else i f ( с == ' 1 1 с = = ' \n ' + +nwhi t e ; else

Вводный УРОК

1 1 с

1 \t 1 )

35

+ +nothe r ; print f ( " d igi t s = " ) ; for ( i = О ; i < 1 0 ; + + i ) print f ( " % d " , ndi g i t [ i ] ) ; print f ( " , whi t e space = % d , other nwh i t e , nothe r ) ;

%d\n " ,

Если задать программе в качестве исходных данных ее же исходный текст, получим следующий результат: digi t s = 9 3 О О О О О О О 1 , whi t e space = 2 1 1 , o t her = 3 6 5 Вот как объявляется массив n d i g i t из 1 0 целых чисел : int ndi g i t [ l O ] ;

В языке С индексы массива всегда начинаются с нуля, т.е. этот массив содержит эле­ менты ndi g i t [ О ] , nd i g i t [ 1 ] , . . . , nd i g i t [ 9 ] . Этот факт отражен в заголовках цик­ лов f o r, которые инициализируют, а затем выводят массив. Индексом может служить любое целочисленное выражение, в том числе целые пере­ менные наподобие i или явные константы. Данная программа по умолчанию полагается на наличие определенных свойств у символьного представления цифр. Так, следующий оператор определяет, является ли цифрой символ в переменной с : i f ( с > = ' 0 ' && с < = ' 9 ' ) Если это так, то вычисляется значение этой цифры: с - '0' Эти операции выполняются корректно, только если константы ' О ' , ' 1 ' , . . . ' 9 ' следуют друг за другом строго в порядке возрастания их числовых значений. К счастью, это так во всех символьных наборах и кодировках. По определению величины типа char являются всего лишь короткими целыми чис­ лами, так что в арифметических выражениях переменные и константы этого типа ведут себя так же, как i n t . Это и естественно, и удобно; например, с - ' о ' - это целочис­ ленное выражение со значением между О и 9, соответствующее одному из символов от ' О ' до ' 9 ' , помещенному в переменную с. Следовательно, это выражение может слу­ жить индексом массива n d i g i t . Решение о том, является л и символ цифрой, пробелом, табуляцией, концом строки или же другим символом, принимается в следующем фрагменте программы : i f ( с > = ' О ' && с < = ' 9 ' ) + + ndi g i t [ с - ' о 1 ] ; \t ) else if ( с == ' 1 1 с == ' \n ' 1 1 с 1

1

+ + nwhi t e ;

el se + + nothe r ;

Такая конструкция часто встречается в программах там, где требуется принимать многовариантное решение. Вот ее общий вид: if

( условиеl ) опера торl e l s e i f ( условие2 ) опера тор2

36

ГЛАВА 1

else опера торN Условия проверяются сверху вниз, пока одно из них не окажется выполненным. В этом случае программа переходит к выполнению соответствующего опер а тор а , и работа всей конструкции завершается. (На месте каждого из о пер а т ор ов может стоять блок операторов в фигурных скобках. ) Если не выполняется ни одно из условий, управ­ ление передается на опера т орN после последнего e l s e (если он присутствует). Если же завершающего e l s e и о п ер а т ор а после него нет, ничего не делается. Между на­ чальным i f и конечным e l s e можно вставить сколько угодно групп такого вида: e l s e i f ( условие ) опера тор

Что касается стиля, то лучше форматировать эту конструкцию так, как было показа­ но. Если каждый i f сдвигать относительно предыдущего e l s e вправо, то длинная це­ почка проверок условий "уедет" за правый край страницы . Еще один путь принятия решений путем выбора из многих вариантов предоставляет оператор s wi t c h, который рассматривается в главе 3. Этот способ особенно удобен, ес­ ли проверка условия состоит в том, равно ли некоторое целочисленное или символьное выражение одной из констант заданного набора. Версия приведенной выше программы с оператором swi t ch будет продемонстрирована в разделе 3 .4. Упражнение 1.13. Напишите программу для вывода гистограммы длин слов во входном потоке. Построить гистограмму с горизонтальными рядами довольно легко, а вот с вер­ тикальными столбцами труднее.

Напишите программу для вывода гистограммы частот, с которыми встречаются во входном потоке различные символы.

Упражнение 1.14.

1 . 7 . Функ ции

Функция в С - это эквивалент понятий подпрограммы и функции в языке Fortran или процедуры и функции в языке Pascal. Функции - это удобный способ свести в одно ме­ сто (инкапсулировать) некоторые вычислительные операции, а затем обращаться к ним много раз, не беспокоясь об особенностях реализации. Если функция написана правиль­ но, нет нужды знать, как она работает; достаточно знать, что именно она делает. В языке С пользоваться функциями легко, удобно и эффективно. Часто можно увидеть, как какая­ нибудь короткая функция определяется для того, чтобы ее вызвали один раз, - только потому, что это проясняет смысл и структуру кода. До сих пор мы пользовались только такими функциями, как pr i nt f , g e t char и put char, написанными кем-то другим. Пора уже самим написать несколько собствен­ ных функций. Поскольку в языке С нет операции возведения в степень наподобие * * в Fortran, воспользуемся случаем и продемонстрируем процесс опре,..\ еления функций, на­ писав функцию powe r ( m , n ) для возведения целого числа m в целую положительную степень n. Например, значением powe r ( 2 , 5 ) будет З 2 . Эта функция не слишком при­ менима на практике, поскольку она работает только с положительными степенями не-

вводный УРОК

37

больших целых чисел, но для иллюстративных целей вполне подойдет. (В стандартной библиотеке имеется функция pow ( х , у ) , вычисляющая хУ .) Ниже приведены функция powe r и программа, выполняющая ее вызов. Таким обра­ зом, здесь продемонстрирован весь процесс работы с функциями.

# include < s tdio . h> int power ( int m , int n ) ; / * т е стирование power - функции возведения в степе нь * / main ( )

{

int i ; for ( i = О ; i < 1 0 ; + + i ) print f ( " %d %d %d\n " , i , power ( 2 , i ) , power ( - 3 , i ) ) ; return О ;

/ * power : возв одит ba s e в n - ю степень ; n > = О * / int power ( int base , int n )

{

int i , р ; р = 1; for ( i = 1 ; i < = n ; + + i ) р = р * base ; return р ; Определение функции имеет следующую форму:

тип - в озвращ- зна ч имя - функции ( объявления параме тр ов )

{

объявления опера торы

Определения функций могут следовать в любом порядке, а также находиться как в одном файле исходного кода, так и в нескольких; однако запрещается разбивать одну функцию между несколькими файлами. Если программа состоит из нескольких файлов исходного кода, необходимо выполнить больше операций по ее компилированию и за­ грузке, чем в случае одного файла. Но это уже особенности работы с операционной сис­ темой, а не атрибуты языка. Пока предположим, что обе функции находятся в одном файле, так что остается в силе все, что мы до сих пор говорили о выполнении программ на с. Функция powe r вызывается из ma i n дважды в следующей строке :

print f ( " %d %d %d\n " , i , power ( 2 , i ) , power ( - 3 , i ) ) ; При каждом вызове передаются два аргумента, а возвращается одно целое число, ко­ торое и выводится согласно спецификации. В выражениях конструкция powe r ( 2 , i ) может употребляться точно так же, как обычные целые константы и переменные, напри­ мер 2 или i . (Не все функции возвращают целые значения; подробнее об этом будет ска­ зано в главе 4.)

38

ГЛАВА 1

Рассмотрим первую строку самой функции power:

int power ( int base , int n) В ней объявляются типы и имена параметров, а также тип результата, возвращаемого функцией. Имена, которые используются в powe r для параметров, локализованы в са­ мой функции и невидимы за ее пределами, так что другие функции программы могут пользоваться теми же именами безо всяких конфликтов. Это верно также для перемен­ ных i и р: счетчик i в функции powe r никак не связан с переменной i в ma in. Будем называть параметром переменную и з заключенного в скобки списка в заго­ ловке функции, а аргументом - значение, подставляемое при вызове функции. Иногда в том же смысле используют термины формш�ьный и фактический аргумент. Значение, вычисляемое функцией power, возвращается в ma i n оператором return. После слова r e t urn может стоять любое выражение:

return

выражение ;

Функция не обязана возвращать значение. Если записать оператор r e turn без выра­ жения после него, то он передаст в вызывающую функцию управление, но не значение, точно так же, как это делается при достижении конца функции - закрывающей фигур­ ной скобки. Можно заметить, что оператор return стоит и в конце функции ma in. Поскольку ma in - такая же функция, как и другие, она может возвращать значение в ту точку, от­ куда ее вызывают. А это фактически та операционная среда, в которой выполняется про­ грамма. Обычно возвращение нуля обозначает нормальное завершение программы, а не­ нулевого значения - нестандартные или ошибочные условия завершения. В целях уп­ рощения мы не ставили return в конце наших функций ma i n до этого момента, но теперь начнем это делать, чтобы подчеркнуть, что программам полезно посылать отчет о корректности своего завершения в операционную среду. Перед функцией ma i n можно заметить такую строку:

int power ( int m , int n ) ; Она сообщает, что powe r является функцией с двумя аргументами типа int, воз­ вращающей также значение типа i n t . Эта декларация называется прототипом функции и обязана согласовываться по форме как с определением (заголовком) функции powe r, так и с любыми ее вызовами. Если определение функции отличается от ее прототипа или формы вызова, возникает ошибка. Имена параметров согласовывать не обязательно. Вообще-то не обязательно даже указывать имена параметров в прототипе функции, поэтому предыдущий прототип мож­ но было бы записать так:

int power ( int , int ) ; Однако хорошо выбранные имена являются своеобразной документацией к програм­ ме, так что они будут часто использоваться. Сделаем одно замечание касательно истории языка. Самые большие изменения в ANSI С по сравнению с более ранними версиями произошли как раз в форме объявления и определения функций. В первоначальном синтаксисе языка С функция powe r была бы записана следующим образом : /* power : возводит base в n - ю степень ; n >= О */ /* версия в старом стиле */

power (base , n ) int bas e , n ; Вводный УРОК

39

int i , р ; р = 1; for ( i = 1 ; i < = n ; + + i ) р = р * base ; return р ; Имена параметров приведены в скобках, а вот типы объявлены перед открывающей левой фигурной скобкой. Необъявленные параметры считаются имеющими тип int. (Тело функции не отличается от предыдущей версии.) Объявление функции powe r в начале программы выглядело бы так: int power ( ) ; Список параметров в старой версии не предусматривался, поэтому компилятор не мог проверить, корректно ли вызывалась функция powe r. Если еще учесть, что тип воз­ вращаемого из powe r значения и так по умолчанию принимался i n t , все это объявле­ ние можно было бы просто опустить. Новый синтаксис прототипов функций позволяет компилятору очень легко выявлять ошибки в количестве аргументов или их типах. Старый стиль объявлений и определений функций все еще работает в ANSI С, по крайней мере в переходный период, но мы на­ стоятельно рекомендуем пользоваться только новой формой, если компилятор ее под­ держивает. Упражнение 1 . 1 5 . Перепишите программу преобразования температур из раздела так, чтобы само преобразование выполнялось функцией.

1 .2

1 .8 . Аргумент ы : переда ч а по зна ч ению Один из аспектов функций С может оказаться новым для программистов, привыкших к другим языкам, в частности к Fortran. В языке С все аргументы функций передаются "по значению". Это означает, что вызываемая функция получает значения своих аргу­ ментов в виде временных переменных, а не оригиналов. Отсюда следуют несколько дру­ гие свойства функций, чем в языках, где происходит "передача по ссылке", - как в Fortran или в блоке var в языке Pascal, где вызывающая процедура имеет доступ к ис­ ходным аргументам, а не локальным копиям. Основное различие между этими подходами заключается в том, что в С вызываемая функция не может модифицировать переменные в вызывающей функции; она вправе из­ менять только свои локальные, временные копии этих переменных. Передача по значению - это не ограничение, а благо. Обычно благодаря ей про­ граммы пишутся компактнее и с меньшим количеством лишних переменных, поскольку в вызываемой функции параметры можно воспринимать как удобно инициализирован­ ные локальные переменные. Например, вот версия функции powe r, эффективно исполь­ зующая это свойство: / * powe r : в о з в о дит base в n - ю с т е п е н ь ; n >= О ; 2 - я версия * / int power ( int base , int n )

40

ГЛАВА 1

int р ; for (р

=

1; n

>

О ; - -n)

р = р * base ; return р ;

Параметр n используется в качестве временной переменной для обратного отсчета в цикле for, идущем "вспять". Цикл заканчивается, когда n становится равной нулю. По­ требность в переменной i отпадает. Операции, выполняемые над n внутри функции power, никак не влияют на исходный аргумент, с которым эта функция вызывалась. По необходимости можно сделать так, чтобы функция модифицировала переменные в вызывающем модуле программы. Для этого вызывающая функция должна передать адрес переменной (в понятиях С - указатель на переменную), а вызываемая - объя­ вить параметр указателем и обращаться к переменной косвенно, по ссылке через указа­ тель. Работа с указателями рассматривается в главе 5 . С массивами происходит друтая история. Если имя массива используется в качестве аргумента, то передаваемое в функцию значение как раз и является местонахождением (адресом) начала массива - копирования элементов массива не происходит. Применяя к переданному аргументу индекс, функция может обращаться к любому элементу массива и модифицировать его. Этому будет посвящен следующий раздел.

1 . 9 . Массивы си м волов

Наиболее распространенный тип массивов в С - это массив символов. Чтобы проил­ люстрировать работу с такими массивами и функции, предназначенные для этого, напи­ шем программу, которая бы считывала набор строк и выводила самую длинную из них. Алгоритм программы довольно прост: whi l e ( по ступа е т следующа я с трока ) i f ( она длинне е предыдущей самой длинной) сохранить е е сохранить е е длину выв е с ти самую длинную с троку

Из алгоритма ясно, что программа естественным образом делится на составные час­ ти. Одна часть занимается вводом новой строки, друтая - ее анализом, третья - сохра­ нением, а оставшаяся - управлением работой всей программы. Раз уж имеется такое четкое разделение обязанностей, программу надо написать так, чтобы это было отражено. Соответственно, вначале напишем отдельную функцию ge t l ine для считывания следующей строки входного потока. Попробуем написать ее так, чтобы она оказалась полезной и для друтих приложений. Как минимум, get l i ne должна уметь возвращать сигнал о конце потока; но еще полезней было бы, если бы она возвращала длину введенной строки и О в том случае, если встретился конец файла. Нуль подходит в качестве сигнала конца файла, поскольку реальная строка данных не может иметь такую длину. В любой текстовой строке есть как минимум один символ; даже строка, содержащая только символ конца строки, имеет длину 1 .

Вводный УРОК

41

Когда встречается строка длиннее, чем предыдущая самая длинная строка, ее нужно где-то сохранить. Поэтому нужно написать вторую функцию, сору, которая бы копиро­ вала новую строку в некое надежное место. Наконец, нужна главная программа для управления работой ge t l ine и сору. Вот что получается в результате: # inc lude < stdio . h > / * максимальная длина строки в потоке * / #de f ine МAXLINE 1 0 0 0

int get l ine ( char l ine [ ] , int max l i ne ) ; voi d copy ( char to ( ] , char f rom [ ] ) ; / * вывод самой длинной строки в потоке * / main ( )

{

/ * длина т екущей строки * / int len ; / * текущая максимальная длина * / int max ; / * текущая введенная строка * / char l ine [МAXLINE ] ; char longest [МAXLINE ] ; / * самая длинная строка из введенных * / max = О ; whi le ( ( l en get l ine ( l ine , МAXLINE ) ) > О ) i f ( l en > max ) { len ; max copy ( l onge s t , l ine ) ;

}

i f ( max > О ) / * была непустая строка * / print f ( " % s " , l onge s t ) ; re turn О ; / * get l ine : считывает строку в s , в о з вращает ее длину * / int get l ine ( char s [ ] , int l im )

{

int с , i ; for ( i s [i] if ( с == s (i] ++i ;

О ; i < l im- 1 && ( c =get char ( ) ) ! = EOF && c ! = ' \ n ' ; + + i ) = с; ' \n ' ) = с;

}

s [i] = ' \0 ' ; return i ; / * сору : копирует строку ' f rom ' в ' t o ' ; длина to счит ается доста ­ точной * / void copy ( char to ( ] , char f rom [ ] )

{

int i ; i = О; whi l e ( ( to [ i ]

42

f rom [ i ] )

! = 1 \о 1 ) ГЛАВА 1

++i ; Функции ge t l ine и сору объявлены в начале программы, которую будем считать хранящейся в одном файле. Функции ma i n и ge t l i ne обмениваются информацией через пару аргументов и возвращаемое значение. В get l ine аргументы объявляются в следующей строке: int get l ine ( char s [ ] , int l im ) Здесь сказано, что первый аргумент s - это массив, а второй, l im, - целое число. Размер массива необходимо указывать при его объявлении, чтобы выделить достаточно памяти. Но знать длину массива s в функции ge t l ine не обязательно, поскольку эта длина определяется в ma in. Из функции ge t l ine в функцию ma i n с помощью опера­ тора re turn возвращается числовое значение - точно так же, как это делалось в функ­ ции powe r. В этой же строке объявляется и тип возвращаемого значения - i n t ; по­ скольку этот тип принимается по умолчанию, его можно опустить. Некоторые функции возвращают полезные значения, тогда как другие, наподобие сору, только выполняют какие-то операции и ничего не возвращают. Тип возвращаемо­ го значения в функции c opy - vo i d, что и означает его отсутствие. Функция get l ine помещает в конец созданного ею массива символ ' \ О ' (нулевой символ, числовой код которого равен нулю) , чтобы обозначить конец строки символов. Этот подход принят в языке С за стандарт. Пусть, например, в программе на С фигури­ рует такая строковая константа: " he l lo\n " В памяти она будет размещена как массив символов, содержащий по порядку все символы строки и заканчивающийся ' \ О ' для обозначения конца: h

е

1

1

о

1

\n

1



1

Спецификация формата % s в вызове p r i nt f предполагает, что соответствующий ар­ гумент будет строкой, представленной именно в такой форме. Функция с ору также рас­ считывает на то, что ее аргумент-источник оканчивается на ' \ О ' , причем она копирует этот символ во вторую строку-аргумент. (При всем этом подразумевается, что символ ' \ О ' не может встречаться в нормальном тексте.) Стоит заметить, что даже такая крохотная программа, как эта, содержит несколько нерешенных технических вопросов. Например, что делать функции ma i n, если ей встре­ чается строка длиннее предельной длины? Функция ge t l ine в этом отношении безо­ пасна, поскольку прекращает ввод, как только массив заполнился, даже если еще не встретился символ конца строки. Проверяя длину строки и последний введенный символ, функция ma i n может определить, не чрезмерную ли длину имеет строка, а затем обра­ ботать эту ситуацию по своему усмотрению. Для краткости мы эту проблему пока игно­ рируем. Пользователь функции get l ine никак не может знать заранее, какой длины будет вводимая строка, поэтому get l ine выполняет проверку на переполнение. С другой стороны, пользователь с ору уже знает (или может выяснить) длину строки, поэтому мы решили не добавлять в нее контроль ошибок. Доработайте главный модуль программы определения самой длинной строки так, чтобы она выводила правильное значение для какой угодно длины строк входного потока, насколько это позволяет текст.

Упражнение 1.16.

Вводный УРОК

43

Упражнение 1 . 1 7 . Напишите программу для вывода всех строк входного потока, имею­ щих длину более 80 символов. Упражнение 1 . 1 8 . Напишите программу для удаления лишних пробелов и табуляций в хвосте каждой поступающей строки входного потока, которая бы также удаляла полно­ стью пустые строки.

Напишите функцию reve r s e ( s ) , которая переписывает свой стро­ ковый аргумент s в обратном порядке. Воспользуйтесь ею для написания программы, которая бы выполняла такое обращение над каждой строкой входного потока по очереди.

Упражнение 1 . 1 9 .

1 . 1 О. В не ш ние пере м енные Переменные в функции ma i n, такие как l ine, l onge s t и т.п., являются закрытыми или локальными для ma in. Поскольку они объявлены внутри тела main, ни одна другая функция не может иметь к ним непосредственного доступа. То же самое справедливо и для переменных других функций; например, переменная i в ge t l i ne никак не связана с i в функции с ору. Каждая локальная переменная в функции начинает свое существова­ ние при вызове функции и прекращает его при выходе из нее. Вот почему такие пере­ менные обычно называются автоматическими, как и в других языках. В дальнейшем в отношении подобных локальных переменных будет употребляться термин "автоматичес­ кие", а в главе 4 вы узнаете о статическом ( s t а t i с) классе памяти, в котором локаль­ ные переменные сохраняют свои значения между вызовами функции. Поскольку автоматические переменные создаются и уничтожаются в связи с вызова­ ми функций и выходами из них, они не сохраняют свои значения между вызовами и должны инициализироваться явным образом при каждом входе в функцию. Если их не инициализировать, они будут содержать случайный "мусор". В противоположность автоматическим переменным, можно определить переменные, которые будут внешними по отношению ко всем функциям, т.е. такие переменные, к ко­ торым любая функция программы сможет обращаться по имени. (Этот механизм - ана­ лог блока COMMON в языке Fortran или переменных языка Pascal, объявленных в самом внешнем блоке программы.) Поскольку внешние переменные доступны во всей про­ грамме глобально, их можно использовать вместо аргументов для обмена данными меж­ ду функциями. Более того, поскольку внешние переменные существуют непрерывно, а не появляются и исчезают, они сохраняют свои значения даже после выхода из функций, которые модифицируют их значения. В нешняя переменная должна быть определена ровно один раз за пределами всех функций программы. При этом для нее выделяется память. Она также должна быть объ­ явлена в каждой функции, которая к ней обращается, с указанием ее типа. Объявление может быть явным, с помощью оператора ext e rn, а может быть и неявным - по кон­ тексту. Чтобы придать нашему изложению конкретность, перепишем программу опреде­ ления самой длинной строки, сделав l i ne, l onge s t и max внешними переменными. При этом придется изменить способы вызова, объявления и тела всех трех функций: # inc lude < S tdio . h > #de f ine МAXLINE 1 0 0 0 / * ма ксимал ь н а я длина с тр о ки в п о т о ке * / int max ;

44

/ * т е куща я максимал ь на я длина * /

ГЛАВА 1

char l ine [МAXL I NE ] ; char longe s t [МAXL I NE ] ;

/ * текущая в в еденная строка * / / * самая длинная строка из в веденных * /

int get l ine ( vo i d ) ; vo id copy ( voi d ) ; / * вывод самой длинной строки в потоке ; специал ь н а я в ерсия * / rna in ( )

{

int l en ; extern int rnax ; extern char longe s t [ ] ; rnax = О ; wh i l e ( ( l en ge t l ine ( ) ) > О ) i f ( l en > rnax ) { rnax len ; сору ( ) ;

}

i f ( rnax > О ) / * была непус т а я строка * / print f ( " % s " , l onge s t ) ; return О ; / * get l ine : специал ь н а я версия * / int get l ine ( vo i d )

{

int с , i ; exte rn char l ine [ ] ; for ( i = О ; i . Эта те­ ма подробно раскрыта в главе 4, а сама библиотека рассматривается в главе 7 и прило­ жении Б. Поскольку специальные версии функций g e t l ine и сору не имеют аргументов, по логике их прототипы в начале файла должны выглядеть как g e t l ine ( ) и сору ( ) . Од­ нако для совместимости со старыми программами на С в стандарте предполагается, что пустой список аргументов - это объявление в старом стиле и что компилятор отключает все проверки списка аргументов. Поэтому для описания действительно пустого списка аргументов следует использовать слово vo i d. Это будет более подробно обсуждаться в главе 4. Следует отметить, что слова объявление и определение в этом разделе употребляются с особой тщательностью, когда речь идет о внешних переменных. "Определение" - это место, в котором переменная фактически создается с выделением памяти для нее. "Объявление" - это место, где указывается тип и характеристики переменной, но ника­ кого выделения памяти не происходит. Кстати, существует тенденция делать внешними (ext e rn) переменными все, что по­ падается на глаза, потому что это якобы упрощает обмен данными: списки аргументов становятся короче, а переменные всегда под рукой в нужный момент. Но, к сожалению, внешние переменные всегда "крутятся" под рукой и в ненужный момент. Слишком ин­ тенсивное использование внешних переменных чревато опасностями, поскольку в таких программах обмен данными далеко не очевиден - переменные могут модифицировать­ ся неожиданно и даже непреднамеренно, а доработка кода сильно усложняется. Вторая версия программы определения самой длинной строки уступает первой в качестве час­ тично по этим причинам, а частично потому, что общность двух полезных функций сильно ухудшается из-за внедрения в их тела прямых ссылок на обрабатываемые ими переменные. Итак, мы рассмотрели все то, что можно условно назвать ядром языка С. Располагая этим небольшим набором элементарных "кирпичиков", уже можно писать программы существенной длины, и было бы очень неплохо, если бы вы побольше попрактиковались

46

ГЛАВА 1

именно на этом этапе. В следующих упражнениях уровень сложности несколько выше, чем у тех программ, которые приводились в разделах этой главы. 1.20. Напишите программу de t ab, которая бы заменяла символы табуля­ ции во входном потоке соответствующим количеством пробелов до следующей границы табуляции. Предположим, что табуляция имеет фиксированную ширину n столбцов. Следует ли сделать n переменной или символическим параметром ? Упражнение

Упражнение 1.21. Напишите программу e n t ab, которая бы заменяла пустые строки, состоящие из одних пробелов, строками, содержащими минимальное количество табу­ ляций и дополнительных пробелов, - так, чтобы заполнять то же пространство. Исполь­ зуйте те же параметры табуляции, что и в программе det ab. Если для заполнения места до следующей границы табуляции требуется один пробел или один символ табуляции, то что следует предпочесть? Упражнение 1.22. Напишите программу для сворачивания слишком длинных строк входного потока в две или более коротких строки после последнего непустого символа, встречающегося перед n-м столбцом длинной строки. Постарайтесь, чтобы ваша про­ грамма обрабатывала очень длинные строки корректно, а также удаляла лишние пробелы и табуляции перед указанным столбцом.

1 .23. Напишите программу для удаления всех комментариев из программы на С . Позаботьтесь о корректной обработке символьных констант и строк в двойных ка­ вычках. Вложенные комментарии в С не допускаются. Упражнение

1.24. Напишите программу для выполнения примитивной синтаксической проверки программ на С, таких как непарные круглые, квадратные и фигурные скобки. Не забудьте об одинарных и двойных кавычках, управляющих символах и комментари­ ях. (Это сложная программа, если пытаться реализовать самый общий случай.) Упражнение

Вводный УРОК

47

Гла ва 2

Т и п ы да н н ы х, оп е рац и и и выраже н и я Фундаментальные объекты данных, с которыми работает программа, - это перемен­ ные и константы. Используемые в программе переменные перечисляются в объявлениях или декларациях, в которых указывается их тип, а также иногда их начальные значения. Манипуляции и преобразования над данными выполняются с помощью знаков операций. Переменные и константы объединяются в выражения, чтобы таким образом порождать новые значения. Тип объекта всегда определяет, какой набор значений может иметь этот объект и какие операции могут над ним выполняться. Все эти "кирпичики" программы и рассматриваются в этой главе. В стандарте ANSI базовые типы и операции подверглись незначительным изменени­ ям и дополнениям по сравнению с исходной версией языка. Теперь для всех целочислен­ ных типов есть формы s i gne d и uns i gned, а также введены способы обозначения констант без знака и шестнадцатеричных констант. Операции с плавающей точкой могут выполняться с одинарной точностью, а для повышенной точности расчетов введен тип l ong douЫ e . Строковые константы можно соединять в одну (т.е. выполнять над ними конкатенацию) на этапе компиляции программы. Перечислимые типы наконец-то стали полноправной частью языка, получив формальное определение после многих лет суще­ ствования де-факто. Объекты можно объявлять с модификатором cons t , который за­ прещает изменять их значения впоследствии. Правила автоматического преобразования арифметических типов были доработаны с целью расширения набора типов.

2 . 1 . И мена переменны х Хотя в главе 1 об этом не говорилось, существуют некоторые ограничения на имена переменных и символических констант. Имя может состоять из букв и цифр, но начи­ наться должно с буквы. Знак подчеркивания (_) считается буквой и часто оказывается полезным для улучшения удобочитаемости длинных имен. Однако не следует начинать с него имена переменных, потому что такие имена часто используются библиотечными функциями. Буквы в верхнем и нижнем регистре различаются, так что х и Х - это два разных имени. Традиционно в С принято записывать имена переменных строчными бук­ вами, а имена символических констант - прописными. Как минимум 3 1 символ из имени, внутреннего для программы, считается значащим. Для имен функций и внешних переменных это количество может быть меньше 3 1 , пото­ му что внешние имена могут использоваться ассемблерами и загрузчиками, над которы­ ми у языка нет никакого контроля. В отношении внешних имен стандарт гарантирует уникальность только первых шести символов и одного регистра. Ключевые слова напо-

добие i f , e l se, int, f l oa t и т.п. зарезервированы - их нельзя использовать в качест­ ве имен переменных. Они должны записываться в нижнем регистре. Разумно выбирать имена переменных так, чтобы они описывали назначение самих переменных и чтобы одно не могло легко превратиться в другое из-за опечатки. Сущест­ вует тенденция использовать совсем короткие имена для локальных переменных, счет­ чиков циклов и т.п. и имена подлиннее для глобальных переменных.

2 . 2 . Типы данных

и

их размеры

В языке С существует всего несколько базовых типов данных: char один байт, содержащий один символ из локального символьного набора; int целое число, обычно имеющее типовой размер для целых чисел в данной системе; f loat вещественное число одинарной точности с плавающей точкой; douЫ e вещественное число двойной точности с плавающей точкой.

Кроме того, существуют модификаторы, которые могут применяться к этим базовым типам. К целым числам применимы модификаторы s hort и l ong: s hort int sb ; l ong int count er ; Слово int в таких объявлениях можно опустить, что обычно и делается. Целью введения этих модификаторов было разграничить длины двух типов целых чи­ сел для практических потребностей. Обычно int имеет стандартную для той или иной системы длину. Тип short часто имеет размер 1 6 бит, l ong - 32 бита, а int - или 1 6, или 3 2 . Компилятору разрешено самостоятельно выбирать размер в соответствии с характеристиками аппаратуры и следующими ограничениями: числа типа s hort и int должны иметь длину не менее 16 бит, l ong - не менее 3 2 бит; тип short должен быть не длиннее int, а i nt - не длиннее l ong. Модификатор s ign e d ("со знаком") или u ns ign ed ("без знака") может применять­ ся к типу char или любому целочисленному. Числа типа u n s i gn ed всегда неотрица­ тельны, а длина диапазона их значений равна степени двойки 2п, где п - количество би­ тов в машинном представлении типа. Например, если char имеет длину 8 бит, то пере­ менные типа uns ig ne d char могут иметь значения от О до 255, а s i gn e d char - от - 1 28 до 1 27 (в системе с дополнением до двух). Являются ли переменные типа char знаковыми ( s igned) или беззнаковыми, зависит от конкретной системы, но выводимые на экран и печать символы всегда имеют положительные коды. Тип l ong douЫ e обозначает число с плавающей точкой повышенной точности. Как и в случае целочисленных переменных, длины вещественных объектов зависят от реализации языка, так что из длин типов f l oa t , douЫ e и l ong douЫ e различными могут быть одна, две или три. Стандартные заголовочные файлы < l im i t s . h > и < f l oat . h > содержат символи­ ческие константы для всех этих размеров, а также других свойств системы и компилято­ ра. Они рассматриваются в приложении Б. Упражнение 2.1. Напишите программу для определения диапазонов переменных типов

char, short, int и l ong (как s igned, так и u n s ign ed ) путем вывода соответст-

50

ГЛА ВА 2

вующих значений из заголовочных файлов, а также с помощью непосредственного вы­ числения. Для второго способа усложним задачу: определите еще и диапазоны вещест­ венных типов.

2 . 3 . Константы Целочисленная константа наподобие 1 2 3 4 имеет тип i n t . Длинную целую констан­ ту записывают со строчной или с прописной буквой l (L) на конце, например 1 2 3 4 5 6 7 8 9 L ; если целое число слишком велико, чтобы поместиться в переменную типа int, оно также будет восприниматься как l ong. Константы без знака записываются с и или U на конце, а суффикс u l или UL обозначает длинную целую константу без знака. Вещественные константы содержат десятичную точку ( 1 2 3 . 4 ) , степенную часть ( l e - 2 ) или и то и другое. Они имеют тип douЫ e , если с помощью суффикса не указано другое. Суффикс f или F обозначает константу типа f l oa t , а l или L - константу типа l ong douЫ e . Значение целого числа можно указывать в восьмеричной или шестнадцатеричной системе вместо десятичной. Если целая константа начинается с нуля ( О ), то она записана в восьмеричном представлении, а если с Ох или ОХ - то в шестнадцатеричном. Напри­ мер, десятичное число 3 1 можно записать как О 3 7 в восьмеричной системе и O x l f или O X l F в шестнадцатеричной. В конце восьмеричных и шестнадцатеричных констант так­ же можно ставить L для превращения их в длинные, а U - для лишения их знака. Так, число OXFUL является константой типа uns i gne d l ong с десятичным значением 1 5 . Символьная константа - это целое число в форме одного символа в кавычках, на­ пример ' х ' . Значением символьной константы является числовое значение кода симво­ ла в наборе, используемом в данной системе. Например, в наборе ASCII символьная кон­ станта ' О ' имеет код 48, который никак не связан с числом О. Если записать ' О ' вместо числового кода 48, который зависит от конкретного символьного набора, мы получим более независимую от системы и удобочитаемую программу. Символьные константы участвуют в арифметических операциях на правах полноправных целых чисел, хотя чаще всех к ним применяются операции сравнения с другими символами. Некоторые символы можно представить в символьных и строковых константах с по­ мощью управляющих последовательностей наподобие \ n (символа конца строки) . Такие последовательности содержат два символа, но обозначают только один. Кроме того, лю­ бую битовую последовательность длиной один байт можно представить таким способом : ' \ 000 1

где строка о о о может содержать от одной до трех восьмеричных цифр ( О . . . 7 ) . Допуска­ ется также следующее представление: ' \xhh '

где hh - одна или две шестнадцатеричные цифры ( 0 . . . 9 , a . . . f , A . . . F). Поэтому можно записать следующее в восьмеричном представлении: #de f ine VТАВ ' \ 0 1 3 ' / * Вертикаль ная табуляция в AS CI I * / / * Звуковой сигнал в ASCI I * / #de f ine BELL ' \ 0 0 7 '

Т И ПЫ ДДННЫХ, ОП ЕРАЦ ИИ И ВЫРАЖЕНИ Я

51

В шестнадцатеричном представлении это будет выглядеть так: #de f ine VТАВ ' \хЬ ' / * Вертикаль ная табуляция в AS CI I * / #de f ine BELL ' \х7 ' / * Звуковой сигнал в AS CI I * / Ниже приведен полный набор управляющих последовательностей, начинающихся с обратной косой черты. \\ \а обратная косая черта подача звукового сигнала \? \Ь вопросительный знак возврат назад и затирание \' \f одинарная кавычка прогон страницы \n \ 11 двойная кавычка конец строки \r \ ооо восьмеричное число возврат каретки \t \ xhh шестнадцатеричное число горизонтальная табуляция \v вертикальная табуляция Символьная константа ' \ О ' представляет символ с нулевым кодом, т.е. нулевой символ. Часто пишут именно ' \ О ' , а не О , чтобы подчеркнуть символьную природу то­ го или иного выражения. Тем не менее числовое значение этой конструкции - просто нуль. Константное выражение - это выражение, содержащее только константы. Такие выражения могут вычисляться в ходе компиляции, а не выполнения программы, и соот­ ветственно употребляться в любом месте, где допускается применение одной константы: #de f ine МAXLINE 1 0 0 0 char l ine [ МAXL I NE + l ] ; Еще один пример: #de f ine LEAP 1 / * в високосных годах * / int days [ 3 1 + 2 8 +LEAP + 3 1 + 3 0 + 3 1 + 3 0 + 3 1 + 3 1 + 3 0 + 3 1 + 3 0 + 3 1 ] ; Строковая константа, строковый литерал, или просто литерал - это последова­ тельность из нескольких (в частном случае ни одного) символов, заключенных в двой­ ные кавычки, например: 11 ! am а s t ring 11

11 11

А вот пример пустой строки: / * пус т а я строка * /

Кавычки не являются частью строки, а только ограничивают ее. Все управляющие последовательности, которые можно включать в символьные константы, допускаются и в строковых; \ 11 представляет символ двойной кавычки. Строковые константы можно сцеплять (выполнять конкатенацию) в процессе компиляции. Так, записи в первой и вто­ рой строках эквивалентны: " he l l o , 11 11 wor l d 11 " he l l o , wor l d " Этой возможностью удобно пользоваться, когда нужно распространить длинные строковые константы на несколько строк исходного кода. Формально строковая константа является массивом символов. Во внутреннем пред­ ставлении строки в конце присутствует нулевой символ ' \ О ' , так что физический объем памяти для хранения строки превышает количество символов, записанных между кавыч­ ками, на единицу. Это означает, что на длину строки не накладываются никакие ограни­ чения, но программа должна перебрать и проанализировать строку целиком, чтобы оп­ ределить ее длину. Стандартная библиотечная функция s t r l en ( s ) возвращает количе-

52

ГЛАВА 2

ство символов в строке, переданной ей в качестве аргумента, не считая завершающего \ о ' . Вот наша версия этой функции: / * strlen : возвращает длину строки s * / int strlen ( char s [ ] ) '

{

int i ; i О; whi l e ( s [ i ] ++i ; return i ;

!=

'

\0 ) '

Функция s t r l en и другие функции для работы со строками объявлены в стандарт­ ном заголовочном файле < S t r in g . h > . Будьте внимательны и н е путайте символьные константы с о строками, содержащими один символ: ' х - это не то же самое, что 11 х 11 • Первая величина - это целое число, используемое для представления кода буквы х в символьном наборе системы, тогда как вторая - массив символов, содержащий один символ (букву х) и завершающий нуль , \О , . Существует еще один тип констант - константы перечислимого типа. Перечисли­ мый тип (перечисление) определяется списком целочисленных символических констант, например: enum bool ean { NO , YES } ; '

Первая константа в списке получает значение О, вторая - 1 и т.д., если не указаны явные значения. Если заданы не все значения, те константы, для которых они не указаны, получат целые значения, следующие по порядку за последним из указанных. Это показа­ но во втором из приведенных ниже примеров. enum es capes { BELL = ' \ а ' , BACKS PACE = ' \Ь ' , ТАВ = ' \ t ' , NEWLINE = ' \ n , VТАВ = ' \ v ' , RETURN = ' \ r ' } ; enum months { JAN = 1 , FEB , МАR , APR , МАУ , JUN , JUL , AUG , SEP , ОСТ , NOV , DEC } ; / * FEB = 2 , МАR = 3 и т . д . * / '

Имена в различных перечислениях должны быть различными. Значения констант в одном перечислении не обязаны быть различными. Перечисления - это удобный способ ассоциировать значения констант с символиче­ скими именами с тем преимуществом перед # de f ine, что значения могут генериро­ ваться автоматически. Хотя можно объявлять переменные типов enurn, компиляторы не обязаны проверять, допустимые ли значения хранятся в таких переменных. Тем не менее переменные перечислимых типов предоставляют возможность такой проверки и потому часто бывают удобнее директив # de f ine. Кроме того, отладчик может выводить значе­ ния перечислимых переменных в их символическом виде.

ТИПЫ ДАННЫХ, ОП ЕРА Ц ИИ И В ЫРАЖЕ НИЯ

53

2 .4. Объявлен и я Все переменные необходимо объявить до их использования, хотя некоторые объяв­ ления могут быть сделаны неявно - по контексту. В объявлении указываются тип и спи­ сок из одной или нескольких переменных этого типа: int l owe r , upper , s t ep ; char с , l ine [ l O O O ] ; Переменные можно распределять по объявлениям любым способом. Приведенные выше списки можно переписать и в таком виде: int l owe r ; int upper ; int s t ep ; char с ; char l ine [ l O O O ] ; Последняя запись занимает больше места, но в нее удобнее добавлять комментарии, а также вносить другие изменения. Переменные можно инициализировать прямо в объявлениях. Если после имени по­ ставить знак равенства и выражение, то значение этого выражения будет присвоено пе­ ременной при ее создании: char esc = ' \ \ ' ; i = О; int l imit = МAXLINE + 1 ; int f l oat eps = 1 . О е - 0 5 ; Если переменная не автоматическая, то ее инициализация выполняется один раз еще до начала выполнения программы. Инициализирующее выражение в этом случае должно быть константным. Автоматическая переменная, инициализируемая явным обра­ зом в объявлении, инициализируется всякий раз, когда управление передается в функцию или блок, где она объявлена. Ее инициализатором может быть любое выражение. Внеш­ ние и статические переменные по умолчанию инициализируются нулями. Автоматиче­ ские переменные, для которых инициализирующие выражения не указаны явно, содер­ жат случайные значения ("мусор"). К объявлению любой переменной можно добавить модификатор c on s t , чтобы зна­ чение этой переменной впоследствии не менялось. В случае массива этот модификатор запрещает изменение любого из элементов. const douЬ l e е = 2 . 7 1 8 2 8 1 8 2 8 4 5 9 0 5 ; const char msg [ ] = " warning : " ; Декларация cons t может употребляться с аргументами-массивами, указывая тем са­ мым, что функция не изменяет передаваемый ей массив: int s t r l en ( con st char [ ] ) ; Если предпринимается попытка изменить переменную, объявленную с модификато­ ром cons t , ее результат будет зависеть от реализации языка.

54

ГЛАВА 2

2 . 5 . Ари ф м ети ч еские операции

В число двуместных арифметических операций входят + , - , * , / и взятие остатка от деления ( % ) . При целочисленном делении дробная часть отбрасывается. Следующее вы­ ражение дает остаток от деления х на у и поэтому равно нулю, если х кратно у: х % у Например, год считается високосным, если его номер делится на 4, но не на 1 00, за исключением лет, которые делятся на 400 и по определению являются високосными. Вот как это проверить: if ( ( year % 4 == О && year % 1 0 0 ! = О ) 1 1 year % 4 0 0 == О ) print f ( " %d i s а leap year\n " , year ) ; else print f ( " %d i s not а leap year\ n " , yea r ) ; Операция % неприменима к числам типа f l oat или douЫ e . Направление округле­ ния при операции / или знак результата при операции % для отрицательных аргументов зависят от системы, равно как и действия, предпринимаемые при переполнении или воз­ никновении машинного нуля. Операции + и - имеют одинаковый приоритет, который ниже, чем приоритет опера­ ций * , / и % . Приоритет трех последних, в свою очередь, ниже, чем приоритет одноме­ стных операций + и - . Арифметические операции ассоциируются слева направо. В табл. 2. 1 приведены приоритеты и ассоциирование всех операций.

2 . 6 . О пера ц ии отно ш ения и логические опера ции >

К операциям отношения принадлежат следующие: >= < = ' 0 ' && s [ i ] < = ' 9 ' ; + + i ) n = 10 * n + (s [i] - ' 0 ' ) ; return n ;

Как говорилось в главе 1 , следующее выражение дает числовое значение цифрового символа, помещенного в s [ i ] , поскольку коды символов ' о ' , ' 1 ' и т.д. образуют не­ прерывную последовательность в порядке возрастания. Еще один пример преобразования из char в int - это функция l ow e r , которая пе­ реводит один символ в нижний регистр в символьном наборе ASCII. Если символ не яв­ ляется буквой в верхнем регистре, функция l owe r возвращает его без изменений. /* l ower : преобразует символ с в нижний ре гистр ; т ол ь ко AS C I I * / int l ower ( int с )

{

i f ( с > = ' А ' && с < = ' Z ' ) return с + ' а ' - ' А 1 ; else return с ;

В наборе ASCII этот алгоритм работает, потому что соответствующие буквы верхне­ го и нижнего регистра расположены на фиксированных расстояниях друг от друга, и ка­ ждый алфавит образует непрерывную последовательность - между А и z нет ничего, кроме букв в алфавитном порядке. Однако эта закономерность неверна, например для символьного набора EBCDIC, и в этом наборе приведенная функция преобразовывала бы не только буквы. Стандартный заголовочный файл < C type . h > , описанный в приложении Б, содержит объявления семейства функций, выполняющих независимые от символьного набора про­ верки и преобразовщия. Например, функция t o l owe r ( с ) возвращает аналог символа с в нижнем регистре, если с - буква верхнего регистра, т.е. это системно-независимый аналог приведенной выше функции l owe r . Аналогично можно заменить следующую непереносимую между символьными наборами проверку: с > = ' 0 ' && с < = ' 9 ' Переносимый код будет выглядеть так: i s digit ( c ) Далее для этих целей всегда будут использоваться функции, определенные в < C type h > . В преобразовании символов в целые числа есть одна тонкость. Стандарт языка н е оп­ ределяет, должны ли переменные типа char быть знаковыми или беззнаковыми. Может ли при преобразовании char в int получиться отрицательное число? Ответ зависит от конкретной системы, отражая различия в аппаратной и программной архитектуре. В не­ которых системах символ char, крайний левый бит которого равен 1 , преобразуется в отрицательное число (это называется "расширением знака"). В других тип char преоб.

ТИ ПЫ ДДННЫХ, ОП ЕРА Ц ИИ И ВЫРАЖЕНИЯ

57

разуется в int путем добавления нулей на левом краю числа, и поэтому результат полу­ чается всегда положительным. Определение языка С гарантирует, что любой печатаемый и видимый на экране сим­ вол из стандартного набора системы никогда не может быть отрицательным, так что и в выражениях такие символы всегда положительны. А вот произвольные последователь­ ности битов, помещаемые в переменные типа char, могут в некоторых системах ока­ заться отрицательными, а в других - положительными. Если необходимо поместить данные несимвольного характера в переменные типа char, то для лучшей переносимо­ сти задайте модификатор s i gned или uns i gned. Выражения отношения наподобие i > j и логические выражения, соединенные зна­ ками && и 1 1 . по определению имеют значение 1 , если они истинны, и О, если ложны. Рассмотрим присваивание: d = с >= ' 0 ' && с < = ' 9 ' В результате его переменная d получает значение 1 , если с - цифра, и О в противном случае. Однако такие функции, как i s di g i t, могут возвращать любое ненулевое значе­ ние в качестве результата "истина". В условных конструкциях операторов i f , whi l e , for и т.д. "истина" означает просто "не нуль", поэтому особой разницы нет. Неявные арифметические преобразования в основном работают так, как можно ожи­ дать от них по логике вещей. В целом, если операция наподобие + или * , имеющая два операнда (двуместная), связывает два аргумента разных типов, то более "узкий" тип расширяется до более "широкого", прежде чем выполняется собственно операция. Ре­ зультат будет принадлежать к более "широкому" типу. В разделе 6 приложения А приве­ дены точные правила таких преобразований. Но если среди аргументов нет чисел типа uns i gned, то можно обойтись следующим неформальным набором правил. •

Если один из операндов имеет тип l ong douЫ e, другой преобразуется в long douЫ e .



В противном случае, если один и з операндов - douЫ e, другой преобразуется в douЫ e .



В противном случае, если один и з операндов - f l o a t , другой преобразуется в f l oa t .



В противном случае c h a r и s hort преобразуются в in t.



Затем, если среди операндов есть l ong, другой преобразуется в l ong.

Заметьте, что числа типа f l oat в выражениях не преобразуются в douЫ e автома­ тически; это - изменение по сравнению с исходным определением языка. В основном все математические функции, объявленные в заголовочном файле < math . h>, использу­ ют двойную точность. Основная причина, по которой иногда используется тип f l oat, заключается в том, чтобы сэкономить память в больших массивах, или, реже, сэконо­ мить время в системах, имеющих особенно затратную вещественную арифметику с двойной точностью. l l равила преобразования становятся сложнее, если в выражениях участвуют аргумен1 ы ·1 и ri a uns i gned. Проблема состоит в том, что сравнение между величинами со зна­ ком и без знака является системно-зависимым, потому что зависит от размеров целочис­ ленных переменных. Для примера предположим, что тип i nt имеет длину 1 6 бит, а l ong - 32 бита. Тогда l L < l U, поскольку l U, будучи числом типа int, расширяет-

58

ГЛА ВА 2

ся до s i gned l ong. Но при этом - l L > l UL, потому что - l L расширяется до uns igned l ong и становится большим положительным числом. Преобразования происходят также в ходе присваивания; значение с правой стороны преобразуется в тип переменной, стоящей слева, и результат будет иметь именно этот тип. Символ типа char преобразуется в целое число, причем с расширением знака или без, как было описано раньше. Длинные целые числа преобразуются в более короткие или величины типа char пу­ тем отбрасывания лишних старших битов. Поэтому после приведенных ниже операций значение с остается неизменным. int i ; c har с ; i с

= =

с; i;

Это справедливо независимо от того, применяется или нет расширение знака. А вот присваивание в обратном порядке может привести к потере информации. Если переменная х имеет тип f l oa t , а i - тип i n t , то преобразования выполняют­ ся как при присваивании х = i, так и при i = х . При преобразовании f l oat в int отбрасывается дробная часть. При преобразовании douЫ e в f l oat значение числа ли­ бо усекается, либо округляется - это зависит от конкретной реализации языка. Поскольку аргументы функций представляют собой выражения, преобразования ти­ пов могут иметь место и при передаче аргументов в функции. Если у функции отсутству­ ет прототип, значения типа char и short становятся int, а f l oat становится douЫ e . Вот почему мы объявляли аргументы функции как int и douЫ e , хотя вызы­ валась она с аргументами типа char и f l оа t . Наконец, в любом выражении можно принудительно выполнить явное преобразование типов. Для этого используется одноместная операция под названием приведение типов: ( имя - типа ) выражение

Этим способом выражение преобразуется в указанный тип по правилам, приведен­ ным выше. Смысл приведения типов состоит в том, что выражение как бы присваива­ ется переменной заданного типа, которая потом и используется вместо всей конструк­ ции. Например, библиотечная функция s qrt ожидает аргумента типа douЫ e и выдает бессмыслицу, если случайно получает аргумент неподходящего типа. (Функция s qrt объявлена в заголовочном файле . ) Если n - целочисленная переменная, ее можно вначале привести к типу douЫ e, а затем уже передавать в функцию: sqrt ( ( douЬle ) n ) Обратите внимание, что приведение типа порождает новое значение нужного типа, никак не изменяя исходную переменную. Операция приведения имеет такой же высокий приоритет, как и другие одноместные операции, сводный список которых приведен в табл. 2. 1 . Если аргументы объявлены в прототипе функции (как это и должно быть), то при лю­ бом вызове функции выполняется принудительное приведение типов. Например, пусть объявлен следующий прототип функции s qrt : douЫ e sqrt ( douЬle ) ; Тогда при следующем вызове целое число 2 будет автоматически преобразовано в вещественное 2 О без необходимости явного приведения: .

ТИ ПЫ ДАН НЫХ, ОП ЕРА ЦИИ И В ЫРАЖЕН ИЯ

59

root 2

=

sqrt ( 2 ) ;

Стандартная библиотека содержит переносимую между системами версию генерато­ ра псевдослучайных чисел, а также функцию для инициализации этого генератора. В первой из них иллюстрируется приведение типов: uns igned long int next = 1 ; / * rand : возвращает п с евдо случайное число от О до 3 2 7 6 7 * / int rand ( void )

{

next = next * 1 1 0 3 5 1 5 2 4 5 + 1 2 3 4 5 ; return ( unsigned int ) ( next / 6 5 5 3 6 ) % 3 2 7 6 8 ;

/ * s rand : устанавлив ает инициализ атор для rand ( ) * / vo id srand ( unsigned int seed)

{

next = seed ;

Упражнение 2.3. Напишите функцию ht o i ( s ) , которая преобразует строку шестна­ дцатеричных цифр (учитывая необязательные элементы Ох или О Х) в ее целочисленный эквивалент. В число допустимых цифр входят десятичные цифры от о до 9, а также бук­ вы a-f И А-F.

2 . 8 . Опера ц ии инкрементирования и декрементирования В языке С сушествуют две необычные операции - инкрементирование и декремен­ тирование переменных. Операция инкрементирования + + добавляет к своему операнду единицу, а операция декрементирования - отнимает. Инкрементирование уже широко использовалось в книге, например в таких конструкциях: i f ( с == ' \n ' ) + +nl ; Необычный аспект этих операций состоит в том, что они могут использоваться как префиксные (т.е. стоять перед переменной, как в + + n ) или как постфиксные (после пе­ ременной: n + + ) . В обоих случаях переменная получает единичное приращение. Однако в выражении + + n переменная инкрементируется до того, как используется, а в выражении n + + - после того. Это означает, что в контексте использования самой переменной, а не только эффекта приращения, выражения + + n и n + + различны. Пусть, например, n равна 5. Тогда следующий оператор делает х равным 5 : х = n++ ; х

60

А этот оператор присваивает х значение 6 : ++n ;

=

ГЛАВА 2

При этом n в обоих случаях становится равным 6. Операции инкрементирования и декрементирования применимы только к переменным; выражения наподобие ( i + j ) + + недопустимы. В контексте, где значение переменной не используется, а только увеличивается, пре­ фиксная и постфиксная формы эквивалентны: i f (с = = ' \n ' ) nl + + ; Но бывают и ситуации, когда специально используются свойства одной из них. Для примера рассмотрим функцию s que e z e ( s , с ) , которая удаляет все вхождения симво­ ла с из строки s . / * squee z e : удаляет в с е симв олы с и з строки s * / vo i d squee z e ( char s [ ] , int с )

{

int i , j ; for ( i = j = О ; s [ i ] ! = ' \ О ' ; i + + ) if ( s [i] ! = с) s [j ++ J = s [ i] ; s [j ] = ' \О ' ;

Всякий раз, когда встречается символ, отличный от с , он копируется в текущую по­ зицию j , и только затем j инкрементируется, чтобы подготовиться к приему следующе­ го символа. Эта форма в точности эквивалентна следующей: if ( s [ i ] ! = с ) { s [j ] = s [i] ; j ++ ; Еще один похожий пример можно взять из функции ge t l ine, написанной нами в главе 1 . Там присутствовала конструкция if ( с == ' \n ' ) { s [i] = с ; ++i ; которую можно заменить более компактной записью: i f ( с = = ' \n ' ) s [i++] = с ; В качестве третьего примера рассмотрим стандартную функцию s t r c a t ( s , t ) , которая присоединяет строку t к концу строки s (выполняет конкатенацию). При этом подразумевается, что свободного места достаточно для хранения удлиненной строки. В нашем варианте s t rc a t не возвращает никакого значения, тогда как стандартная библиотечная версия возвращает указатель на итоговую строку. /* st rcat : присоединяет строку t к концу s ; в s должно быть достаточно ме ст а * / void s t rcat ( char s [ ] , char t [ ] )

{

int i , j ; i = j = О;

ТИ ПЫ ДАН НЫХ, ОП ЕРА ЦИИ И ВЫРАЖЕНИ Я

61

whi l e ( s [ i ] ! = ' \ 0 ' ) / * поис к конца s * / i++ ; whi l e ( ( s [ i + + ] = t [ j + + ] ) ! = ' \ 0 ' ) / * копирование t * /

По мере копирования символов из t в s к индексам i и j применяется постфиксное приращение + +, чтобы сдвинуть их в следующие по порядку позиции для следующего прохода цикла копирования. Упражнение 2.4. Напишите альтернативную версию функции s que e z e ( s l , s 2 ) , которая бы удаляла из строки s l все символы, встречающиеся в строке s 2 . Упражнение 2.5 . Напишите функцию any ( s l , s 2 ) , возвращающую номер первой позиции в строке s l , в которой находится какой-либо из символов строки s 2 , либо - 1 , если строка s l не содержит ни одного символа из s 2 . (Стандартная библиотечная функ­ ция s t rpbrk делает то же самое, но возвращает указатель на найденную позицию.)

2 . 9 . Поразрядные опера ц ии В языке С имеется шесть операций битового (поразрядного уровня), применимых только к целочисленным аргументам, т.е. char, short, int и l ong, как имеющим, так и не имеющим знака. & поразрядное И. 1 поразрядное включающее ИЛИ. поразрядное исключающее ИЛИ. > сдвиг вправо. одноместное поразрядное дополнение до единицы. Поразрядная операция И ( &) часто используется для обнуления некоторого набора битов. Например, следующий оператор обнуляет все биты переменной n, кроме младших семи: n = n & 0177 ; Поразрядная операция ИЛИ ( 1 ), напротив, используется для того, чтобы сделать от­ дельные биты единицами. Следующий оператор делает единицами все биты, которые равны l в константе S ET ON: х = х j SET_ON ; Операция исключающего ИЛИ ( л ) помещает единицу в позиции, где в операндах сто­ ят различные биты, и нуль в те позиции, в которых биты операндов совпадают. Необходимо отличать поразрядные операции & и 1 от логических операций && и 1 j , которые предполагают вычисление результата типа "истина-ложь" слева направо. Напри­ мер, если х равно l, а у равно 2, то х & у равно нулю, тогда как х & & у равно единице. Операции сдвига < < и > > выполняют сдвиг своего левого операнда соответственно влево и вправо на количество разрядов (битовых позиций), заданных правым операндом, который обязательно должен быть положительным. Таким образом, выражение х < < 2 сдвигает значение х влево на две разрядные позиции, заполняя вакантные биты нулями. Получается то же самое, как если бы умножить число на 4. Сдвиг вправо числа типа

62

ГЛАВА 2

uns igned всегда заполняет освободившиеся биты нулями. Сдвиг вправо числа со зна­ ком в некоторых системах приводит к заполнению этих битов значением знакового бита ("арифметический сдвиг"), а в других - нулями ("логический сдвиг"). Одноместная операция - дает поразрядное дополнение целого числа до единицы, т.е. преобразует каждый единичный бит в нулевой и наоборот. Например, следующий опера­ тор делает последние шесть бит переменной х равными О : х = х & -077 ; Заметьте, что х & - 0 7 7 не зависит от длины слова и поэтому предпочтительнее, чем, например, х & 0 1 7 7 7 0 0 , где предполагается, что х - 1 6-разрядная величина. Пе­ реход к переносимой форме не требует дополнительных вычислительных затрат, по­ скольку - О 7 7 - константа, вычисляемая при компиляции. В качестве иллюстрации некоторых поразрядных операций рассмотрим функцию getb i t s ( х , р , n ) , которая возвращает n-битовое поле, начинающееся с позиции р и сдвинутое к правому краю, из переменной х. Предполагается, что позиция О находится на правом краю числа и что n и р корректные положительные числа. Например, ge t b i t s ( х , 4 , З ) возвращает три бита в позициях 4, 3 и 2, выровненные по правому краю: / * getbi ts : извлекает n бит , начиная с р - й позиции * / uns igned getb i t s ( unsigned х , int р , int n ) -

{

re turn ( х

>>

( p+ l - n ) ) & - ( - 0

> ( p + l - n ) сдвигает нужное поле к правому краю слова. Величина - О состоит целиком из единиц; сдвиг ее влево на n позиций с помощью выражения - О < < n заполняет правые n битов нулями. Взятие дополнения с помощью операции порождает маску, в которой крайние правые n бит заполнены единицами. Упражнение 2.6. Напишите функцию s e t Ь i t s ( х , р , n , у ) так, чтобы она возвращала аргумент х, в котором n битов, начиная с позиции р, равны n крайним правым битам ар­ гумента у, а остальные биты не тронуты. Упражнение 2.7. Напишите функцию i nvert ( х , р , n ) , возвращающую свой аргумент х, в котором обращены n бит, начиная с позиции р (т.е. единичные биты заменены нуле­

выми

и

наоборот), а остальные не тронуты.

Упражнение 2.8. Напишите функцию r i ghtrot ( х , n ) , которая бы возвращала значе­ ние своего целого аргумента х после его вращения вправо на n двоичных разрядов .

2 . 1 О . Опера ц ии с присваиванием и выражения с ни ми Операторы присваивания, в которых переменная из левой части повторяется в пра­ вой, часто можно записать в более компактной форме. Возьмем такое выражение: i = i + 2

ТИ ПЫ ДА ННЫХ, ОП ЕРА ЦИ И И ВЫРАЖЕНИ Я

63

Его можно переписать в сжатом виде: i += 2

Знак + = обозначает одну из операций с присваиванием . Большинство двуместных операций (т.е. операций наподобие + и - , у которых два ар­ гумента - левый и правый) имеют соответствующие им операции с присваиванием оп= , где оп - одна из следующих операций: + * / % > & Пусть вырl и выр2 вырl оп= выр2

-

два выражения и имеется запись

Это эквивалентно следующей записи: вырl = ( вырl ) оп ( выр2 ) При этом выражение вырl вычисляется только один раз. Обратите внимание на скобки вокруг выражения выр l . Рассмотрим, например, такой оператор : х *= у + 1 Он эквивалентен следующему: (у + 1 )

х = х *

А вот при отсутствии скобок в определении можно было бы ошибочно подумать, что полная форма выглядит так: х = х * у + 1 Для примера рассмотрим функцию Ь i t c ount , которая подсчитывает количество единичных битов в своем целочисленном аргументе: / * bitcount : подсчитыва ет единицы в двоичной записи х * / int bitcount ( uns igned х )

{

int Ь ; for ( Ь = О ; х ! = О ; х > > = 1 ) if (х & 01) Ь++ ; return Ь ;

Объявление аргумента х с модификатором uns i gned гарантирует, что при сдвиге вправо освободившиеся биты будут заполнены нулями независимо от системы, в кото­ рой выполняется программа. Помимо такого второстепенного преимущества, как краткость записи, операции с присваиванием имеют то преимущество, что они соответствуют способу человеческого мышления. Когда мы говорим "прибавить 2 к i", то имеем в виду "увеличить i на 2", а не "извлечь i , прибавить 2 и поместить результат обратно в i". Поэтому выражение i + = 2 предпочтительнее, чем i = i + 2 . Кроме того, в левой части присваивания встречаются очень сложные выражения: yyva l [yypv [p3 +p4 ] + yypv [ p l +p2 ] ] += 2 В этом случае операция, объединенная с присваиванием, делает код более легким для понимания, поскольку читателю не придется прикладывать немалые усилия, чтобы про­ верить, действительно ли два выражения в левой и правой части идентичны. Операции с присваиванием даже могут помочь компилятору генерировать более оптимальный код.

64

ГЛАВА 2

Мы уже говорили о том, что оператор присваивания имеет значение, как любое дру­ гое выражение, и поэтому может сам входить в выражения. Вот самый распространен­ ный пример такого рода: whi l e ( ( с = get char ( ) ) ! = EOF ) Другие операции с присваиванием ( + = , - = и т.д.) тоже могут фигурировать в выра­ жениях, хотя и встречаются в них не так часто. Во всех таких случаях тип выражения с присваиванием соответствует типу левого операнда, а его значение равно значению этого операнда после присваивания. Уп ражнение 2.9. Благодаря свойствам двоичной системы счисления выражение х &= ( х 1 ) удаляет самый правый единичный бит в переменной х. Докажите это. Воспользуйтесь этим фактом для того, чтобы написать более быструю версию функции Ь i t count . -

2 . 1 1 . Условные выражения Следующий код берет большее из двух чисел, а и Ь, и помещает его в переменную if ( а > Ь) z а; else Ь· z

z:

'

Эту и аналогичные конструкции можно записать другим способом, прибегнув к ус­ ловному выражению с трехместной операцией ? : . Условное выражение имеет следую­ щий вид: выражl ? выраж2 : выражЗ

Вначале вычисляется выражение выражl . Если оно не равно нулю (т.е. истинно), то вычисляется выражение выраж2, которое и становится значением всей условной конст­ рукции. В противном случае вычисляется выражЗ, значение которого считается значе­ нием всего условного выражения. Всегда вычисляется только одно из выраж2 и вы­ р а жЗ . Итак, чтобы присвоить переменной z большее из двух значений, а или Ь, необхо­ димо записать следующее: z = (а > Ь) ? а : Ь ; / * z = max ( a , Ь ) * / Следует отметить, что условное выражение является полноправным выражением и может использоваться везде, где допускается применение выражений. Если выраж2 и выражЗ имеют различные типы, то тип результата определяется правилами преобразо­ вания, рассмотренными ранее в этой главе. Например, если f имеет тип f l oa t , а n тип int, то следующее выражение будет иметь тип f l oa t независимо от положитель­ ности значения n: (n > О ) ? f : n Первое выражение этой условной конструкции не обязательно заключать в скобки, поскольку приоритет операции ? : очень низкий - чуть выше, чем у присваивания. Од­ нако скобки желательны, поскольку они подчеркивают условную часть выражения. Условные выражения часто позволяют сделать код очень кратким. Например, следую­ щий цикл выводит n элементов массива по десять в одной строке, отделяя каждый столбец пробелом и завершая каждую строку (в том числе последнюю) символом конца строки: ТИ ПЫ ДА ННЫХ, ОП ЕРАЦ ИИ И ВЫРАЖЕНИЯ

65

for ( i О ; i < n ; i++ ) print f ( " %6d%c " , a [ i ] , =

( i % 1 0 = = 9 1 1 i = =n - 1 ) ? ' \n '



1

1) ;

Символ конца строки выводится после каждого десятого элемента, а также после n­ го. После всех остальных выводится один пробел. Код может показаться запутанным, но он намного компактнее, чем соответствующая конструкция i f - e l s e . Вот еще один ха­ рактерный пример: print f ( " You have %d i t em% s . \n " , n , n= = l ? " " : " s " ) ; Упражнение 2.10. Перепишите функцию l ower, которая преобразует буквы в верхнем регистре к нижнему, с использованием условного выражения вместо конструкции i f el se.

2 . 1 2 . Приоритет и порядок вычисления В табл. 2. 1 сведены правила определения приоритета и порядка ассоциирования опе­ раций, в том числе и тех, о которых еще не говорилось. Операции, перечисленные в од­ ной строке, имеют одинаковый приоритет. Строки расположены в порядке убывания приоритета; например, * , / и % имеют одинаковый приоритет, более высокий, чем у од­ номестных операций + и - . "Знак операции" ( ) обозначает вызов функции. Операции - > и . используются для обращения к элементам структур. Они будут рассмотрены в гла­ ве 6 вместе с операцией s i z e o f (вычисление размера объекта). В главе 5 рассматрива­ ются операции * (ссылка по указателю) и & (получение адреса объекта), а в главе 3 оператор "запятая". Обратите внимание, что приоритет поразрядных операций &, л и 1 меньше, чем у операций сравнения = = и ! = . Поэтому выражения, в которых анализируются отдельные биты, необходимо брать в скобки, чтобы получить корректные результаты: i f ( ( х & МАSК) = = О ) -

Таблица 2.1. Приоритет и ассоции рование операций О пера ции

()

[] -

++

*

/ %

+

-

>

=

Слева направо >

>=

Слева направо Слева направо Слева направо Слева направо Слева направо

&&

66

Слева направо

ГЛАВА 2

Окончание табл. 2. 1 Опера ции

Ассоциирование

1 1

Слева направо С права налево

?:

+=

* = / = % = &=

А

1 = =

С права налево Слева направо

Как и в большинстве языков, в С в основном не указывается порядок вычисления операндов в операциях. (Исключениями являются & &, 1 1 . ? : и ' , ' .) Например, в сле­ дующем операторе первой может вызываться как функция f ( ) , так и функция g ( ) : X = f(} + g(} ; Если в одной из функций, f или g, модифицируется переменная, от которой зависит работа другой функции, то итоговое значение х будет зависеть от порядка вызова. Чтобы гарантировать определенную последовательность операций, следует поместить проме­ жуточный результат в какую-нибудь временную переменную. Аналогичным образом, не определяется и порядок, в котором вычисляются аргумен­ ты функции при ее вызове. Поэтому следующий оператор может дать различные резуль­ таты при трансляции разными компиляторами: print f ( " %d %d \n " , + + n , powe r ( 2 , n ) ) ; / * НЕП РАВИЛЬНО * / Результат будет зависеть от того, получает ли n приращение до или после вызова функции powe r. Чтобы решить проблему, достаточно записать так: ++n ; print f ( 11 %d %d \n " , n , powe r ( 2 , n ) ) ; Вызовы функций, вложенные операторы присваивания, операции инкрементирования и декрементирования имеют "побочные эффекты" - в ходе вычисления выражений мо­ гут непредсказуемо модифицироваться некоторые переменные. В любом выражении, в котором возможны побочные эффекты, присутствует зависимость от порядка использо­ вания переменных в вычислениях. Одна из типичных ситуаций представлена следующим оператором : a [ i ] = i++ ; Проблема связана с тем, что неизвестно, какое значение индекса i используется для об­ ращения к массиву - старое или новое. Компиляторы могут интерпретировать этот опера­ тор по-разному и выдавать различные результаты в зависимости от своей интерпретации. В стандарте такие вопросы намеренно оставлены открытыми. Когда именно побочным эф­ фектам (присваивание переменным) следует иметь место внутри выражений - ответ на этот вопрос оставлен на усмотрение авторов компиляторов, поскольку наилучший порядок операций сильно зависит от аппаратно-системной архитектуры. (В стандарте все-таки оп­ ределено, что побочные эффекты воздействуют на аргументы до вызова функций, но в приведенном выше примере с функцией print f не поможет и эта информация.) Таким образом, из всего вышесказанного можно сделать вывод, что писать код, зави­ сящий от порядка вычисления аргументов или операндов, - это плохой стиль в любом языке программирования. Разумеется, необходимо знать, каких вещей избегать, однако если вы вообще не знаете, как те или иные операции выполняются в разных системах, у вас не возникнет искушения воспользоваться свойствами конкретной реализации. Типы ДАННЫХ, ОП ЕРАЦ ИИ и ВЫРАЖЕНИ Я

67

Гла ва 3

Уп р а вл я ю щ и е ко н с т ру кц и и Управляющие операторы и конструкции языка задают порядок, в котором выполня­ ются вычислительные операции программы. В примерах предыдущих глав уже демонст­ рировались самые необходимые управляющие конструкции. В этой главе будут описаны все остальные операторы управления, существующие в языке, а также сделаны уточне­ ния, касающиеся уже известных.

3 . 1 . Операторы и блоки Выражение наподобие х О, i + + или p r i nt f ( . . . ) становится оператором, если после него поставить точку с запятой: х = О ; i++ ; print f ( . . . ) ; =

В языке С точка с запятой является элементом оператора и его завершающей частью, а не разделителем операторов, как в языке Pascal. Фигурные скобки, { и } , служат для группировки объявлений и операторов в состав­ ные операторы, или блоки, синтаксически эквивалентные одному оператору. Фигурные скобки, окружающие операторы тела функции, - это самый очевидный пример такого блока, а еще один - это скобки вокруг группы из нескольких операторов после i f, e l s e , whi l e или for. (Переменные можно объявлять в любом блоке; об этом будет сказано подробнее в главе 4.) После правой скобки, закрывающей блок, точка с запятой не ставится.

3 . 2 . Оператор

i f - else

Оператор i f - e l s e выражает процесс принятия альтернативных решений. Его фор­ мальный синтаксис таков: i f ( выражение ) опера торl

else опера тор2

Часть, начинающаяся со слова e l s e , необязательна. Вначале вычисляется выраже ­ ние ; если оно истинно (т.е. имеет ненулевое значение), то выполняется опера торl. Ес­

ли оно ложно (т.е. имеет нулевое значение) и присутствует блок e l s e , то выполняется опера тор2.

Поскольку в операторе i f всего-навсего анализируется числовое значение выраже­ ния, можно несколько упростить отдельные конструкции. Самый очевидный пример упрощение следующей конструкции: if ( выражение ! = О ) Более краткая форма выглядит так: if

( выражение )

Такая запись часто оказывается более естественной и понятной, хотя иногда может вызвать путаницу. Поскольку блок e l s e в операторе i f необязателен, его отсутствие в серии вложен­ ных i f порождает двусмысленность. Но проблема решается благодаря тому, что e l s e всегда ассоциируется с ближайшим предыдущим оператором i f без e l s e . Рассмотрим пример: if (n > О ) if (а > Ь ) z а; else

z

Ь· '

где e l s e соответствует внутреннему i f, что и показано с помощью отступа. Если про­ граммист имел в виду другое, он должен принудительно изменить принадлежность блока e l s e с помощью фигурных скобок: if (n > О ) { if (а > Ь) z = а ; else

z

= Ь;

Особенно вредоносной эта двусмысленность бывает в таких ситуациях: i f (n >= О ) for ( i = О ; i < n ; i + + ) if (s [i] > О ) { print f ( 11 re turn i ; •

else





11 ) ;

/ * ОШИБКА * /

p r i nt f ( 11 e rror - n i s negat ive \ n 11 ) ;

Отступ ясно показывает, чего хочет программист, но компилятор этого не понимает и в результате ассоциирует e l s e с ближайшим вложенным i f . Такие ошибки найти не­ легко, поэтому рекомендуется всегда использовать фигурные скобки для выделения вложенных операторов i f . Кстати, заметьте, что после z = а стоит точка с запятой: if (а > Ь ) z а; else

z

Ь;

По грамматическому определению после ключевого слова i f стоит оператор, а вы­ ражение-оператор наподобие z = а всегда оканчивается точкой с запятой.

70

ГЛАВА 3

3.3 . Конструкция

else - i f

Следующая конструкция встречается так часто, что заслуживает отдельного рассмот­ рения: if

( выражение ) оператор e l s e i f ( выражение ) опера тор e l s e i f ( выражение ) оператор e l s e i f ( выражение ) оператор else оператор

Эта последовательность операторов i f представляет собой самый общий способ за­ писи принятия многовариантного решения. Перечисленные выражения вычисляются по порядку; если какое-нибудь из них оказывается истинным, то выполняется ассоцииро­ ванный с ним опера тор, и цепочка проверки условий прерывается. Как всегда, опера тор может представлять собой как отдельный оператор, так и блок в фигурных скобках. Последний фрагмент данной конструкции ( e l s e без условия) обрабатывает вариант "ничто из перечисленного" - выполняет операции "по умолчанию" в случае, если ни одно из условий не удовлетворяется: else оператор

Иногда этот вариант не нужен - для него не предусмотрены никакие операции. То­ гда и завершающий фрагмент можно опустить или же воспользоваться им для обработки ошибок - перехвата "невозможной" ситуации. Проиллюстрируем выбор решения из трех возможных на примере функции двоично­ го поиска (дихотомии), которая определяет, встречается ли некоторое число х в упоря­ доченном массиве v. Элементы v должны располагаться в порядке возрастания. Функ­ ция возвращает позицию в массиве (число от о до n - 1), если х в нем встречается, или - 1 , если не встречается. Вначале процедура двоичного поиска сравнивает значение х со средним элементом массива v. Если х меньше, чем средний элемент, дальнейший поиск выполняется в ниж­ ней части таблицы, в противном случае - в верхней. В любом из случаев следующий шаг состоит в том, чтобы сравнить х со средним элементом выбранной половины. Про­ цесс деления диапазона пополам продолжается, пока не будет найдено требуемое значе­ ние или пока диапазон не станет пустым. /* binsearch : п о и с к х в v [ O ] ma i n ( )

{

/ * подсчет цифр , пробелов , других симв олов * /

int с ,

i , nwh i t e , nothe r , nd i g i t [ l O ] ;

nwh i t e = nother О; for ( i = О ; i < 1 0 ; i + + ) nd i g i t [ i ] = О ;

72

ГЛАВА 3

wh i l e ( ( с = get char { ) ) ! = EOF ) swi t c h ( с ) { case ' 0 ' : c a s e ' 1 ' : c a s e ' 2 ' : c a s e ' 3 ' : c ase case ' 5 ' : case ' 6 ' : c a s e ' 7 ' : c a s e 1 8 1 : case nd i g i t [ c - ' 0 ' ] + + ; break ; case ' ' case ' \ n ' : case ' \ t ' : nwhi t e + + ; break ; de f aul t : nothe r + + ; break ;

'4' : '9' :

·

}

print f ( " d i g i t s = " ) ; for ( i = О ; i < 1 0 ; i + + ) print f ( " % d " , nd i g i t [ i ] ) ; p r i nt f ( " , whi t e spa ce = % d , other nwhi te , not he r ) ; return О ;

%d\ n " ,

Оператор b r e ak инициирует немедленный выход из оператора s w i t ch. Поскольку блоки c a s e - это, по сути, всего лишь метки, после выполнения кода в одном из них продолжается выполнение кода следующего (насквозь через блоки c a s e), пока не будет предпринята какая-нибудь операция для выхода из s w i t ch. Для этого чаще всего ис­ пользуются операторы break и ret u rn. Оператор break также можно использовать для принудительного выхода из циклов w h i l e , f o r и do, о чем будет сказано позже. Возможность сквозного выполнения блоков c a s e имеет как достоинства, так и не­ достатки. Достоинство состоит в том, что несколько возможных вариантов ( c a s e) мож­ но ассоциировать с одним и тем же набором операций, как это делалось в приведенном примере. Но при этом подразумевается, что для обычного, а не сквозного выполнения каждый блок c a s e должен оканчиваться оператором b re ak. Сквозной метод не способ­ ствует устойчивости работы программы, поскольку при ее доработке могут возникнуть разные побочные эффекты. За исключением использования нескольких меток для одной и той же операции, сквозного выполнения следует по возможности избегать, а все слу­ чаи, когда оно все же применяется, тщательно комментировать. С точки зрения хорошего стиля программирования рекомендуется ставить оператор break даже в конце последнего блока (в данном случае de f au l t), хотя в этом нет не­ обходимости. Со временем, когда в конец оператора s w i t c h добавится еще один блок c a s e , этот нехитрый прием подстраховки спасет вас от лишних неприятностей. Упражнение 3.2. Напишите функцию под именем e s c ape ( s , t ) , которая бы преобра­ зовывала символы наподобие конца строки и табуляции в управляющие последователь­ ности языка С, такие как \n и \ t , в процессе копирования строки t в строку s . Восполь­ зуйтесь оператором s w i t ch. Напишите функцию также и для противоположной опера­ ции - преобразования символических управляющих последовательностей в фактические управляющие символы.

УПРАВЛ ЯЮЩИЕ КОН СТРУКЦИ И

73

3 . 5 . Ц и кл ы

-

wh i l e

и

for

Циклы whi l e и for нам уже встречались. Первый из них устроен следующим образом: whi l e ( выражение ) опера тор

Здесь вначале вычисляется выражение. Если оно не равно нулю, то выполняется опера тор, а затем выражение вычисляется снова. Эти действия повторяются до тех пор, пока выражение не станет равным нулю. После этого управление передается в точку программы, следующую за опера тором. Оператор f o r имеет следующее устройство: for ( выражl ; выраж2 ; выражЗ ) опера тор

Эта конструкция эквивалентна следующей: выражl ;

whi l e ( выраж2 ) опера тор ; выражЗ ;

Исключением является применение операции cont inue, которая рассматривается в разделе 3.7. С точки зрения грамматики все три компонента в заголовке цикла f o r являются вы­ ражениями. Чаще всего выражl и выражЗ являются операторами присваивания или вы­ зовами функций, а выраж2 - выражением отношения или логическим выражением. Любую из трех частей можно опустить, хотя точки с запятыми должны остаться на своих местах. Если опустить exprl или ехрrЗ , то соответствующие операции не будут вы­ полняться. Если же опустить проверку условия, expr2, то по умолчапию считается, что условие продолжения цикла всегда истинно, и следующая конструкция станет бесконеч­ ным циклом (зациклится): for ( ; ; ) {

Подразумевается, что такой цикл должен прерываться другими способами, например с помощью операторов break или return. Какой из двух циклов использовать, whi l e или fo r , - это в значительной мере дело вкуса. Например, в следующем цикле нет ни инициализации, ни модификации перемен­ ных цикла, поэтому для него естественной является конструкция whi l e : whi l e ( ( с = get char ( ) ) = = ' 1 1 с = = ' \n ' 1 1 с = = ' \ t ' ) / * пропуск симв олов пус т о г о пространства * / Цикл f o r следует предпочесть тогда, когда есть простая инициализация и инкремен­ тирование переменных цикла, поскольку все управляющие элементы цикла в этом случае удобно сгруппированы вместе в его заголовке. Вот наиболее очевидный вариант: for ( i = О ; i < n ; i + + ) Это стандартная конструкция (идиома) языка С для обработки n элементов масси­ ва - аналог оператора DO в языке Fortran или f o r в Pascal. Но эта аналогия не является

74

ГЛАВА 3

полной, поскольку счетчик и пределы цикла f o r в С можно изменять как угодно внутри самого цикла, причем переменная i сохраняет свое значение при выходе из цикла по любой причине. Поскольку компоненты цикла f o r представляют собой произвольные выражения, его возможности отнюдь не исчерпываются перебором арифметической прогрессии. Тем не менее перегружать инициализацию и инкрементирование в цикле for операциями, не относящимися к делу, - это плохой стиль. В заголовке цикла сле­ дует выполнять только операции по непосредственному управлению этим циклом. Рассмотрим более объемистый пример - версию функции a t o i для преобразования строки в ее числовой эквивалент. Этот вариант будет несколько более общим, чем в гла­ ве 2; он сможет пропускать символы пустого пространства в начале строки, а также не­ обязательные знаки + и . (В главе 4 будет демонстрироваться функция a t o f , которая выполняет аналогичное преобразование над вещественными числами.) Структура программы отражает формат ее входных данных: -

пропустить симв олы пусто г о про с транств а , е сли та ковые е с ть обра б о та ть зна к , е сли он е с ть про чита ть целую ча с ть и пре о бра зова ть ее в число

На каждом этапе выполняются свои операции, после чего строка готова к выполне­ нию следующего. Обработка строки заканчивается, как только встретился первый сим­ вол, недопустимый в целом числе. # include < Ctype . h>

/ * atoi : преобразует строку s в целое число ; версия 2 * / int atoi ( char s [ ] )

{

int i , n , s i gn ; for ( i : О ; i s space ( s [ i ] ) ; i + + ) / * пропус к пробелов и т . п . * / s ign : ( s [ i ] : : ' - ' ) ? - 1 : 1 ; ' + ' 1 1 s [ i ] : : ' - ' ) / * пропуск знака * / if (s [i] i++ ; for ( n : О ; i sdigit ( s [ i ] ) ; i + + ) n : 10 * n + ( s [i] - ' О ' ) ; return s ign * n ;

В стандартной библиотеке имеется более тщательно проработанная функция s t rt o l для преобразования строк в длинные целые числа. (См. раздел 5 приложения Б) Преимущества группировки всех управляющих элементов цикла в одном месте ста­ новятся еще более очевидными во вложенных циклах. Ниже приведена функция для сор­ тировки массива целых чисел по алгоритму Шелла. Основная идея алгоритма, разрабо­ танного Д.Л. Шеллом (D.L. Shell) в 1 959 году, заключается в том, что на первом этапе сортировки сравниваются далеко отстоящие друг от друга элементы, а не соседние, как в более простых методах. Это позволяет быстро ранжировать большие массы неупорядо­ ченных элементов, чтобы на следующих этапах оставалось меньше работы. Интервал между сравниваемыми элементами постепенно уменьшается до единицы, и в этот мо­ мент сортировка сводится к обмену соседних элементов - методу пузырьков. / * she l l sort : сортирует v [ O ] . . . v [n - 1 ) в порядке возра с т ания * / void she l l sort ( int v [ ] , int n )

{

УПРАВЛ ЯЮЩИЕ КОН СТРУКЦИ И

75

int gap , i , j , t emp ; for ( gap = n/ 2 ; gap > О ; gap / = 2 ) for ( i = gap ; i < n ; i + + ) for ( j = i - gap ; j > = O && v [ j ] > v [ j +gap ] ; j - =gap ) { t emp = v [ j ] ; v [ j ] = v [ j +gap ] ; v [ j +gap ] = t emp ;

В этой функции - три вложенных цикла. Внешний цикл управляет расстоянием ме­ жду сравниваемыми элементами, сжимая его в два раза (начиная с n / 2 ) при каждом проходе, пока он не становится равным нулю. Средний цикл выполняет перебор по всем элементам. Внутренний цикл сравнивает все пары элементов, отделенных друг от друга текушим расстоянием gap, и меняет местами те из них, которые стоят в неправильном порядке. Поскольку gap постепенно уменьшается до нуля, все элементы постепенно упорядочиваются. Обратите внимание, что конструкция цикла for позволяет придать внешнему циклу ту же форму, что и внутренним, хотя он и не представляет собой ариф­ метическую прогрессию. Еще одним знаком операции языка С является запятая, которая чаще всего находит применение в операторе f o r . Пара выражений, разделенных запятой, вычисляется слева направо; тип и значение результата равны типу и значению правого операнда. Таким об­ разом, в разделы заголовка оператора f o r можно вставлять по несколько выражений например, для параллельной обработки двух индексов. Для иллюстрации приведем функцию reve r s e ( s ) , которая меняет порядок символов в строке s на обратный пря­ мо на месте, без дополнительных буферных строк: # include < s t r ing . h > / * revers e : обращает порядок симв олов в строке s * / vo id reve rs e ( char s [ ] )

{

int с , i , j ; for ( i = О , j = s t r l en ( s ) - 1 ; i < j ; i + + , j - - ) { с = s [i) ; s [i] s [j ) ; s [j ] = с ;

Однако запятые, которые разделяют аргументы функций, переменные в объявлениях и т.п., не являются знаками операций и не гарантируют порядок вычисления слева на­ право. Операцию "запятая" следует использовать как можно реже и с осторожностью. Наи­ более уместно ее применение в конструкциях, где объединяются в единое целое связан­ ные друг с другом операции, - как, например, в теле цикла f o r в функции reve r s e , или в макросах, где многошаговые вычисления объединяются в одну операцию. Запятую вполне можно применить при обмене местами элементов в функции reve r s e , посколь­ ку этот обмен можно представить себе единой операцией:

76

ГЛАВА 3

for ( i = О , j = s t r l en ( s ) - 1 ; i < j ; i + + , j - - ) с = s [i] , s [i] = s [j ] , s [j ) = с ; Упражнение 3.3. Напишите функцию expand ( s l , s 2 ) , которая бы разворачивала со­ кращенную запись наподобие а - z в строке s 1 в полный список аЬс . . . xyz в строке s 2 . Учитывайте буквы в любом регистре, цифры, а также записи вида а - Ь - с , а - z о - 9 и - а - z . Сделайте так, чтобы знаки - в начале и в конце строки воспринимались букваль­ но, а не как символы развертывания.

3 . 6. Ц и кл ы

-

do - wh i l e

Как было сказано в главе 1 , циклы whi l e и f o r выполняют проверку условия в на­ чале. В противоположность им третий вид циклов в С - конструкция do - whi l e проверяет условие в конце, после выполнения тела цикла. Таким образом, тело цикла всегда выполняется как минимум один раз . Вот синтаксис оператора do: do опера тор whi l e ( выражение) ; Здесь сначала выполняется опера тор, затем вычисляется выражение. Если оно ис­ тинно (не равно нулю), то снова выполняется опера тор, и т.д. Как только выражение становится ложным, выполнение цикла прекращается. За исключением способа проверки выражения, цикл do - wh i l e аналогичен оператору repeat - unt i l в языке Pascal. Опыт показывает, что цикл do - whi l e используется значительно реже, чем whi l e и for. Тем не менее время от времени он оказывается полезным, как в приведенной ниже функции i t oa, преобразующей целое число в строку символов (т.е. выполняющей опе­ рацию, противоположную a t o i ) . Сделать это несколько сложнее, чем может показаться на первый взгляд, поскольку самый простой метод генерирования цифр расставляет их в обратном порядке. В этой функции мы решили сначала сгенерировать строку с обратным порядком цифр, а потом выполнить перестановку: /* i toa : преобразует число n в с троку симв олов s * / void i toa ( int n , char s [ ] )

{

int i , s ign ; i f ( ( s ign = n ) < О ) / * записываем знак * / n = -n; / * делаем число положитель ным * / i = О; do { / * генерируем цифры в обратном порядке * / s [i++J = n % 10 + ' О ' ; / * извлекаем цифру * / / * удаляем е е * / } whi le ( ( n / = 1 0 ) > О ) ; i f ( s ign < О ) s [i++J = ' - ' ; s [i] = ' \О ' ; reverse ( s ) ;

У ПРАВЛ ЯЮЩ ИЕ КОН СТРУКЦИ И

77

Здесь цикл do - whi l e необходим (или по крайней мере уместен), поскольку в строку в любом случае следует поместить хотя бы один символ, даже если n равно нулю. Также мы заключили в фигурные скобки единственный оператор, составляющий тело цикла do - whi l e , хотя в этих скобках и не было необходимости, чтобы поспешный чи­ татель не принял блок whi l e в конце do за заголовок отдельного цикла whi l e . s

Упражнение 3.4. В представлении чисел с помощью дополнения до двойки наша версия функции i toa не умеет обрабатывать самое большое по модулю отрицательное число, т.е. значение n, равное - (2длина_снова - 1 ). Объясните, почему это так. Доработайте функ­ цию так, чтобы она выводила это число правильно независимо от системы, в которой она работает. Упражнение 3.5. Напишите функцию i t ob ( n , s , Ь ) , которая бы преобразовывала це­ лое число n в его символьное представление в системе счисления с основанием Ь и по­ мещала результат в строку s . Например, вызов i toa ( n , s , 1 6 ) должен представлять n в виде шестнадцатеричного числа. Упражнение 3.6. Напишите версию i t oa, принимающую три аргумента вместо двух. Третий аргумент будет минимальной шириной поля; преобразованное число следует до­ полнить пробелами слева, если оно недостаточно длинное, чтобы заполнить это поле.

3 . 7. Операторы

b r e ak

и

c on t i nue

Иногда бывает удобно выйти из цикла другим способом, отличным от проверки усло­ вия в его начале или в конце. Оператор break вызывает принудительный выход из цик­ лов for, whi l e и do - аналогично выходу из оператора s w i t ch. Выход выполняется из ближайшего (самого внутреннего) цикла или оператора swi t ch. Приведенная ниже функция t r im удаляет пробелы, символы табуляции и конца строки из "хвоста" символьной строки, используя оператор break для выхода из цикла, как только найден самый правый символ, не являющийся одним из перечисленных "пустых" символов. / * t r im : удаляет симв олы пус того прос тран ства из конца с троки * / int t rim ( char s [ ] )

{

int n ; for ( n = s t r l en ( s ) - 1 ; n > = О ; n - - ) i f ( s [ n ] ! = ' ' && s [ n ] ! = ' \ t ' && s [ n] break ; s [ n+ l ] = ' \ 0 ' ; return n ;

! = 1 \n ' )

Функция s t rl en возвращает длину строки. Цикл f o r начинает работу с конца стро­ ки и продвигается по ней назад, разыскивая первый символ, не являющийся одним из трех символов пустого пространства. Работа цикла заканчивается, как только такой сим­ вол найден или как только n станет отрицательным (т.е. пройдена вся строка). Рекомен­ дуется проверить, правильно ли ведет себя функция даже в тех случаях, когда строка пустая или содержит только символы пустого пространства.

78

ГЛАВА 3

Оператор cont inue напоминает b reak, но используется реже; он передает управ­ ление на следующую итерацию (проход) ближайшего цикла f o r, whi l e или do. В цик­ ле whi l e или do это означает немедленную проверку условия, тогда как в цикле f o r дополнительно выполняется инкрементирование. Оператор cont inue применим только к циклам, но не к оператору swi t c h. Если поставить cont inue внутри swi t c h, в свою очередь находящегося внутри цикла, то управление будет передано на следующий проход этого цикла. Например, следующий фрагмент кода обрабатывает только неотрицательные элемен­ ты массива а; отрицательные просто пропускаются: for ( i = О ; i < n; i + + ) { / * пропускаем отрица тель ные элементы * / if ( a [i] < О ) continue ; . . . / * обрабатываем положитель ные элементы * / Оператор cont inue часто используется там, где следующая за ним часть цикла слишком длинная и сложная, так что выделение ее отступами в условном операторе ухудшает удобочитаемость.

3 . 8. Оператор

go t o

и метки

В языке С имеется оператор got o - бездонный источник потенциальных неприят­ ностей - и метки для перехода с его помощью. В техническом плане оператор got o никогда н е бывает необходим, и н а практике почти всегда легче обходиться без него. В этой книге он вообще не используется. Тем не менее есть несколько ситуаций, в которых и go t o может найти свое место под солнцем. Наиболее распространенная - это необходимость прекратить работу управляющей структуры с большим количеством вложений, например выйти сразу из двух и более вложенных циклов. Оператор b reak тут не применим непосредственно, поскольку он обеспечивает выход только из внутреннего, ближайшего цикла. Поэтому получаем вот что: for ( . . . ) for ( . . . ) { i f ( di s aster ) goto error ; error : решить проблему

Такая конструкция бывает удобной, если код для обработки ошибок нетривиален, а сами ошибки могут происходить в разных местах. Метка для перехода имеет ту же форму, что и имя переменной. После нее ставится двоеточие. Ее можно ставить перед любым оператором в той же функции, в которой на­ ходится соответствующий goto. Область действия метки - вся функция. В качестве следующего примера рассмотрим задачу определения того, имеют ли мас­ сивы а и Ь хотя бы один общий элемент. Вот один из возможных вариантов решения: УПРАВЛ ЯЮЩИЕ КОН СТРУКЦИ И

79

for ( i = О ; i < n ; i + + ) for ( j = О ; j < m ; j + + ) if (a [i] == b [j ] ) goto found ; / * общие элементы не найдены * / found : / * е с т ь общие элементы :

a [ i] == b [j ] * /

Код, в котором есть оператор goto, всегда можно переписать без него, хотя, воз­ можно, придется добавить дополнительную проверку или переменную. Например, реше­ ние той же задачи без goto выглядит так: found = О ; for ( i = О ; i < n && ! found ; i + + ) for ( j = О ; j < m && ! f ound ; j + + ) if (a [i] == b [j ] ) found = 1 ; i f ( found ) / * элемент найден : a [ i - 1 ] b [j - 1] */ else

/ * не т общих элементов * / З а немногими исключениями наподобие приведенного здесь примера код, основан­ ный на переходах с помощью оператора g o t o , труднее понимать и дорабатывать, чем код без этих операторов. Хотя мы и не утверждаем этого категорически, все же оператор goto следует стараться употреблять как можно реже или вообще никогда.

80

ГЛА ВА 3

Глава 4

Фун к ц ии и стру ктура про г ра м м ы Функции не только помогают разбить большие вычислительные задачи на набор ма­ леньких, но и позволяют программистам пользоваться разработками предшественников, а не начинать все заново. Хорошо написанные функции скрывают подробности своей работы от тех частей программы, которым их знать не положено, таким образом прояс­ няя задачу в целом и облегчая процесс внесения исправлений и дополнений. Язык С построен так, чтобы сделать использование функций максимально простым и эффективным. Обычно программы на С состоят из множества мелких функций, а не не­ скольких сравнительно больших. Программа может храниться в одном или нескольких файлах исходного кода, которые можно компилировать по отдельности, а затем собирать вместе, добавляя к ним предварительно скомпилированные функции из библиотек. Здесь этот процесс не будет освещаться подробно, поскольку его частности сильно меняются от системы к системе. Объявление и определение функций - это тот раздел языка С, который претерпел самые заметные изменения с введением стандарта ANSI. В главе 1 говорилось о том, что теперь стало возможным объявлять типы аргументов при объявлении функции. Изме­ нился также синтаксис определения функций с целью согласовать объявления с опреде­ лениями. Благодаря этому компилятор может обнаружить гораздо больше ошибок, ранее ему недоступных. Более того, при правильном объявлении аргументов соответствующие приведения типов выполняются автоматически. Сейчас стандарт задает более четкие правила области действия имен; в частности, выдвигается требование, чтобы каждый внешний объект определялся в программе ровно один раз. Более общей стала инициализация - теперь можно инициализировать автома­ тические массивы и структуры. У совершенствованию подвергся также и препроцессор С. В число новых возможно­ стей препроцессора входят расширенный набор директив условной компиляции, новый метод создания символьных строк в кавычках из аргументов макросов, а также более со­ вершенный контроль над процессом раскрытия макросов .

4. 1 . О сновы создания функ ц ий

Для начала спроектируем и напишем программу, выводящую каждую из строк своих входных данных, в которой встречается определенный "шаблон" - заданная строка символов. (Это частный случай программы под названием grep из системы Unix.) Пусть, например, задан поиск строки 11 ou l d 11 в следующем наборе строк: Ah Love ! cou l d you and I with Fate consp ire Т о grasp this sorry S cheme o f Things ent i re ,

Would not we shatter i t to b i t s - and then Re - mould it nearer to the Heart ' s De s i re ! 1 На выходе программы получится следующее: Ah Love ! could you and I with Fate consp ire Would not we shatter i t t o bits - and then Re - mould it nearer to the Heart ' s Des i re ! Задача естественным образом раскладывается на три части: whi l e ( на вход поступа е т о чередная с трока ) i f ( стр ока с одержит зада нный ша блон) выв е с ти е е

Хотя, разумеется, весь код для решения этих задач можно поместить в функцию ma i n, лучше будет все-таки разделить его на части и организовать в виде отдельных функций, чтобы воспользоваться структурированностью программы. С маленькими час­ тями удобнее иметь дело, чем с большим целым, поскольку в функциях можно скрыть несущественные детали реализации, при этом избежав риска нежелательного вмешатель­ ства одной части программы в другую. К тому же отдельные части могут даже оказаться полезными для других задач. Первую часть нашего алгоритма (ввод строки) реализует функция ge t l ine, напи­ санная еще в главе 1 , а третью (вывод результата) - функция p r i n t f , которую давно написали за нас. Таким образом, нам осталось только написать функцию, которая бы оп­ ределяла, содержит ли текущая строка заданный шаблон-образец. Эта задача будет решена с помощью функции s t r i ndex ( s , t ) , которая возвращает позицию (или индекс) в строке s, с которой начинается строка t , либо - 1 , если строка s не содержит t . Поскольку массивы в С начинаются с нулевой позиции, индексы могут быть нулевыми или положительными, а отрицательное значение наподобие - 1 удобно для сигнализации ошибки. Если впоследствии понадобится применить более расширен­ ный алгоритм поиска образцов в тексте, в программе достаточно будет заменить только s t r i ndex, а остальной код не надо будет изменять. (В стандартной библиотеке есть функция s t r s t r, аналогичная по своему назначению s t r i ndex, только она возвраща­ ет не индекс, а указатель.) Имея перед собой общую схему алгоритма, запрограммировать его детали уже не так сложно. Ниже приведен полный текст программы, по которому можно судить, как все его части сведены воедино. Образец, который можно разыскивать в тексте, пока что представляется литеральной строкой, что никак нельзя считать достаточно общим меха­ низмом реализации. Вскоре будет рассмотрен вопрос инициализации символьных мас­ сивов, а в главе 5 рассказывается, как сделать задаваемый образец параметром, который бы указывался при запуске программы. В программу также включена несколько моди­ фицированная версия функции ge t l i n e ; возможно, вам будет поучительно сравнить ее с приведенной в главе 1 . # inc lude < s tdio . h> #de f ine МAXLINE 1 0 0 0 / * максималь ная длина в ходной строки * / int get l ine ( char l ine [ ] , int max ) ; 1

82

Когда к жизни Любовь меня в мир призвала, Мне уроки любви она сразу дала, Ключ волшебный сковала из сердца частичек И к сокровищам духа меня привела. (Омар Хайям, пер. Н. Тенигиной)

ГЛА ВА 4

int strindex ( char source [ ] , char s earchfor [ ] ) ; char pattern [ ] = " oul d " ;

/ * образец для поиска * /

/ * поиск всех с трок , содержащих заданный образец * / main ( )

{

char l ine [МAXLINE ] ; int found = О ; whi l e ( ge t l ine ( l ine , МAXL INE ) > О ) i f ( s t rindex ( l ine , pattern ) >= О ) { print f ( 11 % s 11 , l ine ) ; found+ + ; return found ;

/ * get l ine : считывает строку в s , в о з вращает е е длину * / int get l ine ( char s [ ] , int l im )

{

int с , i ; i = О; whi le ( - - l im > О && ( c =get char ( ) ) s [i++] = с ; i f ( с = = ' \n ' ) s [i++] = с ; s [i] = ' \0 ' ; return i ;

! = EOF && с ! = ' \n ' )

/ * strindex : возвращает индекс строки t в s , - 1 при о т сутствии * / int s t rindex ( char s [ ] , char t [ ] )

{

int i , j , k ; for ( i = О ; s [ i ] ! = ' \ 0 ' ; i + + ) { for ( j = i , k= O ; t [ k] ! = ' \ 0 ' && s [ j ] = = t [ k ] ; j + + , k+ + ) ; i f ( k > о && t [k] = = 1 \ 0 1 ) return i ; return - 1 ; Каждое из определений функций имеет следующую форму:

тип - возвращ- зна ч имя - функции ( объявления аргументов )

{

объявления и опера торы

Различные части этого определения могут отсутствовать; самая минимальная функ­ ция имеет вид dummy ( ) { } ФУНКЦИИ И СТРУКТУРА ПРОГРАММЫ

83

Она ничего не делает и ничего не возвращает. Такого рода функция, не выполняющая никаких операций, часто бывает полезна как заменитель ("заглушка") в ходе разработки программы. Если тип возвращаемого значения опущен, по умолчанию подразумевается int. Всякая программа является всего лишь набором определений переменных и функций. Функции обмениваются данными посредством передачи аргументов и возвращения зна­ чений, а также через внешние переменные. Функции могут следовать друг за другом в файле исходного кода в любом порядке, и текст программы можно разбивать на любое количество файлов, но при этом запрещено делить текст функции между файлами. Оператор return - это механизм возвращения значений из вызываемой функции в вызывающую. После r e t urn может идти любое выражение: return выражение По мере необходимости выражение преобразуется в тип, возвращаемый функцией согласно ее объявлению и определению. Часто выражение заключают в круглые скоб­ ки, но это не обязательно. Вызывающая функция имеет полное право игнорировать возвращаемое значение. Бо­ лее того, после re turn вовсе не обязано стоять выражение ; в этом случае в вызываю­ щую функцию ничего не передается. Точно так же управление передается в вызываю­ щую функцию без возврата значения, если достигнут конец тела функции, т.е. закры­ вающая правая фигурная скобка. Если функция возвращает значение из одного места и не возвращает из другого, это допускается синтаксисом языка, но может указывать на ошибку. Во всяком случае, если функция не возвращает значения, хотя по объявлению должна это делать, в передаваемых ею данных гарантированно будет содержаться "мусор" - случайные числа. Программа поиска образцов в строках возвращает из ma i n результат работы про­ граммы - количество найденных совпадений. Это число может использоваться опера­ ционной средой, которая вызвала программу. Процедура компиляции и компоновки программы на С, хранящейся в нескольких файлах исходного кода, сильно отличается в разных системах. Например, в системе Unix все это делает команда с е , уже упоминавшаяся в главе 1 . Предположим, что три разные функции программы хранятся в трех файлах: ma i n . с, ge t l ine . с и s t r i ndex . с. Для компиляции и компоновки их в программу применяется следующая команда: с е ma in . c get l ine . c s t r i ndex . c Она помещает объектный код, полученный в результате компиляции, в файлы ma in . o, ge t l ine . o и s t r i ndex . o, а затем компонует их все в выполняемую про­ грамму а . out . Если, скажем, в файле ma i n . с встретилась синтаксическая ошибка, этот файл можно будет перекомпилировать заново и скомпоновать с уже существующими объектными файлами с помощью такой команды: се ma in . c get l ine . o s t rindex . o При использовании команды с е объектный код отличают от исходного благодаря применению разных расширений имен файлов - соответственно . о и . с . Упражнение 4. 1 . Напишите функцию s t rr i ndex ( s , t ) , которая бы возвращала ин­ декс самого правого вхождения строки t в s , либо - 1 , если такой строки в s нет.

84

ГЛАВА 4

4 . 2 . Функ ции , возвращающие нецелые зна ч ения До сих пор в наших примерах функции либо не возвращали никаких значений (vo i d), либо возвращали числа типа i n t . А что если функция должна возвращать что-то другое? Многие функции для математических расчетов, такие как s qrt , s i n или c o s , возвращают числа типа douЫ e . Другие специализированные функции возвращают дан­ ные и других типов. Чтобы показать это на примере, напишем функцию a t o f ( s ) для преобразования строки символов s в вещественное число двойной точности, которое она изображает. Функция a t o f - это расширенная версия функции a t o i , разные версии которой демонстрировались в главах 2 и 3. Она может обрабатывать знак и десятичную точку, а также различать, присутствует ли в числе целая и/или дробная часть. Наша вер­ сия не является высококлассной функцией преобразования, потому что иначе она бы за­ няла намного больше места. В стандартной библиотеке С функция a t o f имеется и объ­ явлена в заголовочном файле < s td l i b . h > . Во-первых, следует явным образом объявить тип возвращаемого и з a t o f значения, поскольку это не i n t . Имя типа ставится перед именем функции: # inc lude < ctype . h >

/ * atof : пре образование строки s в число типа douЫ e * / douЫ e atof ( char s [ ] )

{

douЫ e va l , powe r ; int i , s ign ; for ( i = О ; i s space ( s [ i ] ) ; i + + )

/ * пропуск пробелов * /

s ign = ( s [ i ] - ) ? -1 : 1; i f ( s [ i ] == ' + ' 1 1 s [ i ] == - ' ) i++ ; for ( val = О . О ; i sdigi t ( s [ i ] ) ; i + + ) '0'); val = 1 0 . О * va l + ( s [ i ] if (s [i] == . ' ) i++ ; f o r ( powe r = 1 . 0 ; i s d i g i t ( s [ i ] ) ; i + + ) va l = 1 О . О * va l + ( s [ i ] 'о') ; power * = 1 0 ; '

'

'

-

'

-

return s ign * val / powe r ; Во-вторых, и это не менее важно, вызывающая функция должна знать, что a t o f воз­ вращает нецелое число. Один из способов сделать это - объявить a t o f прямо в вызы­ вающей функции. Такое объявление показано ниже в программе-калькуляторе - на­ столько примитивной, что с ее помощью едва ли можно подбить даже бухгалтерский ба­ ланс. Она считывает числа по одному в строке (перед числом может стоять знак), складывает их и выводит текущую сумму после ввода каждой строки:

ФУНКЦИИ И СТРУКТУРА ПРОГРАММЫ

85

# include < S tdio . h> #de f ine МAXLINE 1 0 0 / * примитивный каль кулятор * / ma in ( )

{

douЫ e sum , atof ( char [ ] ) ; char l ine [МAXLINE ] ; int ge t l ine ( char l ine [ ] , int max ) ; sum = О ; whi le ( ge t l ine ( l ine , МAXLINE ) > О ) print f ( " \ t%g \ n " , sum + = a t o f ( l ine ) ) ; re turn О ;

В программе имеется следующее объявление: douЫ e sum , atof ( char [ ] ) ; В нем говорится, что sum является переменной типа douЫ e , а a t o f - функцией, принимающей один аргумент типа char [ ] и возвращающей значение типа douЫ е . Функция a t o f должна иметь согласованные между собой объявление и определение. Если сама функция a t o f и ее вызов в функции ma i n имеют несогласованные типы и находятся в одном файле исходного кода, то компилятор выдаст сообщение об ошибке. Однако если (что более вероятно) функция a t o f компилируется отдельно, то несоответ­ ствие типов замечено не будет - функция возвратит число типа douЬ l e , а ma in вос­ примет его как i n t , и в результате получится бессмыслица. В свете всего сказанного насчет соответствия объявлений и определений функций это может показаться странным. Но дело в том, что здесь отсутствует прототип функции и функция объявляется неявным образом - своим первым появлением в таком выраже­ нии, как sum += atof ( l ine ) Если в выражении встречается имя, которое ранее не объявлялось, и если после него стоят круглые скобки, оно по контексту объявляется функцией. По умолчанию считает­ ся, что эта функция возвращает число типа i n t , а относительно ее аргументов не дела­ ется никаких предположений. Более того, если объявление функции не содержит аргу­ ментов, то и в этом случае не делается никаких предположений относительно их типа и количества, как в этом примере: douЫ e ato f ( ) ; Проверка соответствия параметров для a t o f просто отключается. Это особое значе­ ние пустого списка аргументов введено для того, чтобы старые программы на С могли компилироваться новыми компиляторами. Однако использовать этот стиль в новых про­ граммах - дурной тон. Если функция принимает аргументы, объявляйте их. Если же она не принимает никаких аргументов, пишите vo i d. При наличии функции a t o f , объявленной должным образом, с ее помощью можно написать функцию a t o i (для преобразования строки в целое число): / * atoi : преобразует строку s в целое число с помощью atof * / int atoi ( char s [ ] )

{

86

ГЛАВА 4

douЫ e atof ( char s [ ] ) ; return ( int ) atof ( s ) ; Обратите внимание на структуру объявления и на оператор re turn, обычная форма которого такова: return выражение ; Стоящее в операторе выражение всегда преобразуется к возвращаемому типу перед выполнением оператора. Поэтому значение a t o f , имеющее тип douЬ l e , автоматически преобразуется в тип in t в том случае, если оно фигурирует в этом операторе, поскольку функция a t o i возвращает тип i n t . При выполнении этой операции возможна частич­ ная потеря информации, поэтому некоторые компиляторы в таких случаях выдают пре­ дупреждения. А вот при наличии явного приведения типов компилятору становится ясно, что операция выполняется намеренно, так что он не выдает предупреждений. Усовершенствуйте функцию a t o f так, чтобы она понимала экспонен­ циальную запись чисел вида 123 . 45е-6

Упражнение 4.2.

В этой записи после вещественного числа может следовать символ е или показатель степени - возможно, со знаком.

Е,

а затем

4 .3 . В не ш ние пере м енные Программа на С обычно состоит из набора внешних объектов, являющихся либо пе­ ременными, либо функциями. Прилагательное "внешний" здесь употребляется как про­ тивоположность слову "внутренний", которое относится к аргументам и переменным, определенным внутри функций. Внешние переменные определяются вне каких бы то ни было функций и поэтому потенциально могут использоваться несколькими функциями. Сами по себе функции - всегда внешние, поскольку в С нельзя определить функцию внутри другой функции. По умолчанию внешние переменные и функции имеют то свой­ ство, что все ссылки на них по одним и тем же именам - даже из функций, скомпилиро­ ванных отдельно, - являются ссылками на один и тот же объект программы. (Стандарт называет это свойство внешним связыванием - extemal linkage). В этом отношении внешние переменные являются аналогом переменных блока COMMON в языке Fortran или переменных в самом внешнем блоке в программе на Pascal. Позже будет показано, как определить внешние переменные и функции, чтобы они были доступны (видимы) только в одном файле исходного кода. Поскольку внешние переменные имеют глобальную область видимости, они пред­ ставляют собой способ обмена данными между функциями, альтернативный передаче аргументов и возврату значений. Любая функция может обращаться к внешней перемен­ ной по имени, если это имя объявлено определенным образом. Если функциям необходимо передавать друг другу большое количество элементов данных, то внешние переменные удобнее и эффективнее, чем длинные списки аргумен­ тов. Однако, как было указано в главе 1 , здесь не все так однозначно, поскольку от этого страдает структурированность программы и возникает слишком сильная перекрестная зависимость функций друг от друга. ФУНКЦИИ И СТРУКТУРА ПРОГРАММЫ

87

Внешние переменные также бывают полезны из-за их более широкой области дейст­ вия и более длительного времени жизни. Автоматические переменные являются внут­ ренними для функций; они создаются при входе в функцию и уничтожаются при выходе из нее. С другой стороны, внешние переменные существуют непрерывно и хранят свои значения от одного вызова функции до другого. Поэтому если двум функциям необхо­ димо иметь общий набор данных, но при этом ни одна из них не вызывает другую, быва­ ет удобнее поместить общие данные во внешние переменные, чем передавать их туда­ сюда в виде аргументов. Рассмотрим эти вопросы более подробно на объемном примере. Задача состоит в том, чтобы написать программу-калькулятор с набором операций + , , * и / . Калькуля­ тор будет использовать обратную польскую, или бесскобочную, запись ввиду простоты ее реализации вместо обычной инфиксной. (Обратная польская запись применяется в не­ которых карманных калькуляторах, а также в языках программирования Forth и Postscript.) В обратной польской записи каждый знак операции стоит после своих операндов. Возьмем, например, следующее выражение: (1 2 ) * (4 + 5 ) -

-

В обратной польской записи оно выглядит так: 1 2 - 4 5 + * Скобки здесь не нужны, поскольку запись расшифровывается однозначно, если из­ вестно, сколько операндов у каждой операции. Реализация алгоритма проста. Каждый операнд помещается в стек; как только посту­ пает знак операции, из стека извлекается нужное количество операндов (для двухмест­ ной операции - два), к ним применяется данная операция, и результат снова помещает­ ся в стек. В приведенном примере в стек вначале помещаются 1 и 2, а затем они заменя­ ются их разностью (- 1 ). Затем в стек помещаются 4 и 5, после чего они заменяются их суммой (9). Произведение -1 и 9, т.е. -9, заменяет оба вычисленных операнда в стеке. Как только встретился конец строки входного потока, из стека извлекается и выводится на экран верхнее значение. Таким образом, программа организована в виде цикла, выполняющего операции над знаками и операндами по мере их поступления: whi l e ( о чередной зна к или операнд - не конец файла ) i f ( число ) поме с тить опер а нд в стек

e l s e if ( зна к опера ции) извле чь операнд из стека выполнить опера цию поме с тить рез уль т а т в стек e l se if ( конец с тр оки) извле чь и выв е с ти в ерх стека

else ошибка

Операции помещения в стек и извлечения из него тривиальны, но после добавления проверки и исправления ошибок эти операции становятся достаточно объемными, чтобы поместить их в отдельные функции, а не повторять длинные фрагменты кода несколько раз. Необходимо также иметь отдельную функцию для извлечения следующего операнда или знака операции.

88

ГЛАВА 4

Осталось обсудить основное проектное решение, необходимое для разработки данной программы. Это вопрос о том, где разместить стек, т.е. какая из функций программы бу­ дет с ним работать. Один из возможных вариантов - держать его в функции ma in, пе­ редавая как его, так и текушую позицию в нем в функции, которые помещают и извле­ кают данные. Но функции ma in нет нужды знать что-либо о переменных, которые управляют стеком; ей нужно только уметь помещать и извлекать данные. Поэтому будем хранить стек и связанную с ним информацию во внешних переменных, к которым будут обращаться функции p u s h и рор, но не функция ma in. Превратить это словесное описание в код сравнительно просто. Если пока что ограни­ читься программой, состоящей из одного файла исходного кода, получится следующее: # i nс ludе - директивы # dе f inе - директивы о бъявления функций, исп оль зуемых в ma i n ma in ( )

{ ... }

внешние переменные для функций push и рор vo i d push ( douЫ e f ) { douЫ e pop ( vo i d ) { . . . int getop ( char s [ ] )

.

.

.

}

{ ...

функции, вызыв а емые из getop

Ниже рассказывается, как все это можно распределить по двум или нескольким фай­ лам исходного кода. Функция ma in организована в виде цикла, содержащего объемистый оператор sw i t ch по типам операций и операндов. Это более типичный пример применения опе­ ратора swi t ch, чем тот, который приводился в разделе 3 .4 . # inc lude < s tdio . h > # inc lude < s tdl ib . h > # de f i ne МАХОР # de f ine NUМBER

100 '0'

/ * для объявления a t o f ( ) * / / * максималь ный размер операнда или знака * / / * сигнал , ч т о обнаружено число * /

int getop ( char s [ ] ) ; vo i d push ( douЬ l e ) ; douЫ e pop ( vo i d ) ;

/ * каль кулятор с обратной поль ской записью * / ma i n ( )

{

int typ e ; douЫ e ор2 ; char s [МАХО Р ] ; whi l e ( ( typ e = getop ( s ) ) swi tch ( typ e ) { c a s e NUМBER : push ( at o f ( s ) ) ;

ФУНКЦИИ И СТРУКТУРА ПРОГРАММЫ

! = EOF )

{

89

break ; case

' + ' :

push ( pop ( ) + р ор ( ) ) ; break ; case ' * ' : push ( pop ( ) * р ор ( ) ) ; break ; case ' - ' : ор 2 = р ор ( ) ; push ( pop ( ) - ор2 ) ; break ; case ' / ' : ор2 = р ор ( ) ; i f ( ор2 ! = О . О ) push ( рор ( ) / ор2 ) ; else print f ( " error : z e ro divi sor\n " ) ; break ; case ' \n ' : print f ( " \ t % . 8 g \ n " , рор ( ) ) ; break ; de f aul t : print f ( " error : unknown command % s \n " , s ) ; break ; return О ; Поскольку сложение ( + ) и умножение ( * ) - коммутативные операции, порядок, в ко­ тором их аргументы извлекаются из стека, не имеет значения. А вот для вычитания ( -) и деления ( / ) следует различать левый и правый операнды. Допустим, вычитание пред­ ставлено следующим образом : push ( pop ( ) - р ор ( ) ) ; / * НЕПРАВИЛЬНО * /

В этом случае порядок, в котором обрабатываются два вызова рор ( ) , не определен. Чтобы гарантировать правильный порядок, необходимо извлечь первый операнд заранее во временную переменную, как это и сделано в функции ma i n. #de f ine МAXVAL 1 0 0 / * максимальная глубина стека val * / int sp = О ; douЫ e val [МAXVAL ] ;

/ * следующая с в ободная позиция в с теке * / / * стек операндов * /

/ * push : помещает число f в с т ек операндов * / void push ( douЬl e f )

{

i f ( sp < МAXVAL ) va l [ sp+ + ] = f ; else print f ( " error : s t ack ful l , can ' t push %9\n " , f ) ;

/ * р ор : извлекает и возвращает в ерхнее число из стека * / douЫ e pop ( voi d )

90

ГЛАВА 4

i f ( sp > О ) return va l [ - - sp ] ; else { print f ( " error : s t ack empty\n " ) ; return О . О ;

Переменная является внешней, если она определена вне каких бы то ни было функ­ ций. Поэтому сам стек и его индекс, которые совместно используются функциями push и рор, определены вне этих функций. Но функция ma i n сама не обращается ни к стеку, ни к его индексу непосредственно, поэтому представление стека можно от нее спрятать. Теперь займемся реализацией функции ge t op, которая доставляет на обработку оче­ редной операнд или знак операции. Ее задача проста. Сначала необходимо пропустить пробелы и тому подобные символы. Если очередной символ не является цифрой или де­ сятичной точкой, возвратить его. В противном случае накопить строку цифр (возможно, с десятичной точкой) и возвратить NUМBER сигнал о том, что на вход поступило число. -

# inc lude < ctype . h> int getch (void ) ; vo id ungetch ( int ) ;

/ * getop : извлекает следующий операнд или знак операции * / int get op ( char s [ ] ) ;

{

int i , с ; whi l e ( ( s [ О ]

с

getch ( ) )

1

1

1 1 с

1 \t 1 )

'

s [l] = ' \0 ' ; i f ( ! i sdigit ( c ) && с return с ; /* i = О; i f ( i sdigit ( c ) ) /* whi le ( i sdigit ( s

!= ' . ' ) не число * /

накопле ние целой части * / [ + + i ] = с = get ch { ) ) )

if ( с == ' . ' ) / * накопление дробной части * / whi le ( i sdigi t ( s [ + + i ] = с = getch { ) ) ) ; s [i] = ' \0 ' ; i f ( с ! = EOF ) ungetch ( с ) ; return NUМBER ; Что такое ge t ch и ung e t ch? Часто бывает, что программа не может определить, достаточно ли она прочитала данных, до тех пор, пока не прочитает их слишком много. Один из примеров - это накопление символов, составляющих число. Пока не встретит­ ся первый символ, не являющийся цифрой, ввод числа не завершен; но как только он встретится, он оказывается лишним, и программа не знает, как его обрабатывать. Проблема была бы решена, если бы существовал способ вернуть назад ненужный символ. Тогда каждый раз, когда программа считывала бы на один символ больше, чем

ФУНКЦИ И И СТРУКТУРА ПРОГРА ММ Ы

91

нужно, она могла бы вернуть его в поток ввода, а остальные части программы продол­ жали бы работать с этим потоком так, как будто последней операции ввода не было во­ все. К счастью, возврат символа в поток довольно легко смоделировать, написав для это­ го пару специальных функций. Функция g e t c h будет извлекать следующий символ из потока ввода, а функция ung e t c h - запоминать символы, возвращаемые в поток, что­ бы при следующем вызове g e t c h вначале вводились они, а потом уже те, которые дей­ ствительно поступают из потока ввода. Совместная работа этих функций организована просто. Функция unge t ch помещает возвращенный символ в общий буфер - массив символов. Функция ge t ch читает дан­ ные из буфера, если они там есть, или вызывает get char, если буфер пустой. Необхо­ димо также иметь индексную переменную, которая бы хранила позицию текущего сим­ вола в буфере. Поскольку буфер и индекс совместно используются функциями g e t c h и unget ch, а также должны сохранять свои значения между вызовами этих функций, их следует сде­ лать внешними переменными. Запишем итоговый код для ge t ch, unge t ch и их общих переменных: #de f ine BUFS I Z E 1 0 0 char bu f [ BUFS I ZE ] ; int bu fp = О ;

/ * буфер для ungetch * / / * следующа я свободн а я по зиция в buf * /

int getch ( vo i d ) / * ввод символа ( возможно , возвращенного в поток ) * /

{

return ( bufp > О ) ? bu f [ - - bu fp]

: get char ( ) ;

voi d ungetch ( i nt с ) / * воз вращение символа в поток ввода * /

{

i f ( bufp > = BUFS I Z E ) print f ( " unget ch : too many charact e r s \n " ) ; else buf [bufp+ + ] = с ;

Стандартная библиотека содержит функцию unge t c , которая возвращает в поток один символ; она будет рассматриваться в главе 7. Здесь же для иллюстрации более об­ щего подхода в качестве буфера возврата используется целый массив, а не один символ. При наличии базовой структуры программы усовершенствование калькулятора уже не представляет особых трудностей. Реализуйте операцию взятия ос­ татка ( % ) и работу с отрицательными числами. Упражнение 4.3.

Добавьте в программу реализацию команд для вывода верхнего эле­ мента стека без его удаления, для создания в стеке дубликата этого элемента и для обме­ на местами двух верхних элементов. Также реализуйте команду очистки стека.

Упражнение 4.4.

Упражнение 4.5.

Добавьте реализацию библиотечных математических функций s i n

,

ехр и pow. См. заголовочный файл < ma t h . h > в приложении Б, раздел 4.

Добавьте команды для работы с переменными. (Можно использовать различных переменных, если разрешить имена только из одной буквы.) Введите пе­ ременную, обозначающую последнее выведенное на экран число. Упражнение 4.6. 26

92

ГЛАВА 4

Упражнение 4.7. Напишите функцию unge t s ( s ) , возвращающую в поток целую стро­ ку символов. Следует ли этой функции знать о существовании переменных buf и bufp, или ей достаточно вызывать unget ch? Упражнение 4.8. Предположим, что в поток ввода никогда не будет возвращаться боль­ ше одного символа. Доработайте функции g e t ch и unge t c h соответственно. Упражнение 4.9. Наши функции ge t ch и unge t ch не могут корректно обработать символ конца файла EOF, возвращенный в поток ввода. Подумайте, как эти функции должны реагировать на EOF в буфере, а затем реализуйте ваш замысел . Упражнение 4.10. В качестве альтернативного подхода можно использовать функцию get l ine для считывания целых строк. Тогда ge t ch и ung e t c h становятся ненужны­ ми. Перепишите программу-калькулятор с использованием этого подхода.

4 . 4 . О бласт ь действия

Функции и внешние переменные, составляющие программу на С, не обязательно компилируются в одно и то же время. Исходный текст программы может храниться в не­ скольких файлах, и к тому же могут подключаться заранее скомпилированные функции из библиотек. В этой связи возникает ряд вопросов, на которые необходимо знать ответы. • Как записать объявления переменных, чтобы они правильно воспринимались во время компиляции? • Как организовать объявления так, чтобы при компоновке программы все ее части правильно объединились в одно целое? • Как организовать объявления так, чтобы каждая переменная присутствовала в од­ ном экземпляре? • Как инициализируются внешние переменные? Рассмотрим эти вопросы на примере, распределив код программы-калькулятора по нескольким файлам. С практической точки зрения эта программа слишком мала, чтобы ее стоило так разбивать, но зато это хорошая иллюстрация важных приемов, применяе­ мых в больших программах . Областью действия (видимости) имени называется часть программы, в пределах ко­ торой можно использовать имя. Для автоматической переменной, объявляемой в начале функции, областью видимости является эта функция. Локальные переменные с одинако­ выми именами, объявленные в разных функциях, не имеют никакого отношения друг к другу. То же самое справедливо и для параметров функций, которые по сути являются локальными переменными. Область действия внешней переменной или функции распространяется от точки, в которой она объявлена, до конца компилируемого файла. Например, пусть ma in, sp, val, pu s h и рор определены в одном файле, в порядке, показанном выше, т.е. ma i n ( )

{ ... }

int sp = О ; douЫ e val [МAXVAL ] ; vo i d push ( douЫ e f ) douЫ e pop ( vo i d )

{ . . } .

{ . . } .

ФУНКЦИИ И СТРУКТУРА ПРОГРАММ Ы

93

Тогда переменные sp и val можно использовать в функциях push и рор, просто об­ ращаясь по именам, - никаких дальнейших объявлений внугри функций не нужно. Однако эти переменные невидимы в функции main, как и собственно функции push и рор . Если же необходимо обратиться к внешней переменной до ее определения или если она определена в другом файле исходного кода, то обязательно нужно вставить объявле­ ние с ключевым словом ext ern. Важно понимать различие между объявлением внешней переменной и ее определени­ ем. Объявление сообщает, что переменная обладает определенными свойствами (в ос­ новном типом), а определение выделяет место в памяти для ее хранения. Если следую­ щие строки фигурируют вне функций, то они являются определениями внешних пере­ менных sp и va l : int sp ; douЫ e val [МAXVAL] ;

Благодаря этому определению выделяется память для хранения переменных. Кроме того, это еще и объявление, действительное до конца файла. С другой стороны, следую­ щие строки дают только объявление, также действительное до конца файла, согласно ко­ торому sp имеет тип in t , а val является массивом типа douЫ e (его размер определя­ ется в другом месте). При этом память не выделяется и переменные не создаются. ext ern int sp ; extern douЫ e va l [ ] ; Во всех файлах, образующих исходный текст программы, должно быть в общей сложности не больше одного определения внешней переменной; в других файлах могут содержаться объявления со словом extern, чтобы оттуда можно было к ней обращать­ ся. (В файле, содержащем определение переменной, также могут находиться и ехtеrn­ объявления ее же.) Размеры массивов обязательно указываются в определении, но необя­ зательно - в объявлении со словом ext e rn. Инициализация внешней переменной выполняется только в определении. Хотя в данной программе такая организация ни к чему, функции push и рор можно было бы определить в одном файле, а переменные val и sp - определить и инициали­ зировать в другом. В итоге для связывания программы в единое целое понадобились бы следующие объявления и определения: в

файле fi l e l :

ext ern int sp ; ext ern douЫ e va l [ ] ; vo id push ( douЫ e f ) { douЫ e pop ( voi d ) { в

.

. . }

файле fi l e2 :

int sp = О ; douЫe va l [МAXVAL] ;

Поскольку объявления с ext e rn в файле fi l e l находятся впереди и вовне опреде­ лений функций, они действительны во всех функциях; одного набора объявлений доста­ точно для всего файла. То же самое необходимо было бы записать, если бы определения переменных sp и val стояли после обращения к ним в одном и том же файле.

94

ГЛАВА 4

4.5. З а гол овочны е ф а йл ы

Теперь рассмотрим, как распределить программу-калькулятор по нескольким файлам исходного кода. Это необходимо было бы сделать, если бы каждый из компонентов про­ граммы имел намного большие размеры, чем сейчас. В таком случае функция ma i n бу­ дет храниться в одном файле под именем ma i n . с, функции pus h и р ор вместе с их пе­ ременными - в другом файле, s tack . с, а функция getop - в третьем файле, ge top . с. Наконец, поместим функции g e t c h и unge t c h в четвертый файл, getch . с . Они будут отделены от остальных, потому что в настоящей, н е учебной программе такие функции берутся из заранее скомпилированных библиотек. Единственное, о чем стоит побеспокоиться, - это как разнести определения и объяв­ ления по разным файлам, сохранив возможность их совместного использования. Мы по­ старались сохранить централизованную структуру, насколько это было возможно, - со­ брать все необходимое в одном месте и использовать его на протяжении всей эволюции программы. Весь этот общий материал помещается в заголовочный файл c a l c . h, кото­ рый подключается к файлам кода по мере необходимости. (Директива # i n c l ude рас­ сматривается в разделе 4. 1 1 .) В результате получается следующая программа: calc . h : # de f i ne NUМBER ' 0 ' vo i d pu sh ( douЬle ) ; douЬle pop ( vo i d ) ; int getop ( char [ ] ) ; int get ch ( vo i d ) ; vo id unget ch ( int ) ; rnain . c : # inc lude < s t di o . h> # include # inc lude " calc . h " #de f ine МАХОР 1 0 0 rnain ( ) {

getop . c :

s t ack . c

# inc lude < s t di o . h> # inc lude < ctype . h> # inc lude " calc . h " getop ( ) {

# inc lude < s tdio . h> # inc lude #de f ine МAXVAL 1 0 0 int sp О; douЫ e val [МAXVAL ] ; vo i d push ( douЬl e ) { =

get ch . c : # inc lude < s t di o . h> #de f ine BUF S I Z E 1 0 0 char bu f [ BUFS I Z E ] ; int bu fp О; int getch ( vo i d )

douЫ e рор ( vo i d )

=

vo i d ung e t ch ( int )

ФУНКЦИИ И СТРУКТУРА ПРОГРА ММЫ

95

С одной стороны, хочется разрешить доступ из каждого файла только к самой необ­ ходимой информации; с другой стороны, уследить за множеством заголовочных файлов довольно нелегко. Нужно искать компромисс: добиться одного можно, только жертвуя другим. Пока объем программы не достиг некоторой средней величины, бывает удобнее хранить все, что необходимо для связи двух частей программы, в одном заголовочном файле. Именно такое решение было принято здесь. В программах, намного больших по объему, требуется более разветвленная организация и больше заголовочных файлов.

4 . 6 . Стати ческие переменные Переменные sp и v a l в файле s t ack . с , а также bu f и bufp в файле g e t c h . с предназначены для внутреннего использования функциями в соответствующих файлах кода; всем остальным частям программы доступ к ним закрыт. Если объявление внешней переменной или функции содержит слово s t a t i c, ее область действия ограничивается данным файлом исходного кода - от точки объявления до конца. Таким образом, внеш­ ние статические переменные - это механизм сокрытия имен наподобие bu f и bu fp в паре функций ge t ch-unge t c h, которые должны быть внешними, чтобы использоваться совместно, но не должны быть доступны за пределами указанных функций. Статическое хранение переменной в памяти задается ключевым словом s t а t i с в начале обычного объявления. Пусть в одном и том же файле компилируются две функ­ ции и две переменные: stat i c char buf [BUFS I ZE ] ; / * буфер для unge tch * / stat i c int bufp = О ; / * следующа я свободная позиция в buf * /

int getch ( vo i d ) { . . . vo id ungetch ( int с ) Тогда никакая другая функция не сможет обратиться к переменным bu f и bu fp, и не возникнет конфликта, если в других файлах программы будут употребляться такие же имена. Аналогично можно скрыть и переменные sp и va l , объявив их статическими, чтобы только функции pu s h и рор могли ими пользоваться для операций со стеком. Чаще всего внешними статическими объявляются переменные, но такое объявление применимо и к функциям. Обычно имена функций являются глобальными и видимыми в любой части программы. Но если функцию объявить статической ( s t a t i c ), ее имя бу­ дет невидимо за пределами файла, в котором она объявлена. Объявление s t a t i c применимо и к внутренним переменным. Внутренние статиче­ ские переменные являются локальными по отношению к конкретной функции, как и ав­ томатические. Но в отличие от автоматических статические переменные продолжают существовать непрерывно, а не создаются и уничтожаются при вызове и завершении функции. Получается, что внутренние статические переменные являются средством по­ стоянного хранения скрытой информации внутри одной функции. Упражнение 4.1 1 . Измените функцию g et op так, чтобы ей не нужно было использовать unge t ch. Подсказка: воспользуйтесь внутренней статической переменной.

96

ГЛАВА 4

4 . 7. Р егистровые переменные

Объявление с ключевым словом reg i s t e r сообщает компилятору, что соответст­ вующая переменная будет интенсивно использоваться программой. Идея заключается в том, чтобы поместить такие (регистровые) переменные в регистры процессора и добить­ ся повышения быстродействия и уменьшения объема кода. Впрочем, компилятор имеет право игнорировать эту информацию. Объявления со словом reg i s t e r выглядят следующим образом : reg i s ter int х ; register char с ; Объявление reg i s t e r применимо только к автоматическим переменным и к фор­ мальным параметрам функций. В случае параметров оно выглядит так: f ( regi s ter uns igned m, reg i s t e r long n )

{

regis ter int i ;

На практике существуют ограничения на употребление регистровых переменных, связанные со свойствами аппаратных устройств компьютера. В каждой функции только очень небольшое количество переменных (и только нескольких определенных типов) можно сделать регистровыми. Однако от лишних объявлений reg i s t e r не бывает ни­ какого вреда, поскольку избыточные или неразрешенные объявления просто игнориру­ ются компилятором. Не разрешается вычислять адрес регистровой переменной (эта тема рассматривается в главе 5), причем независимо от того, помещена ли она на самом деле в регистр процессора. Конкретные ограничения на количество и тип регистровых пере­ менных зависят от системы и аппаратного обеспечения.

4 . 8. Блочная структура

Язык С не является блочно-структурным в том же смысле, как Pascal или другие язы­ ки, поскольку в нем функции нельзя определять внутри других функций. С другой сто­ роны, внутри одной функции переменные можно определять в блочно-структурном сти­ ле. Объявления переменных (в том числе инициализации) могут стоять после левой фи­ гурной скобки, открывающей любой составной оператор, а не только после такой, которая открывает тело функции. Переменные, объявленные таким образом, блокируют действие любых переменных с теми же именами, объявленных во внешних блоках, и ос­ таются в силе, пока не встретится соответствующая (закрывающая) правая скобка. Рас­ смотрим пример: if (n > О ) { int i / * объявляе т с я новая переменная i * / for ( i

О; i


= r . pt l . x && р . х && р . у > = r . pt l . y && р . у


элемент - с труктуры

Знак этой операции состоит из знаков "минус" и "больше" без пробела между ними. Поэтому вместо приведенного выше примера можно записать так: print f ( " origin is ( %d , % d ) \n " , рр - >х , рр - > у ) ; Как точка, так и знак - > анализируются при компиляции слева направо. Пусть объяв­ лены следующие структура и указатель: s t ruct rect r , * rp = &r ; Тогда следующие четыре выражения эквивалентны: r . pt l . x rp - >pt l . x ( r . pt l ) . х ( rp - >pt l ) . х Операции обращения к структурам (точка и - > ) наряду с круглыми скобками для вы­ зовов функций и квадратными для индексов массивов находятся на самой вершине ие­ рархии приоритетов, а потому применяются к своим аргументам всегда в первую оче­ редь. Пусть имеется объявление: s t ruct { int l en ; char * s t r ; *р ;

Тогда выражение + + р - > l e n инкрементирует l e n, а не р. По умолчанию скобки в нем расставляются следующим образом: + + ( р - > l e n ) . С помощью скобок можно изме­ нить порядок применения операций: выражение ( + + р ) - > l e n инкрементирует р до об­ ращения к l en, а ( р+ + ) - > l e n инкрементирует его после обращения. (В последнем случае скобки не обязательны.) Аналогично, выражение * p - > s t r дает значение, на которое указывает s t r; * р - > s t r + + инкрементирует s t r после обращения к тому, на что указывает это поле (как и * s + + ); ( *p - > s t r ) + + инкрементирует то, на что указывает s t r ; наконец, * p + + - > s t r инкрементирует р после обращения к данным, на которые указывает s t r. СТРУКТУРЫ

1 43

6 . 3 . М ассивы ст руктур Напишем программу, подсчитывающую, сколько раз встречается в исходных данных каждое ключевое слово языка С. Для этого понадобится массив символьных строк, со­ держащих эти слова, и массив целых чисел для результатов подсчета. Один из возмож­ ных подходов - организовать два параллельных массива keyword и k e yc ount: char * keyword [NKEYS ] ; int keycount [NKEYS ] ; Но сам факт, что эти массивы параллельны и взаимосвязаны, наводит на мысль о друтой организации данных - массиве структур. Каждое ключевое слово и количество этих слов в тексте образуют взаимосвязанную пару: char * word ; int count ; Таких пар много, и их можно организовать в массив. Следующее объявление создает структурный тип key, определяет массив keytab структур этого типа и выделяет для них место в памяти: s t ruct key { char *word ; int count ; keytab [NKEYS ] ; Каждый элемент этого массива является структурой. Это объявление можно записать еще и таким образом : s t ruct key { char * word ; int count ;

};

s t ruct key keytab [NKEYS ] ; Поскольку массив структур keytab содержит постоянный набор имен, его удобно сделать внешней переменной и инициализировать раз и навсегда при определении. Ини­ циализация массива структур аналогична изученной ранее: после определения ставится список инициализирующих значений в фигурных скобках. s t ruct key { char *word ; int count ; keyt ab [ ] { " auto " , О , " break " , О , 11 cas e 11 , О , " char " , О , " const " , О , " cont inue " , О , 11 de f au l t 11 , о , /* . . . */ " uns igned " , О , " voi d " , О , " vo l at i l e " , О , " whi l e " , О , =

};

1 44

ГЛАВА 6

Инициализирующие значения перечислены парами соответственно порядку полей в структурах. Более строго было бы также заключить инициализаторы для каждой "строки" или структуры в фигурные скобки: { " auto " , О } , { " break " , О } , { " case " , О } , Однако в этих внутренних скобках нет никакой необходимости, поскольку инициали­ зирующие значения - простые числовые константы или текстовые строки, причем при­ сутствуют все из них. Как обычно, количество элементов в массиве keytab вычисляется автоматически по количеству инициализирующих значений, поэтому скобки [ ] после keyt ab можно оставить пустыми. Программа подсчета ключевых слов начинается с определения массива keytab. Главная программа считывает исходные данные, вызывая функцию g e t word, задача ко­ торой - доставлять из потока по одному слову за раз. Каждое слово разыскивается в таблице keytab с помощью варианта функции двоичного поиска, разработанной в гла­ ве 3 . Список ключевых слов должен быть отсортирован внутри таблицы в порядке воз­ растания. # inc lude < stdio . h> # inc lude < ctype . h> # inc lude < S t r ing . h>

#de f ine МAXWORD 1 0 0 int getword ( char * , int ) ; int b insearch ( char * , s t ruct key *

i nt ) ;

/ * подсчет ключе вых слов я зыка С * / ma in ( ) { int n ; char word [МAXWORD ] ;

whi l e ( getword ( word , МAXWORD ) ! = EOF ) i f ( i salpha ( word [ O ] ) ) i f ( ( n = b i nsearch ( word , keytab , NKEYS ) ) > = О ) keytaЬ [n] . count + + ; for ( n = О ; n < NKEYS ; n+ + ) i f ( keytaЬ [n] . count > О ) print f ( " %4 d % s \ n " , keytab [ n ] . count , keytaЬ [ n ] . word ) ; return О ; поиск слов а среди t ab [ O ] . . . t ab [ n - 1 ] * / int bins earch ( char *word , s t ruct key t ab [ ] , int n ) { int cond ; int l ow , high , mid ;

/ * b i nsearch :

low = О ; СТРУКТУРЫ

1 45

high = n

-

1;

( l ow < = high ) { mid = ( l ow+high ) / 2 ;

wh i l e

i f ( ( cond = s t rcmp ( word , t ab [mid] . word ) ) < О ) high = mid 1; e l s e i f ( cond > О ) l ow = mid + 1 ; -

else

return mid ; return - 1 ; Несколько позже будет продемонстрирована и функция g e tword; пока же достаточ­ но сказать, что при каждом вызове g e t word считывается слово, которое помещается в массив с тем же именем, что и ее первый аргумент. Величина NKEYS обозначает количество ключевых слов в таблице keytab. Хотя их можно было бы подсчитать вручную, намного легче и безопаснее сделать это автомати­ чески, особенно если список может подвергнуться изменениям. Один из возможных ва­ риантов - завершить список инициализирующих значений нулевым указателем, а затем пройти таблицу k e y t ab в цикле, производя подсчет, пока не встретится конец. Но это, собственно, намного больше, чем реально требуется, поскольку размер мас­ сива полностью определен уже на этапе компиляции. Размер всего массива равен длине одного элемента, умноженной на количество элементов, поэтому количество элементов соответственно составляет: ра змер keytab / ра змер s t ruct key В языке С есть одноместная операция s i z e o f , выполняемая при компиляции с по­ мощью которой можно вычислить размер любого объекта. Следующие два выражения дают в результате целое число, равное размеру заданного объекта или типа в байтах: s i zeof объект s i z e o f ( имя типа ) Строго говоря, операция s i z e o f дает целое значение без знака, тип которого назы­ вается s i z e t и определен в заголовочном файле < s t dde f . h > . Объектом операции может быть переменная, массив или структура. Имя типа может быть именем базового типа наподобие i n t или douЫ e либо производного сложного типа, например структу­ ры или указателя. В нашем случае количество ключевых слов равно размеру массива, деленному на размер одного элемента. Это равенство используется в директиве # de f i ne для опреде­ ления величины NKEYS: #de f ine NKEYS ( s i zeof keytab / s i zeof ( s t ruct key ) ) _

А вот еще один способ, основанный на делении длины массива на длину его же пер­ вого элемента: #de f ine NKEYS ( s i zeof keytab / s i z eof keytab [ O ] ) Здесь имеется то преимущество, что в записи не нужно ничего менять при изменении типа массива. Операцию s i z e o f нельзя применять в директиве # i f , поскольку препроцессор не понимает имена типов. А вот выражение в # de f ine не вычисляется препроцессором, поэтому его вполне можно там употреблять.

1 46

ГЛАВА 6

Теперь вернемся к функции g e t wo rd. Мы написали более общую функцию, чем требуется для этой программы, но она не слишком сложная. Функция getword считы­ вает следующее "слово" из потока ввода, причем словом считается либо строка букв и цифр, начинающаяся с буквы, либо одиночный символ, не являющийся символом пусто­ го пространства. Значением, возвращаемым из функции, является первый символ слова, EOF в случае конца файла или сам символ, если он не алфавитный. /* getword : считывает очередное слово или символ из потока ввода * / int getword ( char *word , int l im)

{

int с , getch ( vo i d ) ; void ungetch ( int ) ; char *w = word ; whi l e ( i s space ( c

getch ( ) ) )

i f ( с ! = EOF ) *w+ + = с ; i f ( ! i salpha ( c ) ) *W = ' \ 0 1 ; return с ; for ( ; - - l im > О ; w+ + ) i f ( ! i s alnum ( *w = get ch ( ) ) ) ungetch ( *w) ; break ;

}

*w = , \ О ' ; return word [ O ] ; Функция getword пользуется функциями g e t c h и unge t ch, разработанными в главе 4. Как только совокупность алфавитно-цифровых символов заканчивается, оказы­ вается, что функция ge tword уже зашла на один символ дальше, чем нужно. Вызов unge t c h помещает этот символ назад во входной поток для обработки следующим вы­ зовом. В getword также используются функции, объявленные в стандартном заголо­ вочном файле < c type . h > : i s space для пропуска пустого пространства, i s a lpha для распознавания букв и i s a l num для распознавания букв и цифр. 6.1 . Наша версия getword не умеет корректно обрабатывать знаки под­ черкивания, строковые константы, комментарии и управляющие строки препроцессора. Усовершенствуйте эту функцию соответствующим образом. Упражнение

6 . 4 . Указатели на структуры Чтобы продемонстрировать некоторые особенности, связанные с указателями на структуры и массивами структур, напишем программу подсчета слов еще раз, но теперь с указателями вместо индексирования массивов. Внешнее объявление keytab не подвергнется никаким изменениям, а вот функции ma in и Ь i n s earch потребуют переработки :

СТРУКТУРЫ

1 47

# include < s tdio . h> # inc lude < C type . h> # inc lude < s t ring . h> #de f ine МAXWORD 1 0 0 int getword ( char * , int ) ; s t ruct key *binsearch ( char *

s t ruct key *

/ * подсчет ключевых слов я зыка С ; ma in ( )

{

int ) ;

*/

char word [МAXWORD ] ; s t ruct key *р ; whi l e ( getword ( word , МAXWORD ) ! = EOF ) i f ( i s alpha ( word [ O ] ) ) i f ( ( p=binsearch ( word , keytab , NKEYS ) ) ! = NULL ) p - >count + + ; for ( р = keytab ; р < keyt ab + NКEYS ; р++ ) i f ( p - >count > О ) print f ( " % 4 d % s \n " , p - >count , p - >word) ; return О ;

/ * binsearch : поиск слова среди t аЬ [ О ] . . . taЬ [ n - 1 ] * / s t ruct key * b insearch ( char * word , s t ruct key * t ab , int n )

{

int cond ; s t ruct key * l ow = &tаЬ [ О ] ; s t ruct key *high = &taЬ [ n ] ; s t ruct key *mid ; whi l e ( low < high ) { mid = l ow + ( high - low ) / 2 ; i f ( ( cond = s t rcmp ( word , mid- >word ) ) < О ) high = mid ; e l s e i f ( cond > О ) l ow = mid + 1 ; e l se return mid ; return NULL ;

Здесь есть несколько примечательных моментов. Во-первых, в объявлении функции Ь i n s e arch необходимо сообщить, что она возвращает указатель на s t ruct key, а не на целое число. Этот факт указывается как в прототипе функции, так и в определении Ь i n s e arch. Если Ь i n s e arch находит слово, то она возвращает указатель на него; ес­ ли нет ТО NULL . Во-вторых, обращение к элементам keyt ab теперь выполняется по указателям. Это требует внесения существенных изменений в Ь i n s e a rch. Теперь переменные l ow и high инициализируются указателями на начало таблицы и на точку непосредственно перед ее концом. -

1 48

ГЛАВА 6

Средний элемент теперь уже нельзя вычислить так просто, как раньше, поскольку складывать указатели нельзя: mid = ( l ow+high ) / 2 / * НЕПРАВИЛЬНО * / А вот вычитать указатели можно, так как выражение h i g h - l ow дает количество элементов. В итоге указатель m i d на средний элемент между l ow и high вычисляется следующим образом : mi d low + ( high- l ow ) / 2 =

Самая важное изменение, которое необходимо внести в алгоритм, - это сделать так, чтобы в ходе его работы никогда не генерировался некорректный указатель и не пред­ принималась попытка обращения к элементу за пределами массива. Проблема состоит в том, что как &t a b [ - 1 ] , так и & t ab [ n ] находятся за пределами массива t ab. Первый из указателей категорически запрещен к употреблению, а по второму нельзя ссылаться. Тем не менее стандарт языка гарантирует, что адресная арифметика с участием первого элемента после конца массива (т.е. &tab [ n ] ) будет работать корректно. В функции ma i n записано следующее: for (р keytab ; р < keytab + NKEYS ; р+ + ) =

Если р указатель на структуру, то арифметические операции над р выполняются с учетом размера этой структуры, так что выражение р + + инкрементирует р настолько, чтобы получить следующий элемент массива структур, и очередная проверка прекраща­ ет работу цикла как раз вовремя. Не следует предполагать, что длина структуры равна сумме длин ее элементов. Раз­ личные объекты по-разному выравниваются по машинным словам, поэтому внутри структуры могут быть неименованные "дыры". Например, если тип char имеет длину 1 байт, а int - 4 байта, то следующая структура может фактически занимать в памяти восемь байт, а не пять: s t ruct { char с ; int i ; -

};

Надо отметить , что операция s i z eof всегда дает правильное значение размера объекта. И еще одно побочное замечание по форматированию текста программы. Если функ­ ция возвращает сложный тип данных наподобие указателя на структуру, ее имя часто бывает трудно различить в тексте или найти в текстовом редакторе: struct key *bins earch ( char *word , s t ruct key * t ab , int n ) Поэтому иногда используют другую форму записи: s t ruct key * binsearch ( char * word , s t ruct key * t ab , int n ) Это исключительно дело вкуса; выберите одну форму и придерживайтесь ее.

6 . 5 . Стру ктуры со ссылками на себ я Предположим, необходимо решить более сложную, чем предыдущая, задачу подсчета частоты, с которой любое слово встречается во входном потоке. Поскольку список слов заранее неизвестен, их нельзя предварительно удобно отсортировать и применить двоичСТРУКТУРЫ

1 49

ный поиск. И все-таки не хочется выполнять линейный поиск (перебор) каждого вновь поступающего слова в таблице уже имеющихся; такая программа выполнялась бы слиш­ ком долго. (Строго говоря, время выполнения зависело бы квадратично от количества поступающих слов.) Как же можно организовать слова так, чтобы эффективно справить­ ся с любым их списком? Одно из возможных решений - это в каждый момент поддерживать порядок в спи­ ске уже поступивших слов и ставить каждое новое слово в соответствующую позицию. Однако это не следует делать, сдвигая слова в линейном массиве, поскольку такая опе­ рация заняла бы много времени. Вместо этого применяется структура данных, именуе­ мая двоичным деревом. Дерево содержит один узел для каждого слова, отличающегося от всех других; каждый такой узел содержит следующую информацию: 8

указатель на текст слова;

8

частота его употребления в тексте;

8

указатель на левый дочерний узел;

8

указатель на правый дочерний узел.

Узел не может иметь больше двух дочерних узлов, но может иметь один или не иметь ни одного. Узлы должны быть организованы так, чтобы в каждом из них левое поддерево со­ держало только слова, "меньшие" по алфавиту, чем слово в узле, а правое поддерево только "большие". Ниже приведено двоичное дерево для предложения "now is the time for all good теп to соте to the aid of their party" 1 , построенное в порядке следования слов. now

/ "'the / \men of/ "'t ime for / \good party \ the /ir \to all /\ is

aid

come

Чтобы выяснить, есть ли вновь поступившее слово в дереве, начинают с корня и сравнивают новое слово с тем, которое помещено в этот корень. Если они совпадают, ответ на вопрос получен. Если новое слово меньше, чем корень дерева, следует продол­ жить поиск в левом поддереве; в противном случае - в правом поддереве. Если в вы­ бранном направлении нужного дочернего узла нет, значит, нового слова вообще нет в дереве, и пустое место, до которого мы добираемся при поиске, - это как раз место для нового слова. Этот процесс имеет рекурсивный характер, поскольку поиск с началом в любом узле включает в себя поиск по одному из его дочерних узлов. Соответственно, для вставки слов и вывода наиболее естественно будет применить рекурсивные функции.

1 В переводе "настало время всем добрым людям прийти на помощь своей стороне'' . Это предложение составил американский учитель Чарльз Э. Уэллер (Charles Е. Weller) как упражнение по машинописи, по­ скольку оно содержит почти все буквы латинского алфавита. Фраза часто употребляется для тестирования различных технологий обработки текста. Примеч. ред. -

-

1 50

ГЛАВА 6

Вернемся к описанию узла. Его удобно представить в виде структуры с четырьмя компонентами: s t ruct tnode { / * узел дере в а : * / char *word ; / * указа т ель на текст сло в а * / int count ; / * ч а с т о т а употребления * / s t ruct tnode * l e f t ; / * левый дочерний узел * / s t ruct tnode * right ; / * правый дочерний узел * /

};

Рекурсивное объявление узла может показаться сомнительным, но оно вполне пра­ вильно. Структуре не разрешается иметь в качестве компонента экземпляр самой себя, но здесь объявляется указатель на tnode, а не сама структура: struct tnode * l e f t ; Иногда встречается и такая разновидность структур, ссылающихся на себя, как струк­ туры, ссылающиеся друг на друга. Вот как это делается: s t ruct t {

};

s t ruct s *р ;

/ * р указыв а е т на с труктуру s * /

s t ruct s {

};

s t ruct t * q ;

/ * q указыв а е т н а с труктуру t * /

Исходный код программы оказывается удивительно коротким (не считая двух-трех вспомогательных функций, уже написанных ранее). В основной части программы функ­ ция getword считывает слова из потока ввода, которые затем добавляются в дерево с помощью функции addt ree. # include < s tdio . h> # include < ctype . h> # inc lude < s t ring . h>

#def ine МAXWORD 1 0 0 s t ruct tnode * addt ree ( s t ruct tnode * , char * ) ; void t reeprint ( s t ruct tnode * ) ; int getword ( char * , int ) ; / * программа подсче та частоты слов * / ma in ( ) { s t ruct tnode * root ; char word [МAXWORD ] ; root = NULL ; whi l e ( getword ( word , МAXWORD ) ! = EOF ) i f ( i salpha ( word [ O ] ) ) root addt ree ( root , word ) ; t reeprint ( root ) ; return О ; =

Функция addt ree является рекурсивной. Слово поступает из функции ma i n на са­ мый верхний уровень (корень) дерева. На каждом этапе это слово сравнивается со елоСТРУКТУРЫ

151

вом, находящимся в узле, и "просачивается" вниз - в правое или левое поддерево - пу­ тем рекурсивного вызова addt ree. В конце концов либо слово совпадает с одним из уже имеющихся в дереве (в этом случае увеличивается на единицу частота его употреб­ ления), либо путь по дереву заканчивается нулевым указателем. В последнем случае не­ обходимо создать новый узел и добавить его к дереву. Когда создается новый узел, addt ree возвращает указатель на него, который фиксируется в родительском узле. s t ruct tnode * t a l loc ( vo i d ) ; char * s t rdup ( char * ) ;

/ * addt ree : добавление узла со словом w в узел р или ниже * / s t ruct tnode * addt ree ( s t ruct tnode *р , char *w ) { int cond ; / * п о ступило новое слов о * / i f ( р = = NULL ) { / * с о здае т с я новый узел * / р = tal loc ( ) ; p - >word = s t rdup ( w ) ; p - > COUПt = 1 ; p - > l e f t = p - >right = NULL ; e l s e i f ( ( cond = s t rcmp ( w , p - >word ) ) = = О ) p - >count + + ; / * пов торяюще е с я слов о * / e l s e i f ( cond < О ) / * меньшее - в левое поддере в о * / p - > l e f t = addt ree ( p - > l e f t , w ) ; else / * большее - в правое п оддере в о * / p - > right = addt ree ( p - >right , w ) ; return р ; Память для нового узла выделяется функцией t a l l oc , которая возвращает указатель на свободное пространство, достаточное для хранения узла дерева. Новое слово копиру­ ется в скрытое место функцией s t rdup. (Эти функции будут рассмотрены более под­ робно несколько позже.) Затем инициализируется счетчик слова и создаются два нуле­ вых указателя на дочерние узлы. Эта часть кода выполняется только над листьями дерева при добавлении новых узлов. Мы опустили контроль ошибок в значениях, возвращаемых из s t rdup и t a l l o c ; вообще говоря, это неразумно, и здесь мы были не правы. Функция treep r i nt выводит дерево в отсортированном порядке; в каждом узле она выводит левое поддерево (все слова, меньшие заданного), затем само слово, затем пра­ вое поддерево (все слова, большие заданного). Если вам не вполне понятен принцип это­ го рекурсивного алгоритма, "проиграйте" функцию t r e eprint на показанном выше дереве. / * t reepr int : вывод дерев а р в алфавитном порядке * / void t reeprint ( s t ruct tnode *р ) { i f ( р ! = NULL ) { t reeprint ( p - > l e f t ) ; print f ( " % 4 d % s \n " , p - >count , p - >word ) ; t reeprint ( p - > r ight ) ;

Практическое примечание: если дерево станет "несбалансированным", потому что слова поступают не в случайном порядке, время работы программы может сильно уве­ личиться. В самом худшем случае - если все слова уже упорядочены - программа вы-

1 52

ГЛАВА 6

полняет довольно затратную симуляцию линейного поиска. Есть обобщения двоичного дерева, которые не подвержены этому недостатку, но здесь мы не будем уделять им вни­ мания. Прежде чем закончить рассмотрение этого примера, сделаем отступление по поводу проблемы распределения памяти. Очевидно, было бы желательно иметь в программе од­ ну функцию распределения памяти, пусть даже ей придется обслуживать различные ви­ ды объектов. Но если одна и та же функция должна обрабатывать запросы, скажем, на указатели типа char и указатели типа s t ru c t tnode s , сразу же возникают два вопро­ са. Первый: как удовлетворить требование большинства реальных систем к выравнива­ нию объектов некоторых типов по границам машинных слов (например, целые числа часто выравниваются по четным адресам)? Второй: каким образом объявить функцию так, чтобы она могла возвращать различные типы указателей? Требования к выравниванию обычно удовлетворить нетрудно за счет некоторых из­ быточных затрат памяти. Для этого нужно сделать так, чтобы функция распределения памяти всегда возвращала указатель, удовлетворяющий всем возможным требованиям к выравниванию. Функция a l l o c из главы 5 не гарантирует никакого конкретного вырав­ нивания, поэтому возьмем для наших целей стандартную библиотечную функцию ma l l oc , которая это делает. В главе 8 будет показан один из способов реализации ma l l oc. Вопрос объявления типа для функции наподобие ma l l oc не так уж прост в любом языке, который серьезно относится к контролю соответствия типов. В языке С правильно будет объявить, что функция ma l l oc возвращает указатель на vo i d, а затем явно при­ вести полученный указатель к нужному типу. Функция ma l l oc и ее аналоги объявлены в заголовочном файле < S tdl ib . h > . Таким образом, функцию t a l l o c можно записать в следующем виде: # include < s tdl ib . h>

/ * t a l l oc : создание узла дерев а типа tnode * / s t ruct tnode * tal loc ( void)

{

}

return ( s t ruct tnode * ) mal loc ( s i z eof ( s t ruct tnode ) ) ;

Функция s t rdup просто копирует свой строковый аргумент в надежное место памя­ ти, выделенное функцией ma l l o c : char * s trdup ( char * s ) / * с оздание дубликата строки s * /

{

char *р ; р = ( char * ) mal loc ( s t r l en ( s ) + l ) ; i f ( р ! = NULL ) s t rcpy ( p , s ) ; return р ;

/ * + 1 для

'

\0

'

*/

Функция ma l l oc возвращает NULL, если она не может выделить участок памяти; в таком случае s t rdup передает это значение дальше, возлагая обработку ошибки на вы­ зь1вающую функцию. Память, выделенную вызовом ma l l oc, можно освободить для последующего по­ вторного использования вызовом функции f ree (см. подробнее главы 7 и 8). СТРУКТУРЫ

1 53

6.2. Напишите программу, которая бы считывала текст программы на С и выводила в алфавитном порядке каждую группу имен переменных, которые совпадают по первым шести символам, но могут отличаться дальше. Не принимайте во внимание слова внутри строк и комментариев. Сделайте так, чтобы число 6 можно было вводить как параметр командной строки.

Упражнение

Упражнение 6.3. Напишите программу анализа перекрестных ссылок, которая бы выво­ дила список всех слов документа, а для каждого слова - список номеров строк, в кото­ рых оно встречается. Удалите несушественные слова наподобие артиклей (в английском тексте), союзов, частиц и т.п.

6.4. Напишите программу, выводящую все различные слова из ее входного потока, отсортированные в порядке убывания частоты употребления. Перед каждым словом выведите его частоту. Упражнение

6 . 6 . П оиск по табли це В этом разделе будет написана содержательная часть программного пакета для поис­ ка информации в таблицах, с помощью которого мы проиллюстрируем различные аспек­ ты работы со структурами. Этот код достаточно типичен для функций работы с таблица­ ми символов, встречающихся в макропроцессорах и компиляторах. Для примера рас­ смотрим директиву #de f ine: #def ine IN 1 Как только в тексте программы встречается такая директива, имя IN и его подстанов­ ка 1 записываются в таблицу. Позже, всякий раз, когда имя IN встретится в тексте, его следует заменить на 1 , например: s t a t e = IN ; Всю работу с именами и текстами подстановки выполняют две функции. Первая, i n s t a l l ( s , t ) , записывает имя s и текст подстановки t в таблицу; s и t простые текстовые строки. Функция l ookup ( s ) разыскивает строку s в таблице и возвращает указатель на место, в котором она ее нашла, или NULL, если не нашла. Алгоритм основан на поиске в хэш-таблице: поступающее имя конвертируется в не­ большое неотрицательное число, которое используется как индекс в массиве указателей. Каждый элемент массива указывает на начало связанного списка блоков, описывающих имена, которые соответствуют данному хэш-коду. Если какому-либо хэш-коду не соот­ ветствуют никакие имена, возвращается NULL. -

Имя

о

Подстановка

о

о

Имя

Подстановка

Блок в этом списке представляет собой структуру, содержащую указатели на имя, на подставляемый вместо него текст и на следующий блок в списке. Если указатель на сле­ дующий блок равен NULL, это соответствует концу списка.

1 54

ГЛАВА 6

struct nl i s t { / * запись таблицы : * / struct nl i s t * next ; / * следующая запис ь в цепочке * / char * name ; / * имя в #de f ine * / char * defn ; / * подставляемый текст * /

};

Массив указателей объявляется просто: #de f ine НASHS I ZE 1 0 1

stat i c s t ruct n l i s t * hasht ab [НASHS I ZE ] ; / * таблица указателей * / Хэш-функция, используемая функциями l ookup и i n s ta l l , добавляет очередной символ из строки к замысловатой шифрованной комбинации предыдущих символов, по­ сле чего возвращает остаток от деления на размер массива. Это не самая лучшая хэш­ функция из возможных, но зато она короткая и быстрая. / * hash : формирование хэш- кода для с троки s * / uns igned hash ( char * s )

{

uns igned hashva l ; for ( hashval = О ; * s ! = ' \ 0 ' ; s + + ) hashval * s + 3 1 * hashva l ; return hashval % НASHS I ZE ; =

Использование беззнакового типа гарантирует, что хэш-код никогда не будет отрица­ тельным. Процедура хэш-кодирования генерирует начальный индекс в массиве ha shtab. Если строка вообще есть в таблице, она непременно окажется в списке блоков, начинающихся с указанного элемента. Поиск выполняется функцией l ookup. Если l ookup находит уже имеющийся в таблице элемент, она возвращает указатель на него; если не находит, возвращает NULL. /* lookup : поиск элемента s в таблице hashtab * / s t ruct nl i s t * lookup ( char * s )

{

s t ruct nl i s t *np ; ! = NULL ; np О) найден * / не найден * /

for ( np = hashtab [hash ( s ) ] ; np i f ( s t rcmp ( s , np - >name ) return np ; / * элемент return NULL ; / * элемент

np - >next )

Цикл for в функции lookup - это стандартная идиоматическая конструкция для перебора элементов связанного списка: for ( p t r = head ; prt ! = NULL ; prt prt - >next ) =

Функция i ns t a l l определяет с помощью l ookup, не существует ли уже в таблице добавляемое в нее имя; если это так, новое определение заменит старое. Если имени еще не существует, создается новая запись. Функция i n s t a l l возвращает указатель NULL, если по какой-либо причине недостаточно места для новой записи. s t ruct nl i s t * lookup ( char * ) ; char * s t rdup ( char * ) ; СТРУКТУРЫ

1 55

/ * iпs t a l l : помещает запись " имя +определение " ( пате , de f п ) в таблицу hashtab */ s t ruct п l i s t * iпsta l l ( char * пате , char *defп)

{

s t ruct п l i s t *пр ; uпs igпed hashva l ; i f ( ( пр = l ookup ( пame ) ) = = NULL ) { / * имя не найдено * / пр = ( s t ruct п l i s t * ) ma l l oc ( s i zeof ( * пp ) ) ; i f ( пр = = NULL 1 1 ( пр - >паmе = s t rdup ( пame ) ) = = NULL ) returп NULL ; hashva l = hash ( пame ) ; пр - >пехt = hashtab [ hashva l ] ; hashtaЬ [ hashval ] = пр ; else / * уже е с т ь в таблице * / free ( ( void * ) пр - >dе fп ) ; / * удаление старого определения * / i f ( ( пр - >dеfп = s t rdup ( de f п ) ) = = NULL ) returп NULL ; returп пр ;

У пражнение 6.5 . Напишите функцию unde f , удаляющую имя и его определение из таблицы, обслуживаемой функциями l ook up и i ns t a l l .

Напишите простую версию обработчика директивы # de f ine (без ар­ гументов), корректно работающую с текстами программ на С. В качестве основы ис­ пользуйте функции этого раздела. Полезными могут оказаться также функции getch и unget ch. У п ражнение 6.6.

6 . 7 . Определ ение новы х типов В языке С есть такое специальное средство для определения имен новых типов дан­ ных, как оператор typede f . Например, чтобы ввести синоним типа int под названием Leng t h, следует записать: typedef iпt Leпgth ; После этого имя Length можно использовать в объявлениях, приведениях типов и т.п. точно так же, как имя самого типа int : Leпgth l еп , maxl eп ; Leпgth * l eпgths [ ] ; Аналогично, следующее объявление делает имя S t r i ng синонимом указателя на символьные данные ( char * ): typede f char * S t riпg ; Теперь можно выполнять объявление и приведение типов переменных с его исполь­ зованием: St riпg р , l i пeptr [МAXLINES ] , a l loc ( iпt ) ; iпt s t rcmp ( St riпg , St riпg ) ; р = ( Striпg ) mal loc ( l O O ) ;

1 56

ГЛАВА 6

Обратите внимание, что имя нового типа, объявляемое в typede f , стоит не сразу после ключевого слова, а на месте имени переменной. Синтаксически ключевое слово t ypede f можно считать аналогом идентификатора класса памяти - например, e xt e rn, s t a t i c и т.п. Новые типы, определяемые с помощью typede f , начинаются с прописной буквы, чтобы можно было их легко различить. Возьмем более сложный пример. Можно определить новые типы для дерева и его от­ дельного узла, описанных ранее в этой главе: typede f s t ruct tnode *Treept r ;

/ * узел дерева : * / typede f s t ruct tnode char *word ; / * указатель н а текст слов а * / int count ; / * ч а с т о т а употребления * / s t ruct tnode * l e f t ; / * левый дочерний узел * / / * правый дочерний узел * / s t ruct tnode * r ight ; Treenode ; Таким путем создаются два новых ключевых слова для типов данных. Один тип на­ зывается Treenode (это структура), а второй - Treep t r (указатель на структуру) . Функция t a l l o c в этом случае приобретет вид. Treeptr tal loc ( void)

{

return ( Treept r ) mal loc ( s i zeof ( Treenode ) ) ;

Следует подчеркнуть, что объявление typede f , собственно говоря, не создает со­ вершенно нового типа; с его помощью определяется новое имя для некоторого уже су­ ществующего типа данных. Нет в нем и никаких смысловых тонкостей: переменные, объявленные таким образом, имеют точно такие же свойства, как и переменные, тип ко­ торых выписан явно, без "псевдонима". Фактически оператор typede f очень напоми­ нает директиву #de f i ne с тем исключением, что, поскольку он анализируется компиля­ тором, он может допускать такие текстовые подстановки, которые препроцессору не по силам. Например : typede f int ( * PFI ) ( char * , char * ) ; Здесь определяется тип P F I - "указатель на функцию от двух аргументов типа char * , возвращающую i nt". Этот тип можно использовать, например, в таком кон­ тексте (см. программу сортировки в главе 5): PFI s t rcmp , numcmp ; Для применения оператора typede f есть две основные побудительные причины, по­ мимо стилевых и вкусовых предпочтений. Первая - это возможность параметризировать программу с целью улучшения переносимости. Если используемые в программе типы дан­ ных могут зависеть от системы и аппаратной конфигурации компьютера, их можно опре­ делить через typede f , а затем при переносе заменять только эти определения. Так, часто с помощью typede f определяются условные имена для целочисленных типов, чтобы уже в конкретной системе подставить short, int или l ong. В качестве примера можно привес­ ти такие типы из стандартной библиотеки, как s i z e_t и pt rdi f f_t . Вторая причина, побуждающая применять typed e f , - это возможность улучшить удобочитаемость, сделать программу самодокументированной. Название типа Treept r может оказаться более информативным, чем просто указатель на какую-то сложную структуру. СТРУКТУРЫ

1 57

6 . 8. О бъ един е ния Объединение - это переменная, которая может содержать объекты различных типов и размеров (но не одновременно); при этом удовлетворение требований к размеру и вы­ равниванию возлагается на компилятор. С помощью объединений можно работать с данными различных типов в пределах одного участка памяти, не привнося в программу элементы низкоуровневого, машинно-зависимого программирования. Объединения ана­ логичны записям с вариантами в языке Pascal. Рассмотрим пример, который мог бы встретиться в модуле управления таблицей сим­ волов какого-нибудь компилятора. Предположим, что константы в компилируемой про­ грамме могут иметь тип int, f l oat или указатель на char. Значение конкретной кон­ станты должно храниться в переменной соответствующего типа. В то же время для веде­ ния таблицы символов удобно, чтобы такие значения занимали одинаковый объем памяти и хранились в одном месте независимо от типа. Именно для этого и существуют объединения - переменные, способные хранить данные любого из нескольких типов. Их синтаксис выглядит следующим образом : uni on u_tag { int ival ; f l oat fval ; char * sval ; и;

Переменная и будет иметь достаточную длину, чтобы содержать данные самого длинного из трех типов; конкретный размер зависит от системы и реализации. Перемен­ ной и можно присваивать данные любого типа, а затем использовать их в выражениях (строго по правилам работы с конкретным типом) . Извлекать можно данные только того типа, какие были помещены при последнем обращении к переменной. Следить и пом­ нить, какие именно данные были помещены в объединение, - это забота программиста; если поместить значение одного типа, а извлечь его как значение другого, результат бу­ дет системно-зависимым и трудно предсказуемым. Обращение к элементам объединения выполняется так же, как к элементам структур, одним из следующих способов: имя - объ единения . элемент ука за тель - на - объ единение- > элемент

Пусть в переменной utype хранится информация о типе данных, находящихся в те­ кущий момент в объединении. Тогда можно представить себе такой код: i f ( utype INT ) print f ( " %d\n " , u . ival ) ; e l s e i f ( utype == FLOAT ) print f ( " % f \ n " , u . fval ) ; e l s e i f ( utype STRING ) print f ( " % s \ n " , u . sval ) ; else print f ( " bad type %d in utype \ n " , utype ) ; ==

= =

Объединения могут употребляться в структурах и массивах, и наоборот. Способ об­ ращения к члену объединения в структуре (или к члену структуры в объединении) пол­ ностью идентичен обращению к элементу вложенной структуры. Пусть, например, опре­ делен следующий массив структур:

1 58

ГЛАВА 6

s t ruct { char *name ; int f l ags ; int utype ; union { int ival ; f l oat fva l ; char * sval ; } u; symtab [NSYM] ; Тогда к элементу i va l нужно обращаться так: symtaЬ [ i ] . u . ival ; А к первому символу строки sval обращение выполняется одним из следующих способов: * symt aЬ [ i ] . u . sva l symtaЬ [ i ] . u . sval [ O ] Фактически объединение является структурой, в которой все элементы имеют нуле­ вое смещение от ее начала, сама она имеет достаточную длину, чтобы в нее поместился самый длинный элемент, и при этом выравнивание выполняется правильно для всех ти­ пов данных в объединении. Над объединениями разрешено выполнять те же операции, что и над структурами: присваивать или копировать как единое целое, брать адрес и об­ ращаться к отдельным элементам. Объединение можно инициализировать только данными того типа, который имеет его первый элемент. Таким образом, описанное выше объединение и можно инициализиро­ вать только целыми числами. В программе распределения памяти в главе 8 будет продемонстрировано, как можно использовать объединение для принудительного выравнивания переменной по границе участка памяти.

6 . 9 . Битовые поля Если место в памяти - н а вес золота и каждый байт на счету, приходится упаковы­ вать несколько объектов в одно машинное слово. Характерный пример - набор одноби­ товых сигнальных флажков-переключателей в таких программах, как компиляторы с их таблицами символов. Возможность обращаться к отдельным фрагментам машинного слова также бывает необходима для работы с заданными извне форматами данных, та­ кими как интерфейсы аппаратных устройств. Представьте себе фрагмент компилятора, управляющий таблицей символов. С каж­ дым идентификатором в программе ассоциируется определенная информация - напри­ мер, является он ключевым словом или нет, является ли он внешним и/или статическим и т.п. Самый компактный способ закодировать такую информацию - это поместить ее в виде однобитовых переключателей в одну переменную типа char или int . Обычный способ сделать это - определить набор "масок", соответствующих бито­ вым позициям в переменной. Например:

СТРУКТУРЫ

1 59

#de f ine KEYWORD 0 1 #de f ine EXTERNAL 0 2 #de f i ne STAT I C 04 Это можно сделать и в другой форме: enum { KEYWORD = 0 1 , EXTERNAL = 0 2 , STAT I C

=

04 } ; Здесь числовые константы должны быть степенями двойки. После этого обращение к отдельным битам сводится к сдвигу, применению маски и взятию дополнения. Все эти операции описаны в главе 2 Некоторые операции встречаются так часто, что превратились в устойчивые конст­ рукции (идиомы). Вот как включаются (устанавливаются равными единице) биты при­ знаков EXTERNAL и S TAT I C в переменной f l ag s : f l ags 1 = EXTERNAL 1 STAT I C ; А отключаются они (устанавливаются равными нулю) таким образом : f l ags &= - ( EXTERNAL 1 STATI C ) ; Следующее выражение истинно, если оба бита отключены: if ( ( f l ags & ( EXTERNAL 1 STATI C ) ) = = О ) Эти идиомы осваиваются вполне легко; тем не менее им есть и альтернатива. В языке С имеется возможность определения полей внутри машинного слова непосредственно, а не с помощью поразрядных операций. Внутри системно-зависимой единицы памяти, ко­ торую мы будем называть "словом", можно задать битовое поле (blt-field) - совокуп­ ность идуших подряд битов. Синтаксис определения и использования полей основан на структурах. Например, приведенный набор директив #de f ine для таблицы символов можно было бы заменить одним определением трех полей: s t ruct { 1 ,· uns i gned int i s_keyword 1; uns i gned int is extern uns igned int is stat i c 1; f l ags ; Здесь определяется переменная f l ag s , содержащая три однобитовых поля. Число после двоеточия задает ширину поля в битах. Поля объявлены как uns i gned int, что­ бы гарантированно быть величинами без знака. Обращение к отдельным полям выполняется так же, как в структурах: f l ag s . i s_keywor d , f l ag s . i s_ext e rn и т.п. По своим свойствам поля являются небольшими целыми числами и могут употребляться в выражениях в этом качестве. По­ этому предыдущие примеры можно переписать в более естественном виде. Вот пример включения битов (установки их в единицу) : f l ags . i s_exte rn = f l ags . i s_s t a t i c = 1 ; А это отключение битов (установка в нуль) : f l ags . i s_ext ern = f l ags . i s_s tat i c = О ; Так выполняется анализ значений: if ( f l ags . i s_ext e rn = = О && f l ags . i s_s tat i c

==

О)

Практически все, что связано с битовыми полями, является системно-зависимым. На­ пример, только в конкретной реализации определяется, могут ли поля перекрывать гра­ ницы слов. Поля не обязаны иметь имена; безымянные поля (двоеточия с размером по-

1 60

ГЛА ВА 6

еле них) часто используются для пропуска и резервирования отдельных битов. Для при­ нудительного выравнивания по границе следующего слова можно использовать специ­ альное значение длины поля, равное О. В некоторых системах поля выстраиваются слева направо, а в других - справа нале­ во. Это означает, что хотя с помощью полей удобно работать с внугренними структура­ ми данных, в случае обработки внешних структур следует тщательно подходить к вопро­ су о том, с какого конца нач инать. Программы, зависимые от такого порядка, не перено­ симы между системами. Поля можно объявлять только с типом int ; с целью переносимости лучше явно указывать модификатор s i gned или un s igned. Совокуп­ ность полей - не массив, и у них нет адресов, поэтому операция & к ним неприменима.

СТРУКТУРЫ

161

Гл ава 7

В вод - в ы вод Средства ввода-вывода не являются частью собственно языка С, поэтому до сих пор наше изложение касалось их только вскользь. А ведь программы могут взаимодейство­ вать со своим системным окружением гораздо более сложными способами, чем было показано ранее. В этой главе рассматривается стандартная библиотека - набор функций для ввода и вывода данных, обработки строк, управления памятью, математических вы­ числений и других операций, необходимых в программах на С. Из всей стандартной библиотеки мы сосредоточимся в основном на функциях ввода-вывода. В стандарте ANSI все библиотечные функции определены совершенно точно, так что они сушествуют в эквивалентной форме в любой системе, поддерживающей язык С. Программы, в которых взаимодействие с системой осушествляется исключительно с по­ мощью средств из стандартной библиотеки, можно переносить из одной среды в другую безо всяких изменений. Объявления библиотечных функций распределены по добрым полутора десяткам за­ головочных файлов. Некоторые из них уже упоминались - это < s tdio . h > , < s t r ing . h >, < C type . h > . Вся библиотека здесь рассматриваться н е будет, поскольку нам интереснее писать свои программы на С, чем изучать ее. Подробное описание биб­ лиотеки приведено в приложении Б.

7. 1 . Стандартны е с р едства ввода-вывода Как было сказано в главе 1 , библиотека реализует простую модель текстового ввода и вывода. Текстовый поток состоит из последовательности строк; каждая строка оканчива­ ется специальным символом конца строки. Если система работает не совсем таким обра­ зом, библиотека делает все необходимое, чтобы программе казалось именно так. Напри­ мер, средства стандартной библиотеки преобразуют символы возврата каретки и перево­ да строки в единый символ конца строки при вводе и выполняют обратное преобразование при выводе. Простейший механизм ввода состоит в том, чтобы считывать по одному символу за раз из стандартного потока ввода (standard input), обычно представляемого клавиату­ рой, с помощью функции get char: int get char ( void) Функция get char при каждом ее вызове возвращает следующий символ из потока или EOF в случае конца файла. Символическая константа EOF определена в < S t d i o . h > . Обычно е е значение равно - 1 , н о в выражениях следует писать EOF, чтобы н е зависеть от конкретного значения.

Во многих средах можно заменить стандартный поток ввода с клавиатуры на файл, используя символ < для перенаправления ввода. Пусть, например, программа prog пользуется функцией get char для ввода. Тогда следующая команда заставит prog считывать символы из файла inf i l е, а не с клавиатуры: p rog < in f i l e Переключение ввода произойдет таким образом, что и сама программа prog никакой разницы не заметит. Например, строка " < i nf i l e " не будет включена в число аргумен­ тов командной строки программы в массиве argv. Переключение ввода незаметно так­ же и в том случае, если входные данные поступают из другой программ ы через конвейер (pipe) . В некоторых системах можно задать такую команду: otherprog 1 prog В результате будут запущены на вы полнение две программы, otherprog и prog, причем стандартный поток вывода програм мы othe rp rog будет перенаправлен в стан­ дартный поток ввода p rog. Ниже приведена простейшая функция вывода. int put char ( int ) Оператор put c h a r ( с ) помещает символ с в стандартный поток вывода (standard output), который по умолчанию соответствует экрану монитора. Функция put char воз­ вращает выведенный символ или EOF в случае ошибки. И в этом случае поток можно перенаправить в файл с помощью директивы > имя_ ф а йла . Если программа prog поль­ зуется для вывода функцией putchar, то следующая команда перенаправит ее вывод в файл ou t f i l e : prog >Out f i l e Если в системе поддерживаются конвейеры, можно перенаправить поток вывода про­ граммы p rog в поток ввода программы ano the rprog: p rog

1

ano t h e rprog

Данные, вы водимые функцией p r i nt f , также поступают в стандартный поток выво­ да. Вызовы put c ha r и p r i n t f можно чередовать как угодно - данные будут появ­ ляться в потоке в том порядке, в каком делаются вызовы функций. Каждый файл исходного кода, в котором выполняется обращение к библиотечным функциям ввода-вывода, должен содержать следующую строку до первого вызова таких функций: # inc lude < S td i o . h > Если имя файла заключено в угловые скобки ( < > ) , заголовочный файл разыскивается в определенном стандартном месте (например, в системах U n i x это каталог / u s r / inc l ude). Многие программы считывают данные из одного потока ввода и записывают резуль­ таты также в один поток: для таких программ впол не достаточно ввода-вы вода с помо­ щью g e t c har, put c har и pr int f - по крайней мере, на первых порах . В частности, это справедливо, если используется перенаправление данных с выхода одной программы на вход другой. Для примера рассмотрим программу l ower, которая переводит посту­ пающие данные в нижний регистр : # inc lude < s t d i o . h > # i n c l ude < C t yp e . h >

164

ГЛАВА 7

/*

ma i n ( )

{

int

l o we r :

п е р е в о д в ых о д ных д а н ных в н ижний р е г и с т р * /

С ;

wh i l e ( ( с = g e t c h a r ( ) ) ! pu t c h a r ( t o l ow e r ( c ) ) ; r e t urn О ;

=

EOF )

Фун кция t o l ow e r определена в файле < c t yp e . h > ; она переводит букву из верхне­ го регистра в нижний, а любой другой сим вол оставляет в исходном виде . Как уже упо­ миналось, "функции" наподобие g e t c ha r и put cha r из < S t d i o . h> и t o l ower из < C t yp e . h> часто я вляются макросам и, благодаря чему удается избежать излишних за­ трат на вызов функций для каждого символа. В разделе 8.5 будет показано, как это дела­ ется. Независимо от того, как функции из файла < c t yp e . h> реализованы в конкретной системе, програм мы с их использованием благодаря им защищены от особенностей того или иного символьного набора. У пражн е н и е 7.1 . Напишите программу для перевода входн ых данных из верхнего реги­ стра в нижний или наоборот в зависи мости от того, буквами какого регистра набрано имя программы при ее запуске (это имя помещается в a rgv [ О ] ) .

7 2 Ф ор ма т и ро в а нн ы й в ы в од и функция p r i n t f .

.

Фун кция вывода p r i n t f преобразует данные из внутренних форматов в выводимые символы . В предыдущих главах она широко использовалась без формального определе­ ния . Здесь рассматриваются самые типичные ее применения, хотя и не дается полное оп­ ределение; за полными сведениями обращайтесь к приложению Б. i n t p r i n t f ( c h a r * f o rmat ,

аргl ,

арг2 ,

. . . )

Функция p r i n t f преобразует, форматирует и выводит свои аргументы в стандарт­ ный поток вы вода согласно строке формата f o rma t . Она возвращает количество вы­ веденных символов. Строка формата содержит два типа объектов: обычные символы , которые копируют­ ся в поток вывода, и специфи кации формата, каждая из которых вызывает преобразова­ ние и вывод очередного аргумента функции p r i n t f . Каждая спецификация формата на­ чинается с символа % и заканчивается сим волом типа преобразования. Между % и симво­ лом типа могут стоять по порядку следующие элементы . -



Знак "минус", задающий выравнивание выводимого аргумента по левому краю отведенного поля вывода.



Число, задающее мини мальную ширину поля . Преобразованный аргумент выво­ дится в поле, ширина которого не меньше, чем заданная . Если необходимо, поле дополняется пробелами слева (или справа, если это задано) до указан ной длины.



Точка, отделяющая ширину поля от точности представления.

Ввод- вывод

165



Число (точность представления), задающее максимальное количество символов при выводе строки, или количество цифр в вещественном числе после десятичной точки, или минимальное количество цифр для целого числа.



Буква h, если целое число следует вывести как короткое ( sho rt), или буква l, ес­ ли как длинное ( l ong).

Символы преобразования приведены в табл. 7 . 1 . Если после знака % стоит не специ­ фикация формата, а что-то другое, результат будет непредсказуемым. Таблица 7. 1 . Основные спецификации форм ата функции p r i n t f Си мвол

Тип аргумента и способ вывода

d, i

int; десятичное целое число

о

int; восьмеричное целое число без знака (без нуля впереди)

х, х

int; шестнадцатеричное целое число без знака (без ох или ох в начале) с применением соответственно цифр abcde f и AВCDEF для 1 0 , . " , 1 5

u

i n t; десятичное целое число без знака

с

int; отдельный символ

s

char * ; выводятся символы из строки, пока не встретится метром точности

f

douЫ e ; [ - J умолчанию 6)

е, Е

douЫ e ; [ J т . dddddde±xx или [ - J т . ddddddE±xx, где количество цифр на месте букв d зада­ ется параметром точности (по умолчанию 6)

g, G

douЫ e ; эквивалентно %е или %Е, если показатель степени меньше 4 или больше, или равен задан­ ной точности; в противном случае эквивалентно % f . Нули и десятичная точка в конце числа не выводятся

Р

vo i d * ; указатель (точное представление зависит от конкретной системы и реализации языка)

%

преобразование аргументов не выполняется, выводится символ %

т.

'

\ о , или в количестве, заданном пара­ •

dddddd, где количество цифр на месте букв d задается параметром точности (по

-

-

,

Ширину или точность можно указать в виде символа * . В этом случае нужное значе­ ние будет вычисляться по следующему аргументу (который должен иметь тип int). На­ пример, вывод не более max символов из строки s выполняется так: pr int f ( " % . * s " , max ,

s) ;

Почти все спецификации формата уже иллюстрировались примерами в предыдущих главах. Одно из исключений состоит в применении параметра точности к строкам. Сле­ дующая таблица показывает, как работают разные спецификации при выводе строки " he l l o , wo r l d " ( 1 2 символов) . Каждое поле вывода окружено двоеточиями, чтобы было видно его длину: : %s : : %10s : : % . lOs : : % - l Os : : % . 15s : : % - 15s : : %15 . lOs : : %- 15 . lOs :

1 66

: he l l o , worl d : : he l l o , worl d : : he l l o , wor : : he l l o , wor l d : : he l l o , wor l d : : he l l o , wor l d he l l o , wor : : he l l o , wor

ГЛА ВА 7

Предупреждение: функция pr i nt f по своему первому аргументу вычисляет, сколько аргументов будет дальше и какого они типа. Если количество или типы аргументов не соответствуют строке формата, возникнет путаница, и будет выдан неверный результат. Следует также иметь в виду разницу между этими двумя вызовами: / * ОШИБКА , ЕСЛИ s СОДЕ РЖИТ % * / print f ( s ) ; print f ( " % s " , s ) ; / * БЕЗОПАСНО * / Функция spr int f выполняет те же преобразования, что и pr i nt f , но помещает ре­ зультат вывода в символьную строку: int sprint f ( char * string , char * format , аргl , арг2 , . . . ) Функция sprint f форматирует аргументы арг l , арг2 и т.д. согласно строке фор­ мата format, как и раньше, но результат помещается не в поток вывода, а в строку s t r ing. Предполагается, что s t r ing имеет достаточную для этого длину. 7.2. Напишите программу, выводящую данные, которые поступают к ней на вход, в удобном представлении. Как минимум, программа должна выводить непеча­ таемые символы в виде восьмеричных или шестнадцатеричных кодов (как привычнее пользователю) и разбивать слишком длинные строки текста. Упражнение

7 3 Списки а ргум е н тов п е р е м е нн о й дл и н ы .

.

В этом разделе приводится реализация сильно урезанной, "скелетной" версии print f , которая демонстрирует, как пишутся переносимые функции с обработкой спи­ сков аргументов переменной длины. Поскольку здесь нас в основном интересует работа со списком аргументов, функция minp r i nt f будет сама анализировать строку формата и аргументы, а для преобразования данных к выводимой форме вызывать функцию p r int f . Вот как на самом деле объявляется функция p r i nt f : int print f ( char * fmt , . . . ) Здесь конструкция . . . означает, что количество и типы аргументов могут меняться. Такая конструкция может стоять только в конце списка аргументов. Наша функция m inp r int f объявляется следующим образом : void minprint f ( char * fmt , . . . ) В отличие от print f , она не возвращает количество выведенных символов. Сложность заключается в том, как же перебрать список аргументов в функции minp r i nt f, когда у него даже нет имени. В стандартном заголовочном файле < s tdarg . h> на этот случай имеется набор макроопределений, дающих способ перебо­ ра списка. Реализация этого заголовочного файла системно-зависима, однако интерфейс всегда единообразный. Для объявления переменной, ссылающейся по очереди на каждый аргумент, имеется тип va_l i s t . В функции minp ri nt f эта переменная имеет имя ар (сокращение от argument pointer - указатель на аргумент). Макрос va_ s t art инициализирует ар так, чтобы переменная указывала на первый безымянный аргумент. Этот макрос нужно выВвод-вывод

1 67

звать один раз перед тем, как обращаться к переменной ар. Функция должна иметь как минимум один аргумент с именем ; последний именованный аргумент используется мак­ росом va_ s t a r t для инициализации своей работы . Каждый вызов va _ a rg возвращает один аргумент и передвигает указатель ар на следующий . Чтобы определить, какого типа аргумент нужно возвращать и на сколько передвигать указатель, va _ a r g использует заданное ему имя типа. Наконец, макрос va _ end выполняет необходимые заверщающие операции. Его необходимо вызвать до возвращения из функции. Все перечисленное служит основой для нашей упрощенной версии p r i n t f :

# inc l ude < S tdarg . h > / * minprint f : о граничен н а я версия print f с о спис ком аргуме нтов переменной длины * / vo id minprint f ( cha r * fmt , . . . ) { va_l i s t ар ; / * ука з а т е л ь н а бе зым я н ные аргументы * / char *р , * sval ; int iva l ; douЫ e dva l ; va st art ( ap , fmt ) ; / * установить ар на 1 - й аргумент без имени * / for ( р = fmt ; * р ; р + + ) { 1%1 ) { i f ( *р ! putchar ( *р ) ; cont inue ; =

}

swi tch ( * + +р ) case 1 d 1 : ival = va arg ( ap , int ) ; print f ( " % � " , iva l ) ; break ; case ' f ' : dva l = va arg ( ap , douЬl e ) ; print f ( " % f " , dva l ) ; break ; case ' s ' : for ( sval = va arg ( ap , char * ) ; * sva l ; sval + + ) put char ( * sva l ) ; break ; de faul t : putchar ( * sval ) ; break ; va end ( ар ) ;

/ * з а в ершающие операции * /

7.3. Доработайте m i np r i nt f так, чтобы она реализовала больше возмож­ ностей p r i n t f .

Упражнение

1 68

ГЛА ВА 7

7 4 Ф ор ма тир о в анный вв од и функция s c an f .

.

Функция s c a n f это аналог p r i n t f для целей ввода. Она реализует м ногие анало­ гич ные преобразования формата в противоположную сторону. Вот ее объявление: -

int s c an f ( char * f o rma t ,

. . . )

Функция s c anf считы вает символы из стандартного потока ввода, интерпретирует их в соответствии со строкой-спецификацией f orma t и помещает результат в остальные аргументы. Строка формата будет рассмотрена ниже. Остальные аргументы , ка.ждый из которых дол.жен быть указателем, определяют, куда нужно поместить поступившие входн ые данные. Как и в случае p r i nt f , в этом разделе дается краткий обзор основных возможностей, а не исчерпывающее описание . Функция s c an f прекращает работу, как тол ько закончится строка формата или как только входные данные придут в противоречие с управляющей ими спецификацией. Она возвращает количество успешно прочитанных, преобразованных и помещенных в ука­ занные места элементо в входны х данных. Это значение можно ис пользовать для учета и контроля введенных данных. В конце файла возвращается EOF; обратите внимание, что это не О, который означает, что очередной введен ный символ не соответствует текущей спецификации в строке формата. При следующем вызове s c an f считывание потока продолжается со следующего символа после последнего введенного и преобразованного . Существует также функция s s c a n f , которая считы вает из строки, а не из стандарт­ ного потока ввода: i nt s s canf ( c har * s t r i ng , char * f orma t ,

аргl , арг2 ,

. . .

)

Эта функция считывает из строки s t r i ng согласно строке формата f o rmat и по­ мещает полученные результаты в аргl , арг2 . Эти аргументы должны быть указателями. Строка формата обычно содержит спецификации формата, используемые для управ­ ления преобразованием входных данных. В строке формата могут содержаться следую­ щие элементы: •

пробелы или табуля ции, которые игнорируются при вводе ;



обы кновенные сим волы (не % ) , которые должны соответствовать ожидае м ы м оче­ редным непусты м символам в потоке ввода;



спецификации формата, состоящие из сим вола % ; необязател ьного с и м вола запре­ та присваивания * ; необязательного ч исла - максимальной ширины поля ; необя­ зательного символа h, l или L , обознач ающего дл ину представления числа, и соб­ ственно символа типа/формата .

Спецификация формата регул ирует преобразование очередного элемента дан ных (поля ввода) в потоке к задан ному типу и форме. Как правило, резул ьтат помещается в переменную, на которую указывает соответствующий аргумент. Если присваивание за­ прещено символом * , то элемент данных пропускается, и никакого присваивания не происходит. П оле ввода по определению представляет собой строку непустых сим волов, закан чивающуюся либо сим волом пустого пространства, либо на заданной ширине (если таковая указана). Отсюда следует, что фун кция s c an f при вводе дан ных пропускает концы строк, поскольку они относятся к символам пустого пространства. (Символы пус-

ввод-вывод

169

того пространства - это пробел, табуляция, конец строки, возврат каретки, вертикаль­ ная табуляция, перевод страницы.) Спецификация формата задает также и смысловую интерпретацию поля ввода. Соот­ ветствующий аргумент должен быть указателем, поскольку этого требует семантика вы­ зова функций в С по значению. Символы-спецификации формата приведены в табл. 7.2. Таблица 7.2. Основные спецификации формата функции s c an f Си мвол

Входные данные и тип аргумента

d

Десятичное целое число; int *

i

Целое число; in t * . Число может записываться в восьмеричной (0 в начале) или шестнадцатеричной (ох или ох в начале) системе

о

Восьмеричное целое число (с нулем или без нуля впереди); int *

и

Десятичное целое число без знака; uns igned int *

х

Шестнадцатеричное целое число (с или без ох/ох в начале); int *

с

С имвол; char * . Очередной символ из потока ввода (по умолчанию один) помещается в указанное ме­ сто. Обычный пропуск пустого пространства отменяется; чтобы считать следующий непустой символ, ис­ пользуйте спецификацию % 1 с

s

Строка символов (без кавычек); char * , указатель на массив символов, достаточно большой для по­ мещения строки с завершающим \ о , который добавляется автоматически •



е, f, g

Вещественное число, с необязательным знаком, необязательной десятичной точкой и необязательной экспоненциальной частью; f l oat *

%

С имвол процента; присваивание не выполняется

Перед спецификациями формата d, i , о, и и х может стоять символ h, означающий, что в списке аргументов находится указатель на short, а не на int. Вместо h может также стоять буква l , которая означает, что соответствующий аргумент - указатель на данные типа l ong. Аналогично, перед спецификациями е , f и g может стоять буква l , означающая, что в списке аргументов н а соответствующем месте находится указатель на douЫ e , а не на f l oa t . В качестве первого примера перепишем примитивный калькулятор и з главы 4 , вы­ полняя ввод данных с помощью s c an f : # include < s tdio . h>

mai n ( )

{

/ * примитивный кал ь кулятор * /

douЫ e sum , v ; sum = О ; whi l e ( s canf ( " % l f " , &v ) = = 1 ) print f ( " \ t % . 2 f \n " , sum + = v ) ; return v ;

Предположим, необходимо вводить строки данных, в которых содержатся даты в следующем формате: 2 5 Dec 1 9 8 8

1 70

ГЛАВА 7

Оператор s canf для этого случая будет выглядеть так: int day , year ; char monthname [ 2 0 ] ;

scanf ( " %d % s %d " , &day , monthname , &year ) ; Знак & с именем mont hname употреблять не надо, поскольку имя массива само по себе является указателем. В строке формата s canf могут фигурировать обычные, не управляющие символы; они должны в точности соответствовать символам во входном потоке. Поэтому считы­ вать дату в формате мм/ дд/ гг из входного потока можно таким оператором : int day , month , year ;

scanf ( " %d/ % d / % d " , &month , &day , &year ) ; Функция s can f игнорирует пробелы и табуляции в строке формата. Более того, при поиске данных во входном потоке она пропускает вообще все символы пустого про­ странства (пробелы, табуляции, концы строк и т.д.). Считывание потока, формат которо­ го не фиксирован, лучше выполнять по одной строке, которую затем разбирать функцией s scanf. Например, пусть необходимо считывать строки, которые могут содержать дату в любом из приведенных выше форматов. Тогда можно записать следующее: whi l e ( get l ine ( l ine , s i zeof ( l ine ) ) > О ) { i f ( s s canf ( l ine , " %d % s % d " , &day , monthname , &year ) = = 3 ) print f ( " val i d : % s \ n " , l ine ) ; / * 2 5 Dec 1 9 8 8 * / e l s e i f ( s s canf ( l ine , " %d/ %d/ % d " , &month , &day , &year ) = = 3 ) printf ( " val i d : % s \ n " , l ine ) ; / * мм/дд / г г * / else print f ( " inval i d : % s \ n " , l ine ) ; / * неправил ь н а я с трока * / Вызовы функции scanf можно чередовать с вызовами других функций ввода. При следующем вызове любой из функций ввода считывание потока продолжится с первого символа, еще не считанного scanf. Повторяем предупреждение в последний раз: аргументы функций s c anf и s s canf должны быть указателями. Самая распространенная ошибка в этой связи - приведен­ ная ниже конструкция. scanf ( " % d " , n ) ; Правильно следует записывать так: s canf ( " % d " , &n ) ; Обычно эта ошибка не выявляется при компиляции. Упражнение 7.4. Напишите мини-реализацию minp r i n t f из предыдущего раздела.

функции

s c anf

по

аналогии

с

Перепишите постфиксный калькулятор из главы 4 так, чтобы для ввода и преобразования чисел в нужную форму использовалась функция s c anf и/или s s canf .

Упражнение 7.5.

Ввод-вывод

1 71

7. 5 . Досту п к фа й л ам До сих пор в о всех при мерах использовался ввод из стандартного потока ввода и вы­ вод в стандартный поток вывода; эти потоки определены для программы по умолчанию операционной системо й . Следующий ш а г - написать программу, которая бы обращалась к файлу данных, не подключенному к программе заранее. Одна из программ, демонстрирующих потребность в такого рода операциях, называется c a t . Она соединяет набор и менованных файлов (выполняет конкатенацию) в один массив данных и вы водит его в стандартный поток вывода. Программа c a t используется для вывода файлов на экран, а также применяется для подготовки входн ых дан ных для программ, которые сами не могут обращаться к файлам по именам . Например, следующая команда вы водит содержимое файлов х . с и у . с (и больше ничего) в стандартн ы й поток вывода : cat х . с у . с

Вопрос состоит в том , как же организовать чтение данных из файлов с именами, т.е. как подкл юч ить имена внешних файлов, задаваемые пол ьзователем, к операторам про­ грамм ы , считывающим данные. Правила для этого просты . Перед тем как читать из файла или записы вать в файл, его необходимо открыть с помощью библиотеч ной функции f open. Эта функция берет внешнее имя наподобие х . с или у . с, выполняет некоторые подготовительные операции, обмени вается запросами с операционной системой (подробности сейчас не важны) и в ито­ ге возвращает указатель, который в дальнейшем используется для чтения или записи. Этот файловый указатель указы вает на структуру, содержащую информацию о фай­ ле: местонахождение буфера, текущую символьную позицию в буфере, характер опера­ ций (чтение или запись), наличие ошибок и состояния конца файла. Пользователям не обязательно знать все детали, поскольку среди различных определений в файле < s t d i o . h> имеется объявление структуры под именем F I LE . Единственное объявле­ ние, которое требуется для файлового указателя, - это создание его экземпляра таким образом, как показано ниже. F I LE * f p ; F I LE * f op e n ( c h a r * name ,

c h a r * mode ) ;

Здесь говорится, что f p указы вает на структуру F I L E, а функция f op e n возвращает указател ь на F I L E . Обратите внимание, что F I LE - это название типа, а не метка структуры , поскол ьку оно определено с помощью оператора t ype de f . (Подробности возможной реал изации f op e n в системе Unix при ведены в разделе 8 . 5 . ) Вызов функции f op e n в программе выполняется следующим образом : fp

=

f op e n ( n ame ,

mode ) ;

Первы й аргумент f op e n - ·по символ ьная строка, содержащая имя файла. Второй аргумент - ре.жим открытия ; это также символьная строка, указывающая способ ис­ пользования файла. Допускаются режи м ы чтения 11 r 11 (от 1 1 re ad 11 ) записи 1 1 w 11 ( 11 wr i t e 1 1 ) и добавления дан ных в конец файла 11 а 1 1 ( " append 1 1 ) . В некоторых систе­ мах делается различие между текстовыми и двоич ными файлам и ; для работы с двои ч­ ными файлами следует добавить в строку режи ма букву 11 Ь 11 (от 11 Ь i n a ry 11 ) . Если файл, открываем ы й для записи ил и добавления в конец, н е существует, пред­ принимается попытка его создания . Открытие существующего файла для записи стирает ,

1 72

ГЛАВА 7

все его содержимое, а открытие для добавления - сохраняет. Попытка читать из несу­ ществующего файла я вляется ошибкой; могут быть и другие причины для ошибки - на­ пример, отсутствие допуска на чтение из данного файла. Если произошла ошибка, функ­ ция f open возвращает NULL . ( Если необходимо, произошедшую ошибку можно опре­ делить точнее; см. описание функций обработки ошибок в конце раздела 1 прило­ жения Б . ) Следующее, что необходи мо для работы, - это средства чтения и л и записи файла после его открытия. Тут на выбор имеется несколько вариантов, из которых самые про­ стые - это функции ge t c и put c . Функция ge t c возвращает очередной символ из файла; чтобы знать, из какого именно, она должна получить указатель на файл : int ge t c ( F I LE * fp )

Итак, функция ge t c возвращает очередной символ из потока, на которы й указывает f p ; при ошибке или в случае конца файла возвращается E O F . Для вывода испол ьзуется функция put c : int pu t c ( i nt

с,

F I LE * f p )

Функция put c зап исывает сим вол с в файл f p и возвращает записанный сим вол или в случае ошибки. Как и ge t c ha r/put c har, функции ge t c и put c могут факти че­ ски быть макросами . Когда программа н а С запускается н а вы полнение, операционная система автомати­ чески откры вает три файла и создает файловые указатели на них. Это стандартны й поток ввода, стандартны й поток вы вода и стандартный поток ошибок. Соответствующие указа­ тели называются s t d i n, s t dout и s t de r r ; они объя влены в файле < S t d i o . h > . Обычно s t d i n ассоци ирован с клавиатурой, а s t dout и s t d e r r с экраном мон ито­ ра, но s t di n и s t dou t можно ассоциировать с файлами или кон вейерам и (перенаправить) , как описано в разделе 7 . 1 . Функции g e t char и put char можно определить через g e t c , put c , s t d i n и s t dout следующим образо м : EOF

-

# de f i n e g e t char ( ) # de f i n e put char ( c )

ge t c ( s t d i n ) p u t c ( ( c ) , s t dout )

Для форматированного файлового ввода-вы вода можно пол ьзоваться функциями f s canf и f p r i nt f . Они аналогичны s c a n f и p r i nt f , тол ько их первым аргументом служит файловы й указател ь, а строка формата стоит на втором месте: int f s c a n f ( F I L E * fp , c h a r * f o rmat , . . . ) int f p r i n t f ( F I LE * fp , cha r * f o rmat , . . . )

Справи вшись с предварительной подготовкой, мы тепер ь готовы приступить к про­ грамме c a t , выполняющей кон катенацию файлов. Общий принцип ее устройства при­ меняется во многих програм мах : если есть аргументы командной строки, они интерпре­ тируются как имена файлов данных и обрабатываются по порядку; если нет, обрабатыва­ ется стандартн ый поток ввода. # i nc lude < S tdi o . h > / * cat : с ц е п л е н и е ф а йл о в п о п о р я д к у , ma i n ( i nt a rgc , char * a rgv [ ] )

{

F I LE * f p ; vo i d f i l e c opy ( F I LE *

ввод- вывод

версия

1

*/

F I LE * ) ;

173

i f ( argc = = 1 ) / * нет аргументов ; стандартный поток * / f i l ecopy ( stdin , s t dout ) ; e l se whi l e ( - - argc > О ) i f ( ( fp = fopen ( * + +argv , " r " ) ) = = NULL ) { print f ( " cat : can ' t open % s \ n " , * argv ) ; return 1 ; else { f i l ecopy ( fp , s t dout ) ; f c l os e ( fp ) ; return О ; / * f i l ecopy : копирование файла i fp в файл ofp * / voi d f i l e copy ( F ILE * i fp , F I LE * ofp ) { int с ; whi l e ( ( с getc ( i fp ) ) put c ( c , ofp ) ;

! = EOF )

Файловые указатели s t d i n и s t dout являются объектами типа F I LE * . Это кон­ станты, а не переменн ые, поэтому им нельзя присваивать какие-либо значения. Обратную к fop e n роль играет функция f c l o s e : int f c lose ( F ILE * fp ) Она разрывает связь между внешним именем и файловым указателем, установленную функцией f ope n, тем самым освобождая указатель для другого файла. Поскольку в большинстве операционных систем имеется ограничение на количество одновременно открытых программой файлов, полезно закрывать файлы и освобождать их указатели, как только файлы становятся ненужными. Именно это делается в программе c a t . Есть и еще одна причина применить f c l o s e к открытому файлу - в результате очищается и сбрасывается в файл буфер, в котором функция put c накапливает выходные данные. Функция f c l o s e вызывается автоматически для каждого открытого файла во время нормального завершения программы. (Можно даже закрыть s t d i n и s t dout , если они не нужны. Их можно затем переназначить библиотечной функцией f reopen.)

7 6 Обр а б отка о ш и бок . П оток s t de r r и функция .

.

ex i t

Обработка ошибок в программе c a t далека от идеальной. Беда в том, что если обра­ щение к одному из файлов по какой-то причине не удалось, соответствующая диагности­ ка выводится в конце сцепленного потока данных. Это нормально, если поток выводится на экран, но неудобно в том случае, если поток направляется в файл или в другую про­ грамму через конвейерный механизм.

1 74

ГЛАВА 7

Чтобы справиться с этой ситуацией, программе назначается второй поток вывода под именем s t derr (standard error - стандартный поток ошибок) точно таким же обра­ зом, как stdin и s tdout . Данные, выведенные в s t derr, обычно появляются на экра­ не, даже если стандартный поток вывода перенаправлен в другое место. Перепишем программу c a t так, чтобы сообщения об ошибках в ней выводились в стандартный поток ошибок. # inc lude < S tdio . h>

/ * cat : main ( int { F I LE voi d char

сцепле ние файлов по порядку , в ерсия 2 * / argc , char * argv ( ] )

* fp ; f i l ecopy ( F I LE * F I LE * ) ; *prog = argv [ O ] ; / * имя программы для сообще ний * /

i f ( argc = = 1 ) / * н е т аргумен т о в ; с т а ндартный п о т о к * / f i l ecopy ( s tdin , stdout ) ; else whi l e ( - - argc > О ) i f ( ( fp = fopen ( * + +argv , " r " ) ) = = NULL ) { fprint f ( s t derr , " % s : can ' t open % s \n " , prog , * argv ) ; exi t ( 1 ) ; else { f i l ecopy ( fp , s t dout ) ; f c l o s e ( fp ) ; i f ( f e rror ( stdout ) { fprint f ( s tderr , 11 % s : error wri t ing s t dout \ n " , prog ) ; exi t ( 2 ) ;

}

exi t ( O ) ; Программа сигнализирует об ошибках двумя способами. Во-первых, диагностика ошибок посылается функцией fprint f в поток s tderr и гарантированно появляется на экране, а не исчезает где-то в недрах файла или конвейера. В сообщения включается имя программы из argv [ О ] , чтобы при использовании данной программы параллельно с другими можно было легко определить источник ошибки. Во-вторых, в программе используется стандартная библиотечная функция exi t, пре­ кращающая работу программы. Аргумент exi t становится доступным тому процессу, который запустил программу на выполнение, так что успешность или ошибочность ее завершения может проанализировать другая программа - та, которая вызвала ее в каче­ стве подчиненного процесса. Принято, что возвращаемое значение О сигнализирует о нормальном завершении; различные ненулевые значения обычно обозначают аварийные ситуации. Функция exi t вызывает f c l o s e для закрытия каждого открытого файла, очищая тем самым буферы вывода. В программе ma i n конструкция r e turn выражение эквивалентна выражению exi t ( выражение ) Функция exi t имеет то преимушество, что ее можно вызвать из других функций и вызовы ее можно найти в тексте программой поиска по образцу напо­ добие приведенной в главе 5 . .

Ввод-вывод

1 75

Функция f e r r o r возвращает ненулевое число, если произошла ошибка в потоке f p : int

f e rror ( F I LE * f p )

Хотя ошибки при выводе очень редки, они все же случаются (например, при заполне­ нии всего дискового пространства), поэтому программа профессионального коммерче­ ского качества должна проверять и такие случаи. Функция f e o f ( F I LE * ) похожа на f e r r o r - она возвращает ненулевое число, есл и в указанном файле дости гнут его конец. int

f e o f ( F I LE * f p )

М ы пока не обращаем внимание на то, с какими кодами завершения заканчиваются наши небольшие илл юстрати вные программ ы , но в любой серьезной программе в таких случаях необходимо возвращать разумные и полезные значения.

7 . 7 . Вв од - в ы в од строк

В стандартной библ иотеке имеется фун кция ввода f g e t s , аналогич ная функции g e t 1 i n e , разработанной и и спользован ной в предыдущих главах: cha r * f g e t s ( c h a r * l i n e ,

int maxl i n e ,

F I L E * fp )

Функция f ge t s сч иты вает очередную строку ( вместе с символом ее конца) из файла в масси в символов 1 i n e ; в общей сложности ч итается не более max l i n e - 1 симво­ лов. Получившаяся строка завершается нулевым сим волом ' \ о ' . Обычно f ge t s воз­ вращает 1 i пе ; в конце файла или в случае ошибки она возвращает NULL. (Наша функ­ ция ge t l i n e возвращает более полезную информацию - дл ину строки ; если она равна нулю, был достигнут конец файла.) Что касается вывода, строка записы вается в файл функцией f pu t s (строка не обяза­ на содержать символ конца ) : fp

i n t fput s ( ch a r * l i n e ,

F I LE * f p )

Функция fpu t s возвращает EOF в случае ошибки и О в проти вном случае. Библиотечные функции ge t s и p u t s аналогичны f g e t s и f pu t s , но работают с потоками s t d i n и s t dout . Функция ge t s удаляет завершающий символ ' \ n ' , а pu t s добавляет его, что часто вызывает путани цу. Чтобы показать, что в функциях наподобие f ge t s и fput s нет ничего особенного, при ведем их полные тексты из стандартной библиотеки в нашей системе. с ч и тыв а н и е не б о л е е n симв о л о в /* f g e t s : c h a r * f ge t s ( char * s , i n t n , F I LE * i op )

{

из

i op * /

re g i s t e r i n t с ; re g i s t e r c h a r * c s ; cs = s ; whi l e ( - - n > О & & ( с = g e t c ( i op ) ) ! = E O F ) i f ( ( * cs++ = с ) == 1 \n ' ) break ; *cs = ' \ 0 ' ; r e t u rn ( с = = E O F && c s s ) ? NULL s '·

1 76

ГЛАВА 7

/ * fput s : выв од с т р о ки s в ф а йл i op i n t fpu t s ( char * s , F I L E * i op )

{

*/

с;

int

whi l e ( с = * s + + ) pu t c ( c , i op ) ; re turn f e rror ( i op )

?

EOF

:

О;

В стандарте указано, что f e r r o r возвращает ненулевое число в случае ошибки; fputs возвращает EOF в случае ошибки и неотрицательное значение в противном случае. С помощью f ge t s очень легко переп исать нашу функцию g e t l i n e : / * ge t l i n e : с ч и т ыв а е т с т р о к у и в о з в р а ща е т е е дли н у * / i n t ge t l i n e ( c h a r * l i ne , i n t max )

{

if

( f g e t s ( l i n e , max , s t d i n ) r e t urn О ; else r e t urn s t r l e n ( l i ne ) ;

= =

NULL )

7.6. Напишите програм му для сравнения двух файлов с выводом первой строки, в которой они отличаются.

У пражнен и е

У пражн е н и е 7.7. Доработайте программу поиска по образцу из главы 5 так, чтобы она брала исходные данные как из набора именованных файло в, так и из стандартного пото­ ка ввода (в том случае, есл и не заданы аргументы командной строки). Следует ли выво­ дить имя файла в случае, когда найдено соответствие?

7.8. Напишите программу, выводящую данные из набора файлов. Кажды й новый файл должен начи наться с новой страницы и заголовка в виде его имени; должна вестись сквозная для всех файлов нумерация страниц.

У пражн е н и е

7 . 8 . Р а зл ичн ы е функции

В стандартной библиотеке имеется широкое разнообразие всевозможных функци й . В этом разделе дается краткий обзор самых полезных из н и х . Дополнительные функции и более подробные описания можно найти в приложении Б.

7 8 1 Операции со строками .

.

.

Ранее уже упоминались такие функции для работы с о строками, как s t r l en, s t rcpy, s r t c a t и s t rcmp , объявленные в файле < s t r i ng . h > . Далее в списке s и t имеют тип char * , а с и n - тип i nt . strcat ( s , t )

присоединяет t в х вост к s

strncat ( s , t , n )

присоедин я ет n символов из t в х вост к s

st rcmp ( s , t )

дает отрицател ьное, нуль или положительное число при s в етст венно

Ввод- вывод




t СООТ·

1 77

s t rncmp ( s , t , n )

то же, что s t rcmp, н о выполняется над первыми n символами

st rcpy ( s , t )

копирует t в s

s t rncpy ( s , t , n )

копирует н е более n символов t в s

s t r l en ( s )

вычисляет длину s

s t rchr ( s , с )

возвращает указатель на первый символ с в s или NULL, если символ не найден

s trrchr ( s , с )

возвращает указатель на последний символ с в s или NULL, если символ не найден

7 . 8 . 2 . Анализ, классификация и преобразование символов Несколько функций и з файла < c typ e . h > выполняют анализ и преобразование сим­ волов. Далее считается, что с - число типа i n t , которое можно интерпретировать либо как u n s i gn e d char, либо как EOF. Все функции возвращают i n t . i s alpha ( с )

не нуль, если с - алфавитный символ; О , если нет

i supper ( с )

не нуль, если с - буква в верхнем регистре; О , если нет

i s l ower ( с )

не нуль, если с - буква в нижнем регистре; О , если нет

i sd i g i t ( с )

не нуль, если с - цифра; О , если нет

i s a l num ( с )

не нуль, если выполняется i s alpha ( с ) или i sdig i t ( с ) ; О , если нет

i s space ( с )

не нуль, если с - пробел, табуляция, конец строки, возврат каретки, перевод страницы, верти­ кальная табуляция

t oupper ( с )

возвращает символ с , приведенный к верхнему регистру

t ol ower ( с )

возвращает символ с , приведенный к нижнему регистру

7.8.3. Функция

unge t c

В стандартной библиотеке имеется довольно ограниченная версия функции ung e t ch, написанной нами в главе 4. Эта функция называется ung e t c : i n t unge t c ( int с , F I LE * fp ) Она помещает символ с назад в файл f p и возвращает либо с , либо EOF в случае ошибки. Для каждого файла гарантирован возврат только одного символа. Функцию ung e t c можно использовать совместно с любой из функций ввода s c a n f , ge t c или g e t char.

7 .8. 4 . Выполнение команд Функция sys t e m ( char * ) выполняет команду, содержащуюся в символьной стро­ ке s , а затем возобновляет выполнение текущей программы. Допустимое содержимое строки s сильно зависит от конкретной операционной среды. Ниже приведен тривиаль­ ный пример из системы Unix. sys t em ( " date " ) ;

1 78

ГЛАВА 7

Эюг оператор запускает на выполнение программу date ; она выводит текущую дату время в стандартный поток вывода. Функция sys t em возвращает целочисленный код за­ вершения выполненной команды, который зависит от характеристик системы. В системе Unix код завершения представляет собой значение, передаваемое с помощью функции exi t . и

7 .8.5. Управление памятью С помощью функций ma l l oc и c a l l o c выполняется динамическое распределение блоков памяти. Функция ma l l oc возвращает указатель на n байт неинициализирован­ ной памяти или NULL, если запрос на память нельзя выполнить: void *malloc ( s i ze_t n ) Функция c a l l o c возвращает указатель на участок памяти, достаточный для разме­ щения n объектов заданного размера s i z е или NULL, если запрос на память невыпол­ ним. Память инициализируется нулями. vo id * c a l l oc ( s i ze t n , s i ze_t s i ze ) Указатель, возвращаемый функциями ma l l oc и c a l l oc , выровнен в памяти надле­ жащим образом, но его еще нужно привести к нужному типу: int * ip ; ip = ( int * ) calloc ( n , s i zeof ( int ) ) ; Функция free ( р ) освобождает участок памяти, на который указывает указатель р, первоначально полученный вызовом функции ma l l oc или c al loc. Порядок освобожде­ ния выделенных участков памяти не регламентируется. Однако если указатель не бьm по­ лучен с помощью ma l l oc или c a l l oc , то его освобождение является грубой ошибкой. Обращение по указателю после его освобождения - также ошибка. Часто встречает­ ся неправильный фрагмент кода, в котором элементы списка освобождаются в цикле: for (р = head ; р ! = NULL ; р = p - >next ) / * НЕПРАВИЛЬНО* / free ( p ) ; Правильным было бы записать все, что нужно перед освобождением: for ( р = head ; р ! = NULL ; р = q ) { q = p - >next ; f ree ( р ) ; В разделе 8.7 показана реализация функции распределения памяти наподобие mal l oc, в которой выделенные блоки можно освобождать в любом порядке.

7 .8.6. Математические функции В заголовочном файле < ma t h . h > объявлено больше двадцати математических функций ; ниже перечислены наиболее часто используемые из них. Каждая принимает один или два аргумента типа douЫ e и возвращает результат типа douЫ e . sin (x)

синус х, х в радианах

cos ( x )

косинус х , х в радианах

atan2 ( y , x )

ар ктангенс отношения у / х в радианах

ввод-вывод

1 79

ехр ( х )

экспоненциальная функция (е в степени х )

l og ( x )

натуральный логарифм х ( х > о )

l ogl O ( x )

десятичный логарифм х ( х > о )

pow ( x , y )

х в степени у

sqrt ( x )

квадратный корень из х (при условии х >= о )

fabs ( х )

абсолютное значение х

7.8.7. Генерирование случайных чисел Функция rand ( ) вычисляет последовательность псевдослучайных целых чисел в диапазоне от О до RAND_МАХ - величины, определенной в файле < s t d l i b . h > . Вот один из способов генерировать случайные вещественные числа, большие или равные ну­ лю, но меньшие единицы: # de f ine f rand ( )

( ( douЬ l e )

rand ( )

/ ( RAND_МAX + l . O ) )

Учтите, что если в библиотеке вашего компилятора уже есть функция для вычисления случайных вещественных чисел, то она, скорее всего, имеет лучшие статистические свойства, чем приведенная. Функция s rand ( uns i gned ) устанавливает для rand инициализирующее значение. Системно-независимая реализация rand и s rand, предлагаемая стандартом, приведена в разделе 2. 7. Упражнение 7.9. Такие функции , как i s upper, можно реализовать с позиций экономии

времени , а можно с позиций экономии места. Исследуйте обе возможности .

1 80

ГЛАВА 7

Глава 8

Интерфейс системы

U n ix

Операционная система Unix предоставляет свои служебные средства программам че­ рез системные вызовы ( system calls), которые по сути я вляются встроенными функциями системы, вызы ваем ыми из пол ьзовател ьских программ. В этой главе описано, как поль­ зоваться некоторыми наиболее важны м и системными вызовам и в программах на языке С. Если вы работаете с системой Unix, эта информация пригодится вам непосредственно, поскольку часто бывает необходимо применять системные вызовы с максимальной эф­ фективностью или обращаться к каким-то средствам, которых нет в стандартной библио­ теке. Но даже если вы пользуетесь языком С в другой операционной системе, изучение этих примеров поможет вам глубже понять программирование на С хотя конкретные подробности зависят от с истемы, аналогичный код можно найти повсюду . Так как биб­ лиотека ANSI С в основном построена на идеях Unix, приведенный в этой главе код мо­ жет помочь вам также в понимании работы библиотечных средств . Тематику этой главы можно сгруппировать в три больших раздела: ввод-вывод, фай­ ловая система и управление памятью. Для изучения первых двух подразумевается неко­ торое знакомство с внеш ними характеристи ками системы Unix. В главе 7 изучался интерфейс ввода-вывода, общий для всех о перационных систем в силу стандарта. В любой конкретной системе функции стандартной библиотеки пишутся на основе средств, предоставляемых системой. В следующих нескольких разделах рас­ сматриваются системные вызовы Unix для ввода и вывода, а затем демонстрируется , как с их помощью можно реализовать средства стандартной библиотеки. -

8 . 1 . Дескрипторы ф айлов

В операционной системе Unix весь ввод-вывод осуществляется путем чтения и записи файлов, поскол ьку все периферийные устройства, даже клавиатура и экран монитора, яв­ ляются файлами единой файловой системы. Это означает, что любая передача данных между программой и периферийными устройствами управляется через единообразн ы й интерфейс. В самом общем случае, перед тем как читать или записы вать файл, необходимо про­ информировать об это м намерении операционную систему. Этот процесс называется открытием файла. Если будет выполняться запись в файл, может оказаться необходи­ мым создать его или удалить его предыдущее содержимое. Система проверяет, имеете ли вы право на это (существует ли файл и есть ли у вас допуск к нему), и, если все в по­ рядке, возвращает в программу небольшое неотрицательное целое число, и менуемое де ­ скриптором файла (jile descriptor). Независимо от того, будет ли выполняться ввод или вывод, для идентификации файла вместо его имени будет при меняться дескриптор . (Дескри птор аналогичен файловому указателю из стандартной библиотеки; в системе MS-DOS также используется нечто похожее. ) Вся и нформация об открытом файле нахо-

дится в ведении операционной системы ; прикладная программа обращается к файлу только по его дескриптору. Поскольку ввод-вывод с применением клавиатуры и экрана очень распространен, для удобства его выполнения приняты некоторые дополнительные меры. Когда программа запускается на выполнение в командном интерпретаторе (системной выполняющей сре­ де или оболочке), автоматически открываются три файла с дескрипторами О, 1 и 2. Они называются стандартными потоками ввода, вывода и ошибок. Если программа вводит данные только из файла О, а выводит в 1 и 2, ей нет нужды беспокоиться об открытии файлов. Пользователь программы может перенаправить ввод-вывод в файл и из файла с по­ мощью символов-команд < и > :

prog < i nf i l e > out f i l e В этом случае среда изменяет автоматически ассоциированные с дескрипторами О и 1 файлы на заданные. Обычно дескриптор 2 всегда остается ассоциированным с экраном, чтобы на него можно было выводить сообщения об ошибках. Примерно то же самое имеет место в случае ввода-вывода через конвейеры. В любом случае ассоциацию деск­ рипторов файлов выполняет операционная среда, а не программа. Программа понятия не и меет, откуда берутся ее входные данные и куда поступают выходные; она знает только, что входной поток соответствует дескриптору О, а два выходных 1 и 2. -

8 . 2 . В вод- в ы вод низкого уровня функ ц ии r e ad и wr i t e При вводе и выводе используются системные вызовы r e a d и wri t e , к которым про­ грамм ы на С обращаются через две функции с теми же именами. У обеих функций пер­ вый аргумент - дескриптор файла. Второй аргумент представляет собой символьный массив из программы, в который или из которого поступают данные. Третий аргумент количество байт, которые необходимо переслать.

int n_read read ( int fd , char *buf , int n ) ; int n_wri t ten wr i t e ( int fd , char * buf , int n ) ; =

=

При каждом вызове возвращается количество переданных байт. При чтении возвра­ щаемое количество байт может быть меньше, чем было запрошено . Нулевое количество означает, что достигнут конец файл, а - 1 обозначает какую-либо ошибку. При записи возвращается количество записанных байт; если оно не равно количеству, посланному на запись, то произошла ошибка. В ходе одного вызова можно записать или прочитать любое количество байт. Наибо­ лее распространенные случаи - это один символ за раз ("небуферизованный ввод­ вывод") либо количество наподобие 1 024 или 4096, которое соответствует размеру фи­ зического блока данных на периферийном устройстве. Большими блоками передавать данные эффективнее, поскольку при этом выполняется меньше системных вызовов. Из всего вышесказанного следует, что можно написать простую программу для копи­ рования входного потока в выходной - эквивалент программы копирования файлов из главы 1 . Эта программа сможет копировать любые потоки, поскольку ввод и вывод можно перенаправить на любой файл или устройство.

1 82

ГЛАВА 8

# inc lude " sys ca l l s . h " ma in ( )

{

/ * копирование потока в в ода в по т о к вывода * /

char buf [ BUFS I Z ] ; int n ; whi l e ( ( n = read ( O , buf , BUFS I Z ) ) > О ) wri t e ( l , buf , n ) ; return О ;

Здесь функциональные прототипы системных вызовов считаются собранными в файл sys c a l l s . h, который будет включаться в программы этой главы. Имя этого файла не является стандартным. Параметр BUFS I Z также определен в файле sys c a l l s . h; его размер выбирается подходящим для конкретной системы. Если размер файла не кратен BUFS I Z , в какой-то момент при вызове read будет прочитано меньше байт (и затем записано вызовом wr i t e), а следующий вызов возвратит значение О. Целесообразно рассмотреть вопрос, как можно использовать read и wri te для кон­ струирования функций более высокого уровня - get char, put char и т.п. Например, далее приводится версия g et c har, выполняющая небуферизованный ввод путем считы­ вания из стандартного потока по одному символу за раз. # inc lude " sysca l l s . h " / * getchar : небуферизованный в в о д одиночного симв ола * / int get char ( vo i d )

{

char с ; re turn ( read ( O , &с , 1 ) = = 1 ) ? ( un s i gned cha r ) с : EOF ;

Переменная с должна иметь тип char, поскольку read требует указателя на символ. Приведение с к типу uns i gned char в операторе return устраняет потенциальную проблему с расширением знака. Вторая версия ge t char выполняет ввод большими буферами, но выдает символы по запросам по одному за раз. # include " syscal l s . h " / * getchar : версия с про с т ой буферизацией * / int getchar ( vo i d )

{

static char bu f [ BUFS I Z ] ; stat ic char *bufp = bu f ; s ta tic int n = О ; i f (n == О ) { / * буфер пустой * / n = read ( O , buf , s i zeof bu f ) ; bufp = buf ;

}

return ( - - n >= О ) ? ( uns igned cha r ) * bufp++ И НТЕРФ ЕЙС С И СТЕМЫ UNIX

EOF ;

1 83

Если бы эти версии g e t char компилировались с включением файла < s t d i o . h > , необходимо было бы отключить определение и м е н и g e t c h a r с помощью директивы # unde f на тот случай, если оно введено как макрос.

8 . 3 . Функции

op en , c r e a t , c l o s e ,

un l i nk При работе с файлами, н е являющимися стандартными потоками ввода, вывода и ошибок, необходимо открыть их явным образом, чтобы и меть возможность читать и за­ писывать . Для этого существуют два системных вызова - open и c r e a t (именно так, а не c re a t e ) . Функция op en очень похожа на f open, которая рассматривалась в главе 7 , но она возвращает не файловый указатель, а файловый дескриптор, представляющий собой все­ го-навсего целое число типа i n t . В случае ошибки возвращается - 1 . # i nc lude < f cnt l . h> int fd ; int open ( char * name , fd

=

open ( name ,

int f l ags ,

i nt perms ) ;

f l ags , perms ) ;

Как и в случае f open, аргумент name - это символьная строка, содержащая имя от­ крываемого файла. Второй аргумент, f l ag s , имеет тип int и указывает способ открытия файла. Ниже приведены некоторые основные значения, которые он может принимать. О

RDONLY

О

открытие тол ько для чтения

WRONLY

О

открытие тол ько дл я записи

RDWR

открытие для чтения и записи

Эти константы определены в файле < f c n t l . h> в верси и системы Unix под названи­ ем System У и в файле < sys / f i l e . h> в версии BSD (Berkeley) . Существующий файл открывается для чтения следующим образом : fd

=

open ( name , O_RDONLY ,

О) ;

Во всех дальнейших применениях функции open аргумент pe rms всегда будет равен О. Попытка открыть несуществующий файл является ошибкой. Для создания новых файлов или перезаписи существующих заново используется системный вызов c r e a t : i n t creat ( char * name , i n t perms ) ; fd

=

creat ( name , perms ) ;

При этом возвращается дескриптор файла, если создание прошло успешно, или - 1 , если нет. Если файл уже существует, c r e a t усекает его до нулевой длины, тем самым уничтожая имеющееся содержимое. Таким образом , создавать существующий файл с помощью c r e a t не является ошибкой. Если файл не существует, c re a t создает его с набором допусков, определяемых ар­ гументом p e rms . В файловой системе Unix с файлом ассоциируется девять битов слу­ жебной информации о допусках к нему. Они регулируют чтение, запись и запуск этого

1 84

ГЛАВА 8

файла его владельцем , группой, к которой принадлежит владелец, и всеми остальными пользователями . Для иллюстрации покажем упрощенную верси ю программ ы ер из среды Unix, кото­ рая копирует один файл в другой. Наша версия копирует всего один файл, не допускает имени каталога в качестве второго аргумента, и сама устанавливает допуски вместо ко­ пирования их.

# ine lude < s tdio . h > # inc lude < f cnt l . h > # ine lude " sysea l l s . h " #def ine PERМS 0 6 6 6 / * Чтение / запись дпя владельца , группы , осталь ных * / voi d e rror ( ehar * ,

. . .) ;

/ * ер : копиров ание файл а f l в f 2 * / main ( int arge , ehar * argv [ ] )

{

int f l , f2 , n ; ehar buf [ BUFS I Z ] ; i f ( arge ! 3 ) e rror ( 11 Usage : ер f rom t o 11 ) ; i f ( ( f l = open ( argv [ l ] , O_RDONLY , О ) ) -1) e rror ( 11 ep : ean ' t open % s " , argv [ l ] ) ; if ( ( f2 creat ( argv [ 2 ] , PERMS ) ) = = - 1 ) e rror ( 11 ep : ean ' t ereate % s , mode % 0 3 0 11 , argv [ 2 ] , PERMS ) ; wh i l e ( ( n read ( f l , buf , BUFS I Z ) ) > О ) i f ( wr i t e ( f 2 , buf , n ) ! = n ) e rror ( 11 ep : wr i t e error on f i l e % s " , argv [ 2 ] ) ; return О ; =

= =

=

=

Эта программа создает файл для записи с всегда фиксированными допусками 0 6 6 6 . С помощью системного вызова s t a t , описанного в разделе 8.6, можно определить ре­ жим допусков существующего файла и установить такой же для его копии. Обратите внимание, что функция e rror вызывается с переменными списками аргу­ ментов аналогично p r i nt f . Реализация e r r o r показывает пример использования еще одного члена семейства p r i nt f . Существует библиотеч ная функция vp r i nt f , которая во всем похожа на p r i nt f , но вместо переменного списка аргументов принимает один аргумент, инициализированный вызовом м акроса va_s t a r t . Аналогичным образом v f p r i nt f и v s p r i nt f повторяют возможности соответственно f p r i nt f и s p r i n t f .

# ine lude < s tdio . h> # inelude < s tdarg . h > / * e rror : вывод со обще н и я об ошибке и о с т а н о в пр о граммы * / voi d error ( ehar * fmt , . . . )

{

va l i s t args ; va_start ( args , fmt ) ; fp r i n t f ( s t derr , 11 e rror : 11 ) ; v f p r i nt f ( s t derr , fmt , args ) ;

И НТЕРФ ЕЙС СИСТЕМЫ UNIX

1 85

fprint f ( s tderr , " \ n " ) ; va end ( args ) ; exit ( l ) ; Существует предельное количество (обычно около 20) файлов, которые программа может одновременно держать открытыми. Соответственно любая программа, которой необходимо обрабатывать сразу много файлов, должна быть готова к п о втор но му ис­ пользованию файловых дескрипторов. Функция c l o s e ( i n t f d ) разрывает связь меж­ ду файловым дескриптором и открытым файлом и освобождает дескриптор для после­ дующей работы с другим файлом. Она соответствует функции f c l o s e из стандартной библиотеки с той разницей, что при этом нет никакого очищаемого буфера. Выход из программы с помощью функции exi t или возвращение из тела ma i n оператором return автоматически закрывает все открытые файлы. Функция un l i nk ( char * name ) удаляет заданное имя файла из файловой системы. Она соответствует стандартной библиотечной функции remove . Упражнение 8. 1 . Перепишите программу c a t и з главы 7 с использованием системных вызовов read, wri t e , op e n и c l o s e вместо их эквивалентов из стандар тно й библио­ теки. Проведите эксперименты по сравнению быстродействия двух версий этой про­ граммы .

8 . 4 . Прямой до ступ к фа й л у и функ ц ия l s e e k Ввод и вывод обычно выполняются п осл едо вател ь н о : каждая операция read или wr i t е выполняется в той позиции файла, на к оторо й остановилась предыдущая опера­ ция. Однако по мере необходимости файл можно считывать или записывать в произ­ вольном порядке. Системный вызов l s eek поз в ол яет передвигаться по файлу, не счи­ тывая и не записывая н и каки х данных: l ong l seek ( int f d , long o f f s e t , int origin ) ;

Эта функция устанавливает указатель те кущей позиции в ф айле с дескриптором f d на расстояние o f f s e t байт от места, з аданн ого параметром o r i g i n. Следующая операция чтения или записи будет выполняться в этой позиции . Ар гум ент or i g i n может прини­ мать значения О, 1 или 2, указывая тем самым, что смещение o f f s e t следует отсчиты­ вать соответственно от начала, текущей позиции или конца файла. Например, чтобы до­ писать данные в ко не ц файла (для чего в командной оболочке Unix служит команда пе­ аргумент 11 а 11 ) , следует перед записью ренаправления > > , а в функции f open установить указатель позиции на конец ф айла: l s eek ( f d , O L , 2 ) ; -

Чтобы переместиться в начало ("перемотать" файл), нужно записать следующее:

l seek ( f d , O L , О ) ; Обратите внимание н а аргумент O L ; его также можно записать в виде ( l ong ) О или просто О , если l s e e k объявлена должным о б р азом .

1 86

ГЛАВА 8

С помощью функции l s e e k можно обращаться с файлами более-менее как с боль­ шими массивами, ценой некоторого снижения скорости доступа. Например, следующая функция считывает заданное количество байт из произвольного места файла. Она воз­ вращает количество считанных байтов или 1 в случае ошибки. -

# inc lude " syscal l s . h " / * get : считывание n байт из по зиции pos * / int get ( int fd , l ong pos , char * bu f , int n )

{

i f ( l seek ( fd , pos , О ) > = О ) / * подход к pos * / return read ( fd , bu f , n ) ; else return - 1 ;

Из l s e e k возвращается значение типа l ong, дающее новую текущую позицию в файле, или - 1 в случае ошибки. Стандартная библиотечная функция f s e e k аналогична l s e ek, но ее первый аргумент должен иметь типа F I LE * , а в случае ошибки она воз­ вращает ненулевое число.

8 . 5 . Пример реализации фун кций f op en и g e t c Продемонстрируем, как некоторые из описанных средств работают вместе, на приме­ ре реализации стандартных библиотечных функций f open и ge t c . Напом ним, что файлы в стандартной библиотеке описываются файловым и указате­ лями, а не дескрипторами. Файловый указатель - это указатель на структуру, содержа­ щую набор данных о файле. Набор данных включает в себя указатель на буфер, через ко­ торы й файл можно читать большими фрагментами; счетчик количества символов, ос­ тавшихся в буфере; указатель на очередную символьную позицию в буфере; дескриптор файла; различные флаги, задающие режим чтения и записи, текущие ошибки и т.д. Структура данных, описывающая файл, определена в файле < S t d i o . h > , который необходимо включить с помощью директивы # i nc lude в любой файл исходного кода, где используются функции и средства стандартной библиотеки ввода-вывода. Функции самой библиотеки также подключают этот файл. В следующем фрагменте типичного файла < s t d i o . h> имена, предназначенные для использования только функциями самой библиотеки, начинаются со знака подчеркивания, чтобы уменьшить вероятность их сов­ падения с идентификаторами прикладных программ . Это соглашение используется все­ ми стандартными библиотечными функциями.

#de f ine #de f ine #de f ine #de f ine

NULL EOF BUFS I Z OPEN МАХ

typede f s t ruct int cnt ; char *p tr ;

О (-1)

1024 2 0 / * максимал ь но е колич е с т в о о т крытых файлов * / iobuf { / * скол ь ко о с т ал о с ь символов * / / * следующая симв ол ь н а я позиция * /

И НТЕРФ ЕЙС СИСТЕМЫ UNIX

1 87

/ * ме с т о нахождение буфера * / char *base ; int f l ag ; / * режим до с т упа к файлу * / / * де с крип т ор файл а * / int f d ; F I LE ; exte rn F I L E i оЬ [ О РЕN_МАХ] ; #de f ine stdin #de f i ne s t dout #de f ine s t derr enum _f l ags READ WRITE UNBUF EOF ERR

};

int int

( &_iob [ О ] ) ( &_i ob [ 1 ] ) ( &_iob [ 2 ] ) /* /* /* /* /*

01 , 02 , 04 , 010 , 020

файл о т крыт для ч т е ния * / файл о т крыт для записи * / ф айл бе з буферизации * / в ф айле до стигнут конец * / произошла ошибка * /

f i l lbuf ( F I LE * ) ; f l ushbuf ( int , F I LE * ) ;

#def ine f e o f ( p ) #def ine f e rror ( p ) #de f ine f i l eno ( p )

( ( ( р ) - > f l ag ( ( ( p ) - > f l ag ( (p) - >fd)

& &

EOF ) ERR )

!= О) != О)

# de f ine getc ( p )

( - - ( p ) - > cnt > = О \ ( un s i gned cha r ) * ( p ) - >ptr+ + f i l lbuf ( p ) ) #de f ine put c ( х , р ) ( - - ( р ) - >cnt > = О \ _f lushbuf ( ( x ) , p ) ) ? * ( p ) - >p t r + + = ( х ) ?

# de f ine get char ( ) #de f ine put char ( x )

getc ( stdin ) put c ( ( х ) , s tdout )

В большинстве случаев макрос ge t c уменьшает на единицу количество непрочитан­ ных символов, передвигает указатель вперед и возвращает прочитанный символ . (Напоминаем , что слишком длинная директива # de f i ne продолжается на следующей строке с помощью обратной косой черты . ) Если счетчик символов становится отрица­ тельным, g e t c вызывает функцию _ f i l l b u f для пополнения буфера, заново инициа­ лизирует содержимое структуры, а затем возвращает полученный символ. Символы воз­ вращаются в формате uns i gned, чтобы гарантировать их положительность. Хотя детали здесь не обсуждаются , все же мы включили в текст определение put c , чтобы показать, что этот макрос устроен и работает примерно так же, как g e t c , вызывая функцию _f l u s hbu f в случае заполненного буфера. Включены также определения макросов для обращения к и ндикаторам ошибок и конца файла, а также для получения файлового дескриптора. Теперь можно написать функцию f o p e n . Больша.я часть этой функции посвящена открытию файла и установке указателя в нужное место, а также заполнению битов­ флагов состояния. Функция f open не занимается выделением памяти для буфера; эта задача возложена на функцию _ f i l l bu f , которая вызы вается при первом чтении из файла.

# inc lude < f cnt l . h > # inc lude " sysca l l s . h "

1 88

ГЛАВА 8

/ * разрешены ч т е ние и запись * / #def ine PERMS 0 6 6 6 / * для владел ь ца , группы , о с т ал ь ных * / / * fopen : о т крытие файла с возвращением файло в о г о указателя * / F I LE * f open ( char * name , char * mode )

{

int f d ; F I LE * fp ; i f ( *mode ! = ' r ' & & *mode ! = ' w ' && * mode ! = ' а ' ) return NULL ; for ( fp = i ob ; fp < iob + OPEN МАХ ; fp+ + ) i f ( ( f p - > f l ag & () EAD 1 _WRITE ) ) = = О ) / * найдено свободное ме с т о для файла * / break ; i f ( fp > = i ob + ОРЕN_МАХ ) / * н е т с в о бодных мест для файла * / return NULL ; i f ( *mode = = ' w ' ) fd = creat ( name , PERMS ) ; e l se i f ( *mode = = ' а ' ) { i f ( ( fd = open ( name , O_WRONLY , О ) ) -1) fd = creat ( name , PERMS ) ; l seek ( f d , O L , 2 ) ; else fd = open ( name , О-RDONLY , О ) ; i f ( fd = = - 1 ) ; / * указанное имя недоступно * / return NULL ; fp - > fd = fd ; fp - > Cnt = О ; fp - >base = NULL ; _WR ITE ; ' r ' ) ? READ fp - > f l ag = ( * mode return fp ;

Эта версия f open не работает со всеми возможными режимами доступа к файлу, имеющимися в стандарте, хотя их реализация и не потребовала бы много дополнитель­ ного кода. В частности, наша функция f open не распознает режим 11 Ь 11 для двоичного доступа к файлу, поскольку система Unix не делает такого различия, а также режим 11 + 11 для комбинированного чтения и записи. При первом вызове g e t c с конкретны м файлом счетчи к непрочитанных символов равен нулю, поэтому автоматически вызывается _ f i l l bu f . Если функция _ f i l lbuf обнаруживает, что файл не открыт для чтения, она немедленно возвращает EOF. В про­ тивном случае она пытается выделить буфер памяти (если чтение должно буферизиро­ ваться). Как только буфер создан, _ f i l l bu f вызывает read для его заполнения, устанавли­ вает счетчик и указатели и возвращает символ из начала буфера. При последующих вы­ зовах _f i l lbuf буфер уже будет в готовом виде.

# inc lude " sysca l l s . h " / * f i l lbuf : создание и заполне ние буфера в в ода * / int f i l lbuf ( F ILE * fp )

{

int buf s i z e ;

И НТЕРФ Е ЙС СИСТЕМ Ы UNIX

1 89

i f ( ( fp - > f lag& ( _READ l _EOF l _ERR ) ) ! = _READ ) return EOF ; buf s i z e = ( fp - > f l ag & _UNBUF ) ? 1 : BUFS I Z ; i f ( fp - >base = = NULL ) / * буфера еще нет * / i f ( ( fp - >base = ( char * ) mal loc ( buf s i ze ) ) = = NULL ) return EOF ; / * не удается создать буфер * / fp - >pt r = fp - >base ; fp - > cnt = read ( fp - > f d , fp - >pt r , buf s i ze ) ; i f ( - - fp - > cnt < О ) { i f ( fp - > cnt = = - 1 ) fp - > f lag 1 = _EOF ; else fp - > f lag 1 = _ERR ; fp - > cnt = О ; re turn EOF ; } return ( uns igned cha r ) * fp - >pt r+ + ; Единственное, что осталось проработать, - инициализацию всех файловых опера­ ций. Следует определить массив _i ob и инициализировать его потоками s t din, s t dout и s t de r r :

F I LE _iоЬ [ ОРЕN_МАХ] = { О , ( char * ) О , { О , ( char * ) О , { О , ( char * ) О , };

{ ( char ( char ( char

/* *) *) *)

stdin , stdout , stderr : * / О , _READ , О } , О , _WR I TE , 1 } , О , _WR I TE 1 _UNBUF , 2 }

Инициализация полей f l ag файловых структур показывает, что s t d i n будет исполь­ зоваться для чтения, s t dout для записи, а s tde rr для записи без буферизации. -

Упражнение

-

8.2. Перепишите функции f open и _f i l lbuf с применением битовых

полей вместо явных операций над битами. Сравните объем кода и быстродействие. Упражнение

8.3. Спроектируйте и напишите функции _f l u s hЬu f , f f lush и f c l os e .

Упражнение

8.4. Следующая стандартная библиотечная функция эквивалентна l s eek:

int f s eek ( FI LE * fp , l ong o f f set , int origin) Различие состоит в том, что е е аргумент fp - это файловый указатель, а н е файловый дескриптор, и возвращаемое ею значение типа int сообщает статус завершения, а не те­ кущую позицию. Напишите функцию f s e ek. Сделайте так, чтобы она согласовывала свою работу с буферизацией, выполняемой другими библиотечными функциями.

8 . 6 . П р име р получ е н и я сп и ска фа йл о в в каталоге Иногда бывает необходимо получить через файловую систему информацию не о со­ держимом файла, а о самом файле. Примером может служить программа l s из системы Unix для вывода списка файлов в каталоге с сопутствующей информацией о них, такой как размеры, допуски и т.д. Аналогичные функции выполняет команда d i r в системе MS-DOS.

1 90

ГЛАВА 8

Поскольку каталог в Unix представляет собой просто файл, программе l s нужно все­ го лишь прочитать этот файл и извлечь из него нужные имена. Но для получения другой информации - например, размера файла - необходимо пользоваться системными вы­ зовами. В других системах даже для получения имен файлов в каталоге не обойтись без системного вызова; именно так обстоит дело в MS-DOS. Хотелось бы обеспечить доступ к этой информации сравнительно системно-независимым способом, пусть даже реализа­ ция окажется существенно основанной на средствах конкретной системы . Проиллюстрируем сказанное, написав программу под названием f s i z e . Эта про­ грамма - особая разновидность l s , выводящая размеры всех файлов, перечисленных в списке ее аргументов командной строки. Если один из файлов оказывается каталогом, f s i z e применяет сама себя рекурсивно к этому каталогу. Если аргументов нет вообще, программа работает с текущим каталогом. Начнем с краткого обзора устройства файловой системы Unix. Каталогом (directory) в ней называется файл, содержащий список имен файлов и информацию об их местопо­ ложении. Эта информация представляет собой ссылку на другую таблицу - список фай­ ловых индексов. Файловый индекс (inode) представляет собой структуру данных, содер­ жащую всю информацию о файле, кроме его имени. Каждый пункт списка файлов в ка­ талоге обычно состоит из имени файла и соответствующего номера индекса. К сожалению, формат и точное содержание файла-каталога меняется от одной версии системы к другой. Поэтому разделим задачу на составные части и попытаемся изолиро­ вать непереносимые фрагменты. На внешнем уровне определяется структура D i rent и три функции, opend i r, readd i r и c l o s e d i r, которые обеспечивают системно­ независимый доступ к имени и номеру индекса в списке каталога. Функция f s i z e будет написана с применением этого интерфейса. Затем будет показано, как реализовать эти средства в системах, имеющих ту же структуру каталогов, что и системы Unix Version 7 и System 5. Проработку вариантов оставляем читателю в качестве упражнения. Структура D i rent содержит номер индекса и имя. Максимальная длина компонен­ ты, соответствующей имени, равна NАМЕ_МАХ и зависит от реализации системы. Функ­ ция opend i r возвращает указатель на структуру D I R, аналогичную F I LE, которая ис­ пользуется в readd i r и c l o s e d i r . Вся эта информация собрана в файле d i rent . h: #de f ine NАМЕ МАХ 14 / * самая длинная компонента имени * / / * системн о - зависимая * / / * универс ал ь ный пункт списка : * / typede f s t ruct { / * номер инде кса * / l ong ino ; / * имя + \ о * / char name [NAМE_МAX+ l ] ; Di rent ; '

typede f s t ruct int fd ; Di rent d ; DIR ;

'

/ * минимум : б е з буферизации и т . п . * / / * файловый дескрип т ор каталога * / / * пункт списка файл ов * /

DIR * opendi r ( char * di rname ) ; Di rent * readdi r ( D I R * d f d ) ; vo id c l osedi r ( D I R * d f d ) ; Системный вызов s t a t принимает имя файла в качестве аргумента и возвращает всю информацию из индекса этого файла или - 1 в случае ошибки. Следующий фрагмент коИ НТЕРФ ЕЙС СИСТЕМЫ UNIX

191

да заполняет структуру stbuf информацией из индекса, соответствующей заданному имени ф айл а: char *name ; s t ruct stat s tbuf ; int stat ( char * , s t ruct stat * ) ; s tat ( name , &s tbuf ) ; Структура, описывающая возвращаемое из s t a t значение, определена < sys / s t a t . h> и обычно выглядит так : s t ruct stat / * информация о файле , в о звращаемая stat * /

{

};

dev t ino t short short short short dev t of f t t ime t t ime t t ime t

st_dev ; st i no ; s t_mode ; s t nl ink ; st_u i d ; s t_g i d ; st _ rdev ; st_s i z e ; st_at ime ; st_mt ime ; s t c t i me ;

/* /* /* /* /* /* /* /* /* /* /*

в

устройс т в о индекса * / номер инде кса * / биты режима * / колич е с т в о ссылок н а файл * / иде нтификационный номер владел ь ца * / идентификационный номер группы владельца * / для специаль ных файлов * / р а змер файла в символах * / время п о следн е г о обр аще ния * / время п о следней модификации * / время по следней модификации индекса * /

Бол ьшинство значений разъяснены в комментариях. Типы dev_ t и i no_ t опреде­ лены в файле < sys / typ e s . h > , который также следует подключить. Поле s t_mode содержит набор флаговых битов, описывающих свойства файла. Оп­ ределения флагов также даны в файле < sys / s ta t . h > ; нам понадобится только та часть, которая имеет отношение к типам файлов :

#def ine # de f ine #de f ine #def ine # de f ine

S I FMT S I FD I R S I FCHR S I FBLK S I FREG

0160000 0040000 0020000 0060000 0100000

/* /* /* /* /*

тип файла : * / ка т ал о г * / симв оль ный с п ециал ь ный * / блочный с п е циал ь ный * / обычный * /

/* " . */ Теперь мы готовы к написанию программы f s i z e . Если режим, полученный от s t a t , соответствует файлу, а не каталогу, то размер сразу же оказывается под рукой, и его можно непосредственно вывести. Однако если файл я вляется каталогом, тогда необ­ ходимо дополнительно проч итать из него список файлов по одному за раз. К тому же ката­ лог может содержать подкаталоги, так что обработка будет носить рекурсивный характер. Главный модуль программы обрабатывает аргументы командной строки; он передает каждый аргумент по очереди в функцию f s i z e .

# inc lude # i nc lude # inc lude # inc lude # inc lude # inc lude # include

1 92

< stdio . h> < S t ri ng . h > " sysca l l s . h " < f cnt l . h > < Sys / types . h > < sys / s tat . h > 1 1 di rent . h "

/ * фл а ги ч т е ни я и з а пис и * / / * определе ния т и п ов * / / * с трукт ура , в о з вр аща ема я s t a t * /

ГЛАВА 8

vo id f s i z e ( char * ) ; / * вывод ра змеров файлов * / ma in ( int argc , char * * argv ) { i f ( argc = = 1 ) / * по умолч анию т е кущий катало г * / fsize ( " . " ) ; e l se whi l e ( - - argc > О ) f s i z e ( * + + argv ) ; return О ; Функция f s i z е выводит размер файла. Однако если файл является каталогом, f s i z e вначале вызывает функцию d i rwa l k для обработки всех файлов каталога. Обра­ тите внимание, как для определения того, является ли файл каталогом, используются флаги S_I FMT и S_I FD I R из файла < sys / s t a t . h > . Наличие скобок обязательно, по­ скольку приоритет операции & ниже, чем = = .

int stat ( char * , s t ruct s t at * ) ; vo i d dirwal k ( char * , void ( * f cn ) ( char * ) ) ; / * f s i z e : вывод размера файла name * / vo i d f s i z e ( char * name ) { s t ruct stat stbuf ; i f ( s tat ( name , &stbuf ) = = - 1 ) fprint f ( s t derr , " f s i z e : can ' t a c c e s s % s \ n " , name ) ; return ;

}

i f ( ( s tbuf . s t mode & S I FMT ) = = S I FD I R ) d i rwa l k ( name , f s i z e ) ; print f ( " % 8 l d % s \ n " , s tbuf . s t_s i z e , name ) ; Функция d i rwa l k имеет обобщенное назначение ; ее задача - применить заданную функцию к каждому файлу в каталоге. Она открывает каталог, перебирает все имеющие­ ся в нем файлы, вызывает заданную функцию с каждым из них, а затем закрывает ката­ лог и завершается. Поскольку функция f s i z e вызывает d i rwa l k в каждом каталоге, эти две функции связаны между собой рекурсивным вызовом.

#de f ine МАХ_РАТН 1 0 2 4 / * d i rwalk : применение fcn к о в с ем файлам каталога d i r * / vo i d dirwal k ( char * di r , vo i d ( * f cn ) ( char * ) ) { char name [МAX_PATH ] ; D i rent * dp ; D I R * dfd ; i f ( ( dfd = opendi r ( di r ) ) = = NULL ) { fprint f ( s tderr , " di rwa l k : can ' t open % s \ n " , di r ) ; re turn ;

}

whi l e ( ( dp = readd i r ( df d ) ) И НТЕРФ ЕЙС СИСТЕМЫ UNIX

! = NULL ) {

1 93

i f ( s t rcmp ( dp - >name , 11 • 11 ) == О 1 1 st rcmp ( dp - >name , 11 • • 11 ) == О ) cont inue ; / * пропустит ь себя и родитель с кий * / i f ( s t r l en ( d i r ) + s t r l en ( dp - >name ) + 2 > s i zeof ( name ) ) fprint f ( s t derr , 11 d i rwa l k : name % s % s too long \ n 11 , di r , dp - >name ) ; else { sprint f ( name , 11 % s / % s 11 , di r , dp - >name ) ; ( * f cn ) ( name ) ;

}

c l osedi r ( df d ) ; При каждом вызове функции readd i r возвращается указатель на информацию об очередном файле или NULL, если файлов больше нет. Каждый каталог всегда содержит пункты списка, соответствующие ему самому, с именем 11 • 11 , и родительскому каталогу с именем 11 • • 11 • Их необходимо пропустить, иначе программа зациклится. Вплоть до этого уровня код был независим от того, в каком формате хранится ин­ формация о каталогах. Следующий наш шаг - написать минимальные, урезанные вер­ сии функций opendi r, readd i r и c l o s e d i r для конкретной системы . Приведенные ниже варианты написаны для Unix Version 7 и System V; они используют формат инфор­ мации о каталогах из заголовочного файла < sys / d i r . h > , который выглядит так:

# i fnde f D I RS I Z #de f ine D I RS I Z #endi f s t ruct d i rect

{

};

14 /* запись в каталоге * /

/ * номер индекса * / ino t d_ino ; char d_name [ D I RS I Z ] ; / * в длинном имени нет ' \ 0 ' * /

Некоторые версии системы разрешают намного более длинные имена и имеют более сложную структуру каталогов. Тип i no_t определяется через typede f и обозначает номер в списке файловых ин­ дексов. В системе, которой мы обычно пользуемся, он совпадает с uns igned short , но информацию об этом нельзя внедрять в программу, поскольку в разных системах тип может отличаться. Поэтому лучше использовать typede f . Полный набор "системных" типов можно найти в файле < S ys / types . h > . Функция opend i r открывает каталог, проверяет, что файл является каталогом (на этот раз системным вызовом f s t a t , аналогичным s t a t с той лишь разницей, что он применяется к файловому дескриптору), размещает в памяти структуру каталога и запи­ сывает информацию :

int f s tat ( int f d , s t ruct stat * ) ; / * opend i r : о т крыв а ет каталог для вызовов readd i r * / D I R * opendi r ( char * d i rname )

{

1 94

int fd ; s t ruct st at stbuf ; D I R * dp ;

ГЛАВА 8

i f ( ( fd = open ( di rname , о RDONLY , О ) ) = = - 1 ) 1 1 f stat ( fd , &s tbuf ) == � 1 1 1 ( s tbuf . st_mode & S_I FMT ) ! = S_I FD I R 1 1 ( dp = ( D I R * ) mal l oc ( s i zeof ( D I R ) ) ) = = NULL ) return NULL ; dp - > fd = fd ; return dp ; Функция c l o s e d i r закрывает файл каталога и освобождает место в памяти: / * c losedi r : закрытие катало га , о т крыт о г о opendi r * / void c l osedi r ( D I R * dp )

{

i f ( dp ) { close ( dp - > f d ) ; f ree ( dp ) ;

Наконец, функция readd i r с помощью функции read считывает каждую запись о файле в каталоге. Если какая-либо ячейка в каталоге не используется в текущий момент (файл удален), то номер индекса равен нулю, и данная позиция пропускается. В против­ ном случае номер индекса и имя помещаются в статическую структуру, указатель на ко­ торую возвращается пользователю. При каждом вызове затирается информация, полу­ ченная предыдущим вызовом .

# inc lude < sys / di r . h>

/ * местная с труктур а катало га * /

/ * readdi r : чтение записей в каталоге по порядку * / Di rent * readdi r ( D I R * dp )

{

s t ruct direct dirbuf ; / * местная структура каталога * / stat i c Di rent d ; / * перено симая с труктура * / whi l e ( read ( dp - > f d ,

( char * ) &dirbuf , s i zeof ( di rbuf ) ) s i zeof ( di rbuf ) ) { i f ( di rbuf . d_ino == О ) / * ячейка не занята * / cont inue ; d . ino = di rbuf . d ino ; s t rncpy ( d . name , d i rbuf . d name , D I R S I Z ) ; d . name [ D I RS I Z ] = ' \ 0 ' ; / * гарантированный конец * / return &d ; ==

return NULL ; Хотя программа f s i z e достаточно узко специализирована, она все-таки иллюстри­ рует несколько важных идей. Во-первых, многие программы не являются "системными программами"; они просто пользуются информацией, которую хранит и предоставляет операционная система. В таких программах очень важно, чтобы способ представления информации был целиком описан в стандартных заголовочных файлах, которые под­ ключаются к программе; объявления и определения не должны включаться в текст про­ граммы непосредственно. Во-вторых, при известной сноровке можно разработать интер­ фейс к системно-зависимым объектам, который сам по себе будет относительно независим от системы. Функции стандартной библиотеки могут служить хорошим примером этого.

И НТЕРФЕЙС СИСТЕМ Ы UNIX

1 95

Упражнение 8.5 . Переработайте программу f s i z e так, чтобы она выводила другую

информацию из файловых индексов.

8. 7. Пример распредел ени я пам ят и В главе 5 была представлена функция распределения памяти с очень ограниченными возможностями, основанная на работе со стеком. Версия, которая будет представлена здесь, не имеет ограничений. В ызовы функций ma l l oc и f re e могут выполняться в любом порядке ; ma l l o c обращается к операционной системе, запрашивая у нее допол­ нительную память по мере необходимости . Эти функции иллюстрируют некоторые со­ ображения, касающиеся написания системно-зависимого кода в сравнительно системно­ независимой манере, а также демонстрируют реальное практическое применение струк­ тур, объединений и операторов type de f . Вместо выделения памяти из массива фиксированной длины, объявленного еще при компиляции, функция ma l l oc будет запрашивать память у операционной системы по мере возможности . Поскольку в ходе других операций в программе также может запра­ шиваться память без помощи этой функции распределения памяти, управляемое ma l l oc пространство может оказаться не непрерывным. Таким образом, свободная память будет храниться в виде списка свободных блоков. Каждый блок содержит свой размер, указа­ тель на следующий блок и сам участок памяти. Блоки хранятся в порядке возрастания адресов, а последний блок (с самым старшим адресом) ссылается на первый.



Спи сок свободнь1х фра гм ентов

::::::: l ··- 1

l'" r;§'T::::::::: 1 --1141:: : :

О 1 Занято 1 � �

Свободно , принадлежит ma ll oc З ан ято, принадnежит mа ll ос

Не принадnежит mа l l ос

Когда поступает запрос на память, выполняется перебор списка свободных блоков, пока не обнаруживается достаточно большой блок. Этот алгоритм кратко называется "первый подходящий" - в отличие от "наилучшего подходящего", в котором отыскива­ ется наименьший из блоков, подходящих по размеру. Если блок имеет точно такой раз­ мер , как запрашивается, он отсоединяется от списка и передается пользователю. Если блок имеет больший размер, он делится на части , и пользователь получает столько, сколько просил, а остаток остается в списке. Если не удается найти достаточно большой блок, у операционной системы запрашивается очередной большой фрагмент памяти, ко­ торый подключается к списку. При освобождении также выполняется поиск по списку свободных блоков: отыскива­ ется место для вставки освобождаемого блока. Если освобождаем ый блок вплотную гра-

1 96

ГЛАВА 8

ничит со свободным блоком с какой-либо из двух сторон, то он сливается с ним в еди­ ный блок большего размера, чтобы не слишком фрагментировать память. Определить, как граничат блоки, нетрудно, поскольку список свободных блоков организован по воз­ растанию адресов. Одна проблема, на наличие которой мы намекали в главе 5, заключается в том, чтобы обеспечить правильное выравнивание блока памяти, получаемого от ma l l o c , для хра­ нения помещаемых в нее объектов. Различные системы устроены по-разному, но в каж­ дой есть самый "требовательный" тип данных: если элемент данных этого типа можно поместить по определенному адресу, то любые другие элементы также можно поместить туда. В некоторых системах самый требовательный тип - douЫ e , тогда как в других достаточно int или l ong. Свободный блок содержит указатель на следующий блок в цепочке, размер блока и собственно участок свободной памяти ; управляющая информация в начале называется "заголовком''. Чтобы упростить выравнивание, все блоки по длине кратны размеру заго­ ловка, а сам заголовок выровнен надлежащим образом. Это делается с помощью объеди­ нения, содержащего желаемую структуру заголовка и экземпляр самого требовательного типа данных, которым мы назначили l ong:

typede f l ong Al ign ;

/ * для выр авнивания п о гр а нице l ong * /

/* заголовок блока * / un i on header s t ruct { union header *pt r ; / * следующий бло к , если е с т ь * / uns igned s i z e ; / * р а змер э т о г о бло ка * / s· Al ign х ; / * принудител ь н о е выр а в нивание блоков * / '

};

typede f uni on header Heade r ; Поле Al i gn никогда не используется; его наличие просто выравнивает каждый заго­ ловок принудительно по границе самого требовательного типа. В функции ma l l oc запрашиваемое количество памяти (в символах) округляется до размера, кратного длине заголовка. Выделяемый участок содержит на один такой блок больше, чем нужно, - для помещения заголовка. Именно эта длина и записывается в поле s i z е заголовка. Указатель, возвращаемый из ma l l о с , указывает на свободное пространство, а не на заголовок. Пользователь может делать с полученным участком па­ мяти все, что захочет, но если что-либо будет записано за пределами выделенной памяти , весь список скорее всего будет испорчен . ----� Указы вает на следующий свободн ый бп о к Размер

t

...___

Адрес , возвращаемый попьзоватепю

Поле длины, s i z e , необходимо, поскольку блоки, распределяемые функцией ma l l o c , не обязаны занимать сплошную область, - поэтому вычислить их длину с по­ мощью адресной арифметики невозможно. Для начала работы используется переменная b a s e . Если f reep равен NULL, как это имеет место при первом вызове ma l l o c , создается вырожденный список свободных

И НТЕРФ ЕЙС СИСТЕМЫ UNIX

1 97

блоков. Он содержит один блок нулевого размера и указывает сам на себя. В любом слу­ чае затем выполняется поиск по списку. Поиск свободного блока подходящей длины на­ чинается в точке ( f re ep), в которой был найден последний запрошенный блок; такая схема позволяет поддерживать единообразие работы со списком. Если найден слишком длинный блок, пользователю возвращается его "хвост" ; благодаря этому в заголовке ис­ ходного блока остается исправить только размер. Во всех случаях указатель, возвращае­ мый пользователю, указывает на свободное пространство в блоке, которое начинается через один блок после заголовка.

/ * пус т ой список для начала * / stat i c Header base ; / * начало списка * / stat i c Header * f reep = NULL ; / * ma l l oc : функция распределения памя ти * / vo i d * ma l l oc ( uns igned nbytes )

{

Header *р , *prevp ; Header *morecore ( unsigned ) ; uns igned nuni t s ; nun i t s = ( nbyte s + s i zeof ( Heade r ) - 1 ) / s i zeof ( Heade r ) + 1 ; i f ( ( prevp = f reep ) = = NULL ) { / * списка еще нет * / base . s . pt r = f reep = prevp = &bas e ; base . s . s i z e = О ; for ( р = prevp - > s . pt r ; ; prevp = р , р = p - > s . pt r ) { i f ( p - > s . s i z e > = nun i t s ) { / * до с т а т очный размер * / i f ( p - > s . s i z e = = nunit s ) / * в точн о с ти * / prevp - > s . pt r = p - > s . pt r ; / * отрез аем " хв о с т " * / e l se { p - > s . s i z e - = nuni t s ; р + = p - > s . s i ze ; p - > s . s i z e = nuni t s ;

}

f reep = prevp ; return ( vo i d * ) ( p + l ) ;

i f ( р = = f reep ) / * ссылае т с я на сам список * / i f ( ( р = morecore ( nuni t s ) ) = = NULL ) re turn NULL ; / * не о с т ало с ь памя ти * /

Функция morecore запрашивает и получает память от операционной системы. Де­ тали реализации такого запроса меняются от системы к системе. Запрос памяти у систе­ мы - это сравнительно трудоемкая операция, и ее не стоит выполнять при каждом вы­ зове ma l l o c . Вот почему функция mor e co re запрашивает сразу как минимум NALLOC блоков; полученный большой блок потом будет "нарезаться" по мере необходимости. После установки поля размера morecore добавляет полученную память в общий фонд, вызывая функцию f r e e . Системный вызов Unix под названием s b rk ( n ) возвращает указатель на n дополнительных байт памяти . Эта функция возвращает - 1 , если места в памяти недос­ таточно, хотя возвращать NULL в этом случае было бы удачнее. Число - 1 следует при­ вести к типу char * , чтобы можно было сравнивать его с возвращаемым значением.

1 98

ГЛАВА 8

Приведение типов делает функцию сравнительно устойчивой к различиям представлений указателей в разных системах. Правда, здесь сделано предположение о том, что указате­ ли на различные блоки, возвращаемые из s brk, можно сравнивать и получать при этом правильные ответы. Стандарт этого не гарантирует, поскольку разрешает сравнивать указатели только в пределах массива. Поэтому данная версия ma l l oc переносима толь­ ко между системами, в которых сравнение произвольных указателей имеет смысл и реа­ лизовано корректно.

#def ine NALLOC

1 0 2 4 /* минимально запрашиваемое количество блоков * /

/ * morecore : запрос дополнительной памяти у сист емы * / stat i c Header *morecore ( uns igned nu ) { char * ер , * sbrk ( int ) ; Header *up ; i f ( nu < NALLOC ) nu = NALLOC ; ер = sbrk ( nu * s i zeof ( Heade r ) ) ; i f ( ер == ( char * ) - 1 ) / * ме ста в памяти нет * / return NULL ; up = ( Header * ) ер ; up - > s . s i z e = nu ; f ree ( ( void * ) ( up + l ) ) ; return freep ; И последней в нашем наборе функций идет f re e . Она перебирает список свободных блоков, начиная с freep, в поисках места для вставки освобождаемого блока. Такое ме­ сто может найтись либо между двумя блоками, либо на одном из концов списка. В лю­ бом случае, если освобождаемый блок примыкает вплотную к одному из соседей, сосед­ ние блоки объединяются в один. Единственное, о чем следует тщательно позаботить­ ся, - это о том, чтобы указатели указывали на правильные места и были заданы правильные размеры .

/ * f ree : помещение блока ар в список свободных блоков * / void f ree ( voi d * ар ) { Header *Ьр *р ; ,

Ьр = ( Header * ) ар - 1 ; / * указ . на за голо вок * / for ( р = f reep ; ! ( Ьр > р & & Ьр < p - > s . pt r ) ; р = p - > s . pt r ) i f ( р > = p - > s . pt r && ( Ьр > р 1 1 Ь р < p - > s . pt r ) ) break ; / * освобождаемый блок в начале или в конце * / i f ( Ьр + bp - > s . s i z e = = p - > s . pt r ) { / * к верхнему со седу * / bp - > s . s i z e + = p - > s . pt r - > s . s i z e ; p - > s . pt r - > s . pt r ; bp - > s . ptr else bp - > s . ptr p - > s . pt r ; / * к нижнему со седу * / i f ( р + p - > s . s i z e = = Ьр ) { p - > s . s i z e + = bp - > s . s i ze ; p - > s . pt r = bp - >s . pt r ; else И НТЕРФ ЕЙС СИСТЕМЫ UNIX

1 99

p - >s . pt r freep = р ;

Ьр ;

Хотя распределение памяти - по самой своей сути всегда системно-зависимая опе­ рация, приведенный код иллюстрирует, как эту зависимость можно сделать управляемой и локализовать в очень малой части программы. Выравнивание регулируется с помощью typede f и объединений (при условии, что s brk возвращает правильный указатель). Приведение типов, применяемое к указателям, сделано явным и даже приспособлено к неудачному системному интерфейсу. Хотя подробности реализации приведенных функ­ ций относятся к распределению памяти, общий подход можно применять и к решению других проблем. Упражнение 8.6. Стандартная библиотечная функция c a l loc ( n , s i z e ) возвращает указатель на n объектов размера s i z e и инициализирует участок памяти нулями. Напи­ шите функцию c a l l oc, либо просто вызывая ma l l o c, либо доработав ее. Упражнение 8.7. Функция ma l l oc принимает запрос на участок памяти определенного размера, не проверяя разумность этого размера; в функции f re e предполагается, что ос­ вобождаемый блок содержит корректно заданное поле размера. Доработайте эти функ­ ции так, чтобы они более тщательно проверяли возможные ошибки.

Напишите функцию Ь f ree ( р , n ) , которая бы освобождала произ­ вольный блок р из n символов и добавляла его в список свободных блоков, которым управляют функции ma l l oc и f ree. С помощью Ь f ree пользователь всегда сможет добавить к списку свободных блоков любой статический или внешний массив. Упражнение 8.8.

200

ГЛАВА 8

П ри л оже н и е А

Спр а во ч но е ру ководс тво по яз ы ку с

А. 1 . В ведение

Данное руководство описывает язык С в том виде, в каком о н определен проектом документа "American National Standard for Information Systems - Programming Language С, ХЗ . 1 59- 1 989" ("Американский государственный стандарт информационных систем язык программирования С, ХЗ . 1 59- 1 989"), поданного на утверждение в комитет ANSI 31 октября 1 988 года. Справочник является интерпретацией предложенного стандарта, но не самим стандартом, хотя нами были приложены все усилия, чтобы сделать его на­ дежным пособием по языку. Данный документ в основном следует схеме изложения стандарта, который в свою очередь построен аналогично первой редакции этой книги, хотя детали могут отличать­ ся. Представленная здесь грамматика основного ядра языка полностью совпадает со стандартом, если не считать отличий в наименованиях нескольких правил, а также отсут­ ствия строгих определений лексических единиц препроцессора. В данном справочнике повсеместно встречаются комментарии, оформленные так, как этот аб зац. Чаще всего эти комментарии поясняют различия между стандартом ANSI языка С и его определением, данным в первой редакции этой книги (или усовершенствованиями, вне­ сенными впоследствии в различных компиляторах).

А. 2 . Лексические соглашения

Программа состоит из одной или нескольких единиц трансляции (translation units), хранящихся в виде файлов. Она проходит несколько этапов трансляции, описанных в разделе А. 1 2. На начальных этапах осуществляются лексические преобразования низко­ го уровня, выполняются директивы, заданные в программе строками, начинающимися с символа # , обрабатываются и раскрываются макроопределения. По завершении работы препроцессора (раздел А. 1 2) программа представляется в виде последовательности лек­ сем (tokens).

А. 2 . 1 . Лексемы Существует шесть классов лексем : идентификаторы , ключевые слова, константы, строковые литералы, знаки операций и прочие разделители. Пробелы, горизонтальные и

вертикальные табуляции, символы конца строки, символы прогона страницы и коммен­ тарии (имеющие общее название "символы пустого пространства") игнорируются; они рассматриваются компилятором только как разделители лексем . Такие символы необхо­ димы, чтобы отделить друг от друга соседние идентификаторы, ключевые слова и кон­ станты. Если входной поток уже разбит на лексемы до некоторого заданного символа, то следую­ щей лексемой будет самая длинная строка символов, которые могут составлять лексему.

А. 2 . 2 . Комментарии Комментарий начинается с символов / * , а заканчивается символами * / . Коммента­ рии нельзя вкладывать друг в друга, а также помещать внутри строковых или символь­ ных литеральных констант.

А. 2 .3. Иденти ф икаторы Идентификатор - �то последовательность букв и цифр. Первым символом должна быть буква; знак подчеркивания (_) считается буквой. Буквы нижнего и верхнего регист­ ров различаются. Идентификаторы могут иметь любую длину; для внутренних иденти­ фикаторов значимыми являются не менее 3 1 первого символа; в некоторых реализациях принято большее количество значимых символов. К внутренним идентификаторам отно­ сятся имена препроцессорных макросов и все остальные имена, не имеющие внешних связей (раздел А. 1 1 .2). На идентификаторы с внешними связями могут накладываться более строгие ограничения: в конкретных реализациях языка могут восприниматься не бо­ лее шести первых символов, а также не различаться буквы верхнего и нижнего регистров.

А. 2 .4. Ключевые слова Следующие идентификаторы зарезервированы в качестве ключевых слов и никаким другим способом использоваться не могут: auto

douЫ e

break

else

long

swi tch

case

enum

reg i s t e r

typede f

char

ext e rn

return

union

const

f l oat

short

uns i gned

int

s t ruct

cont i nue

for

s i gned

voi d

de f au l t

goto

s i z eof

volat i l e

do

if

stat i c

whi l e

В некоторых реализациях зарезервированы также слова f o rt ran и a sm. Ключевые слова cons t , s i gned и vo l a t i l e впервые появились в стандарте ANSI; enum и vo i d - не использовались в первом издании книги, но уже вошли в употреб­ ление; ранее зарезервированное слово ent ry нигде не использовалось и поэтому больше не является ключевым словом .

202

П РИЛ ОЖЕНИЕ А

А. 2 . 5. Константы Существует несколько видов констант. Каждая имеет свой тип данных ; базовые типы рассматриваются в разделе А.4.2. константа : целочисленная-конста нта симв ольная- константа константа - с - плавающей- точкой константа - пере числимого- типа

А.2 . 5 . 1 . Целочисленные константы Целочисленная константа, состоящая из последовательности цифр, воспринимается как восьмеричная, если она начинается с О (цифры "нуль"), и как десятичная в против­ ном случае. Восьмеричные константы не содержат цифр 8 и 9. Последовательность цифр, перед которой стоит Ох или ОХ (также цифра "нуль"), считается шестнадцатерич­ ным целым числом. В число шестнадцатеричных цифр включены буквы от а (или А) до f (или F) со значениями соответственно от 1 0 до 1 5 . Целочисленная константа может записываться с суффиксом и (или U ) ; в этом случае она считается константой без знака. Она также может иметь суффикс 1 (или L), указы­ вающий на тип l ong. Тип целочисленной константы зависит от формы ее записи, значения и суффикса. (Типы подробнее освещены в разделе А.4.) Если константа - десятичная и без суффик­ са, то она будет иметь первый из следующих типов, которого достаточно для представ­ ления ее значения: int, l ong int, uns i gned l ong int. Восьмеричная или шестна­ дцатеричная константа без суффикса принимает первый подходящий из следующего списка типов: int, uns igned int, l ong int, uns igned l ong int. Если констан­ та имеет суффикс и или U, она принимает первый подходящий из типов uns i gned i nt и uns i gned l ong int. Если константа имеет суффикс 1 или L, то список допусти­ мых типов состоит из l ong int и uns i gned l ong i n t . Типы целочисленных констант были существенно доработаны и развиты по сравнению с первой редакцией языка, в которой большие целые числа имели просто тип l ong. Суффиксы U введены впервые.

А. 2.5.2. Символьные константы Символьная константа - это последовательность из одного или нескольких симво­ лов, заключенная в одинарные кавычки (например, ' х ' ) . Значение символьной констан­ ты, состоящей из одного символа, равно числовому коду символа в символьном наборе, принятом в системе в момент выполнения программы. Значение константы из несколь­ ких символов зависит от реализации языка. Символьные константы не могут содержать одинарную кавычку ( ' ) или символ кон­ ца строки. Чтобы изобразить их и некоторые другие символы, используются управляю­ щие последовательности или специШ1ьные символы (escape seqиeпces). Конец строки Горизонтал ьная табуляция Верт икал ьн ая табуляция

NL ( LF )

\n

нт

\t



\v

СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

203

Возврат н а один символ н азад В озврат каретк и П ро гон ст ра ницы Звуковой си гнал

BS



CR

\r

FF

\f

BEL



О брат ная косая черта

\

\\

Вопросител ьный зна к

?

\? \'

Один арн ая кав ыч ка Двойн ая ка в ыч ка Вос ьмеричный код символа Ш естнадцатеричный код символ а

\" ООО

\ ооо

hh

\ xhh

Управляющий символ \ ооо начинается с обратной косой черты, за которой следуют одна, две или три восьмеричные цифры, воспринимаемые как код символа. Самым рас­ пространенным примером такой конструкции является \ О (без дальнейших цифр); она обозначает символ NULL. Управляющая последовательность \ xhh состоит из обратной косой черты с буквой х, за которыми следуют шестнадцатеричные цифры, восприни­ маемые как код символа. Количество цифр формально не ограничено, но результат будет не определен, если код получившегося символа превысит самый большой из допустимых символов. Если в данной реализации тип char считается числом со знаком, то значения и в восьмеричных, и в шестнадцатеричных управляющих последовательностях получа­ ются путем расширения знака, как если бы выполнялась операция приведения к типу char. Если после обратной косой черты не следует ни один из перечисленных выше символов, результат будет не определен. В некоторых реализациях имеется расширенный набор символов, который не может быть представлен только типом char. Константа для такого набора записывается с бук­ вой L впереди (например, L ' х ' ) и называется расширенной символьной константой. Та­ кая константа имеет целочисленный тип wchar_ t, определенный в стандартном заголо­ вочном файле < s tdde f . h > . Как и в случае обычных символьных констант, здесь также возможны восьмеричные и шестнадцатеричные специальные символы; если заданное кодовое значение выходит за пределы типа wchar_ t, результат будет не определен. Некоторые из приведенных управляющих последовательностей (в частности, шестна­ дцатеричные) являются новыми для языка. Новым является и расширенный тип симво­ лов. Наборы символов, о бычно используемые в США и Западной Европе, удобно коди­ ровать типом c h a r , тогда как тип w c h a r_t был введен главным образом для азиат­ ских языков.

А. 2 . 5 . 3 . Вещественные константы с плавающей точкой Вещественная константа с плавающей точкой состоит из целой части, десятичной точ­ дробной части, символа е или Е, целого показателя степени с необязательным знаком и необязательного суффикса типа - одной из букв f, F, l или L. Целая и дробная части со­ стоят из последовательностей цифр. Может отсутствовать либо целая, либо дробная часть (но не обе сразу). Тип определяется суффиксом: F или f обозначают тип f l oat, L или l - тип l ong douЫ e . При отсутствии суффикса принимается тип douЫ e . ки,

Суффиксы вещественных констант введены в новом стандарте языка; первонач ально они не существовали.

204

ПРИЛ ОЖЕНИЕ А

А. 2.5.4. Константы перечислимых типов Идентификаторы, объявленные в составе перечислимого типа (см. раздел А . 8 .4), яв­ ляются константами типа int.

А. 2 . 6 . Строковые литералы (константы) Строковый литерал, который также называется строковой константой, - это после­ довательность символов, заключенная в двойные кавычки ( 11 11 ) . Такая строка имеет тип "массив символов" и класс памяти s t at i c (раздел А.4) и инициализируется задан­ ными символами. Представляются ли одинаковые строковые литералы одним фактиче­ ским экземпляром строки в памяти или несколькими, зависит от реализации языка. Ре­ зультат работы программы, пытающейся изменить строковый литерал, не определен. Написанные слитно строковые литералы сцепляются (конкатенируются) в одну стро­ ку. После любой конкатенации к строке добавляется нулевой байт ( \ О ), что позволяет программам, просматривающим строку, находить ее конец. Строковые литералы не мо­ гут содержать символ конца строки или двойную кавычку; для представления таких сим­ волов нужно использовать те же управляющие последовательности, что и в символьных константах. Как и в случае символьных констант, строковый литерал с символами из расширен­ ного набора должен начинаться с буквы L (L 11 11 ). Строковый литерал из расширен­ ного набора имеет тип "массив элементов wchar_ t". Результат конкатенации обычных и расширенных строковых литералов друг с другом не определен. •











То, что строковые литералы не обязательно представляются разными экземплярами в памяти, запрет на их модификацию, а также конкатенация слитно записанных строко­ вых литералов - нововведения стандарта ANSI. Расширенные строковые литералы также введены впервые.

А. З . Система си н таксических о б означений В системе формальной записи синтаксиса, используемой в этом справочнике, синтак­ сические категории набираются курсивом, а слова и символы, читаемые буквально, м о н ошири н н ым шрифтом. Альтернативные конструкции обычно перечисляются в стол­ бик, каждый вариант - в отдельной строке; в ряде случаев длинный набор близкородст­ венных альтернативных вариантов располагается в одной строке, помеченной словами "один из". Необязательный терминальный (основной) или нетерминальный (вспомогательный) символ грамматики помечается индексом "необ" . Так, следующая за­ пись обозначает выражение, заключенное в фигурные скобки, которое в общем случае может и отсутствовать: выражениенеоб

}

Формализованное описание синтаксиса приведено в разделе А. 1 3 . отличие от грамматики, приведенной в первом издании этой книги, данная здесь грамматика явно описывает приоритет и ассоциативность операций в выражениях.

В

СП РАВОЧНОЕ РУКО В ОДСТ В О ПО ЯЗ Ы КУ С

205

А . 4 . У потр е бл ени е идент и ф и като ров Идентификаторы, или имена, обозначают различные компоненты программы: функ­ ции; метки структур, объединений и перечислений; элементы структур или объединений; имена новых типов в typede f ; объекты. Объектом (называемым иногда переменной) является участок памяти, интерпретация которого в программе зависит от двух главных характеристик: ее класса памяти и типа. Класс памяти определяет время жизни памяти, ассоциированной с обозначаемым объектом, а тип определяет, какой смысл вкладывает­ ся в данные, находящихся в объекте. С именем также ассоциируются своя область види­ мости или действия (т.е. тот участок программы, где это имя известно) и способ связы­ вания, определяющий, обозначает ли это имя тот же самый объект или функцию в дру­ гой области действия. Области действия и способы связывания рассматриваются в разделе А. 1 1 .

А.4. 1 . Классы памяти Существуют два класса памяти: автоматический и статический. Класс памяти объекта задается рядом ключевых слов в совокупности с контекстом объявления этого объекта. Автоматические объекты локальны в блоке (раздел А.9.3) и при выходе из него уничто­ жаются. Объявления внутри блока создают автоматические объекты, если в них отсутст­ вуют указания на класс памяти или указан модификатор auto. Объекты, объявленные с ключевым словом regi s t e r, являются автоматическими и размещаются (по возможно­ сти) в быстро доступных регистрах процессора. Статические объекты могут быть локальными в блоке или внешними по отношению ко всем блокам, но в обоих случаях их значения сохраняются после выхода из блока или функции до повторного входа. Внутри блока, в том числе и в теле функции, статические объекты объявляются с ключевым словом s t a t i c . Объекты, объявляемые вне блоков на одном уровне с определениями функций, всегда являются статическими. С помощью ключевого слова s t at i c их можно сделать локальными в пределах единицы трансляции; в этом случае для них устанавливается внутреннее связывание. Они становятся глобальными для всей программы, если опустить явное указание класса памяти или использовать ключе­ вое слово ext e rn; в этом случае для них устанавливается внешнее связывание.

А.4. 2 . Базовые типы Существует несколько базовых типов. Стандартный заголовочный файл < l imi t s . h>, описанный в приложении Б, содержит определения самых больших и самых малых зна­ чений для каждого типа в данной конкретной реализации языка. В приложении Б приве­ дены минимально допустимые величины. Размер объектов, объявляемых как символы ( char ), позволяет хранить любой сим­ вол из символьного набора, принятого в системе во время выполнения программы. Если в объекте типа char хранится действительно символ из данного набора, то его значение эквивалентно коду этого символа и неотрицательно. Переменные типа char могут со­ держать и другие значения, но тогда диапазон их значений и особенно вопрос о том, имеют ли эти значения знак, определяется конкретной реализацией языка.

206

П РИЛ ОЖЕНИЕ д

·

Символы без знака, объявленные с помощью ключевых слов uns igned char, за­ нимают столько же памяти, сколько и обычные символы, но всегда неотрицательны. Аналогично, с помощью ключевых слов s i gned char можно явно объявить символы со знаком, которые занимают столько же места, сколько и обычные символы. Тип uns igned char отсутствовал в первой редакции этой книги, хотя давно вошел в широкий обиход. Тип s igned char - новый.

Помимо char, разрешено реализовать еще три целочисленных типа трех различных размеров: short int, int и l ong i nt . Обычные объекты типа int имеют естест­ венный размер, принятый в архитектуре конкретной системы, другие размеры предна­ значены для специальных целей. Более длинные целые типы имеют как минимум не меньшую длину в памяти, чем более короткие, однако в некоторых реализациях обычные целые типы могут быть эквивалентны коротким ( s hort) или длинным ( l ong). Все типы int по умолчанию представляют числа со знаком, если не оговорено обратное. Целые числа без знака объявляются с помощью ключевого слова uns igned и под­ чиняются правилу автоматического взятия остатка от деления 2n, где п - количество би­ тов в представлении числа. Следовательно, в арифметике целых чисел без знака никогда не бывает переполнения. Множество неотрицательных значений, которые могут хра­ ниться в объектах со знаком, является подмножеством значений, которые могут хранить­ ся в соответствующих объектах без знака; представления таких чисел со знаком и без знака совпадают. Любые два из вещественных типов с плавающей точкой - с одинарной ( f loat), с двойной (douЫ e) и с повышенной ( l ong douЫ e) точностью - могут быть синони­ мичными (обозначать одно и то же), но каждый следующий тип в этом списке должен обеспечивать точность по крайней мере не хуже предыдущего. Тип l ong douЫ e введен в новом стандарте. В первой редакции синонимом для douЫ e был тип l ong f l oat, который теперь изъят из обращения.

Перечисления или перечислимые типы (епитеrаtiопs) - единственные в своем роде целочисленные типы; с каждым перечислением связывается набор именованных кон­ стант (раздел А.8.4). Перечисления по свойствам аналогичны целочисленным типам, но компилятор обычно выдает предупреждение, если объекту некоторого перечислимого типа присваивается значение, отличное от одной из его констант, или выражение его же типа. Поскольку объекты этих типов можно воспринимать как числа, такие типы будем на­ зывать арифметическими. Типы char и int всех размеров, каждый из которых может иметь или не иметь знака, а также перечислимые типы собирательно называют целочис­ ленными (iпtegral) типами. Типы f l oa t , douЫ e и l ong douЫ e называются вещественными или типами с плавающей точкой (floatiпg types). Тип vo id обозначает пустое множество значений. Он используется как тип "возвращаемого" функцией значения в том случае, когда функция не возвращает ничего.

А.4.3. П роизводные типы Наряду с базовыми типами существует практически бесконечный класс производных типов, конструируемых из базовых типов следующими способами: •

как массивы объектов определенного типа;

СП РАВОЧНО Е РУКОВОДСТВО ПО ЯЗЫКУ С

207



как функции, возвращающие объекты определенного типа;



как указатели на объекты определенного типа;



как структуры , содержащие последовательности объектов различных типов;



как объединения, способные содержать один объект из заданного множества не­ скольких объектов разных типов.

В общем случае эти методы создания новых объектов могут применяться последова­ тельно, в сочетаниях друг с другом.

А.4.4. М оди ф икаторы типов Тип объекта может дополняться модификатором (qиalifier). Объявление объекта с модификатором con s t указывает на то, что его значение далее не будет изменяться; объявление объекта как vo l a t i l e указывает на его особые свойства для выполняемой компилятором оптимизации. Ни один из модификаторов не влияет на диапазоны значе­ ний и арифметические свойства объектов. Модификаторы рассматриваются более под­ робно в разделе А.8.2.

А . 5 . Объект ы и именующие в ыражения Объект это именованная область памяти; именующее выражение (lvalue) это выражение, обозначающее объект или ссылающееся на него. Очевидным примером име­ нующего выражения является идентификатор с соответствующим типом и классом па­ мяти. Существуют операции, которые выдают именующее выражение в качестве резуль­ тата. Например, если Е выражение типа "указатель", то * Е именующее выражение для объекта, на который указывает Е. Термин lvalue образован от leftside value ("левостороннее значение") в контексте записи присваивания El Е 2 , в которой левый операнд Е 1 должен быть как раз именующим выражением, чтобы присваивание было допустимым. При описании тех или иных операций мы сообщаем, требуют ли они име­ нующих выражений в качестве операндов и выдают ли они такое выражение в качестве результата. -

-

-

-

=

А . 6 . Пре о браз о вани я тип о в

Некоторые операции в зависимости от своих операндов могут инициировать преобра­ зование значений из одного типа в другой . В этом разделе рассказывается, каких резуль­ татов следует ожидать от таких преобразований. В разделе А.6.5 формулируются прави­ ла преобразований для большинства обычных операций; эти правила дополняются и конкретизируются при рассмотрении каждой отдельной операции.

208

П РИ Л ОЖЕНИЕ А

А. 6 . 1 . Расширение целочисленных типов Символ, короткое целое число, целочисленное битовое поле, а также объект перечис­ лимого типа - все они, со знаком или без, могут использоваться в выражениях везде, где разрешено применение целых чисел. Если тип int позволяет представить все значе­ ния исходного типа операнда, то операнд приводится к int, в противном случае - к uns igned int. Эта процедура называется расширением целочисленного типа (integral promotion).

А. 6 . 2 . П реобразование целочисленных типов Чтобы привести целое число к некоторому заданному типу без знака, ищется конгру­ энтное (т.е. имеющее то же двоичное представление) наименьшее неотрицательное зна­ чение, а затем получается остаток от деления его на UМAX+ l , где UМАХ - наибольшее число в данном типе без знака. При использовании двоичного представления в дополни­ тельном коде ("дополнения к двойке") для этого либо отбрасываются лишние левые раз­ ряды, если двоичное представление беззнакового типа уступает в длине исходному типу, либо недостающие старшие разряды заполняются нулями (в числах без знака) или значе­ нием знака (в числах со знаком), если тип без знака шире исходного. В результате приведения целого числа к знаковому типу его значение не меняется, если оно представимо в этом новом типе; в противном случае результат зависит от реа­ лизации.

А. 6 . 3 . П реобразование целых чисел в вещественные и наоборот При преобразовании из вещественного типа в целочисленный дробная часть числа отбрасывается; если полученное при этом значение нельзя представить величиной за­ данного целочисленного типа, то результат не определен. В частности, не определен ре­ зультат преобразования отрицательных чисел с плавающей точкой в целые числа без знака. Если число преобразуется из целого в вещественное и находится в допустимом диа­ пазоне, но представляется в новом типе недостаточно точно, то результатом будет боль­ шее или меньшее ближайшее значение нового типа. Если результат выходит за границы диапазона допустимых значений, результат не определен.

А. 6. 4 . Вещественные типы При преобразовании из вещественного типа меньшей точности к типу большей точ­ ности число не изменяется. Если же преобразование выполняется от большей точности к меньшей и число остается в допустимых пределах нового типа, то результатом будет большее или меньшее ближайшее значение нового типа. Если результат выходит за гра­ ницы допустимого диапазона, результат не определен.

СП РАВ ОЧНО Е РУКО В ОДСТ В О ПО ЯЗ Ы КУ С

209

А. 6. 5 . Арифметические преобразования Во многих операциях преобразование типов операндов и определение типа результа­ та следуют одной и той же схеме. В результате операнды приводятся к некоторому об­ щему типу, который также является и типом результата. Эта схема включает в себя обычные арифметические преобразован ия . • Во-первых, если какой-либо из операндов имеет тип long douЫ e , то и другой приводится к l ong douЫ e . •

Иначе, если какой-либо и з операндов имеет тип douЫ e, то и другой приводится к douЫ e .



Иначе, если какой-либо и з операндов имеет тип f l oat, то и другой приводится к f l oat.



Иначе, для обоих операндов выполняется расширение целого типа; затем, если один из операндов имеет тип uns i gned l ong int, то и другой преобразуется в uns igned l ong i n t .



Иначе, если один и з операндов имеет тип l ong int, а другой - uns i gned int, то результат зависит от того, представляет ли long int все значения uns igned int ; если это так, то операнд типа uns igned int приводится к long int; если нет, то оба операнда преобразуются в uns igned long int.



Иначе, если один из операндов имеет тип l ong int, то и другой приводится к l ong int.



Иначе, если один из операндов - uns igned int, то и другой приводится к uns igned int.



Иначе, оба операнда имеют тип int . В эту часть языка внесены два изменения. Во-первых, арифметические операции с опе­ рандами типа f l oat теперь могут выполняться с одинарной точностью, а не только с двойной; в первой редакции языка вся арифметика с плавающей точкой имела двойную точность. Во-вторых, более короткий тип без знака в комбинации с более длинным зна­ ковым типом не распространяет свойство отсутствия знака на тип результата; в первой редакции типы без знака всегда доминировали. Новые правила немного сложнее, но до некоторой степени уменьшают вероятность неожиданных э ффектов в комбинациях знаковых и беззнаковых величин. Однако неожиданный результат все же может полу­ читься при сравнении беззнакового выражения со знаковым того же размера.

А.6 .6 . Связь указателей и целых чисел К указателю можно прибавлять (и вычитать из него) выражение целочисленного ти­ па; последнее в этом случае преобразуется так, как описано в разделе А.7.7 при рассмот­ рении операции сложения. Можно вычитать два указателя на объекты одного типа, принадлежащие к одному массиву; результат приводится к целому числу так, как описано в разделе А.7.7 при рас­ смотрении операции вычитания. Целочисленное константное выражение со значением О или такое же выражение, приведенное к типу vo i d * , можно преобразовать в указатель любого типа операциями

210

П РИ Л ОЖ ЕНИЕ А

приведения, присваивания и сравнения. Результатом будет нулевой указатель NULL, ко­ торый равен любому другому нулевому указателю того же типа, но не равен никакому указателю на функцию или объект. Допускаются и некоторые другие преобразования с участием указателей, но в них может присутствовать зависимость результата от реализации. Такие преобразования должны указываться явно - с помощью операции приведения типа (см. разделы А. 7 .5 и А.8.8). Указатель можно привести к целочисленному типу, достаточно большому для его хранения; требуемый размер зависит от реализации. Функция отображения из множества целых чисел в множество указателей также зависит от реализации. Объект целочисленного типа можно явно преобразовать в указатель. Если целое чис­ ло получено из указателя и имеет достаточно большой размер, такое преобразование всегда даст тот же указатель; в противном случае результат зависит от реализации. Указатель на один тип можно преобразовать в указатель на другой тип. Если исход­ ный указатель не ссылается на объект, должным образом выровненный по границам слов памяти, в результате может возникнуть исключительная ситуация, связанная с адреса­ цией. Если требования к выравниванию у нового типа менее строгие или такие же, как у первоначального типа, то гарантируется, что преобразование указателя к другому типу и обратно не изменит его. Само понятие "выравнивания" зависит от реализации, однако в любой реализации объекты типа char предъявляют наименее строгие требования к вы­ равниванию. Как описано в разделе А.6.8, указатель может также преобразовываться в vo i d * и обратно без изменения своего значения. Указатель можно преобразовать в другой указатель того же типа с добавлением или удалением модификаторов (разделы А.4.4, А.8.2) того типа объектов, на которые он ука­ зывает. Новый указатель, полученный путем добавления модификатора, имеет то же зна­ чение, но с дополнительными ограничениями, внесенными новыми модификаторами. Удаление модификатора у объекта приводит к тому, что восстанавливается действие его первоначальных модификаторов, заданных в объявлении этого объекта. Наконец, указатель на функцию можно преобразовать в указатель на функцию друго­ го типа. Вызов функции по преобразованному указателю зависит от реализации; но если указатель снова преобразовать к его исходному типу, результат будет идентичен вызову по первоначальному указателю.

А.6. 7. Тип

vo i d

Значение (несуществующее) объекта типа vo i d никак нельзя использовать, его также нельзя явно или неявно привести к типу, отличному от vo id. Поскольку выражение типа vo i d обозначает отсутствие значения, его можно применять только там, где не требует­ ся значения: например, в качестве выражения-оператора (раздел А.9.2) или левого опе­ ранда операции "запятая" (раздел А. 7 . 1 8) . Выражение можно преобразовать в тип vo i d операцией приведения типа. Например, операция приведения к vo i d применительно к вызову функции, используемому в роли выражения-оператора, подчеркивает тот факт, что результат функции отбрасывается. Тип void отсутствовал в первой редакции этой книги, однако за прошедшее время стал общеупотребительным.

СПРАВОЧНО Е РУКОВОДСТВО ПО ЯЗЫКУ С

21 1

А.6.8 . Указатели на vo i d Любой указатель на объект можно привести к типу vo i d * без потери информации. Если результат подвергнуть обратному преобразованию, получится исходный указатель. В отличие от преобразований "указатель в указатель", рассмотренных в разделе А.6.6, которые требуют явного приведения к типу, указатели типа vo i d * можно употреблять совместно с указателями любого типа в операциях присваивания и сравнения каким угодно образом . Такая интерпретация указателей voi d * введена в новом стандарте; ранее роль нети­ пизированного указателя отводилась указателю типа char * . Стандарт ANSI подчер­ кивает допустимость использования указателей vo id * совместно с указателями дру­ гих типов в операциях присваивания и сравнения; в других сочетаниях указателей стандарт требует явных преобразований типа.

А. 7 . В ыраже н ия

Приоритет знаков операций в языке С совпадает с порядком, в котором следуют под­ разделы данного раздела: вначале идут подразделы операций с более высоким приорите­ том. Например, выражения, являющиеся операндами операции + (раздел А.7.7), опреде­ лены и описаны в разделах А.7. 1-А.7.6. В пределах каждого подраздела операции имеют одинаковый приоритет. В каждом разделе для описываемых операций указывается ассо­ циативность (левая или правая). Описание грамматики в разделе А. 1 3 также включает информацию о приоритетах и ассоциативности операций. Приоритет операций и их ассоциативность указаны здесь полностью, однако порядок вычисления выражений не определен, за некоторыми исключениями, даже для подвыра­ жений, дающих побочные эффекты. Это значит, что если в определении операции специ­ ально не оговаривается та или иная последовательность вычисления ее операндов, то в реализации можно выбирать любой порядок вычислений по желанию и даже чередовать его. Однако любая операция комбинирует значения своих операндов в соответствии с синтаксической структурой выражения, в котором она встречается. Это правило отменяет ранее имевшуюся свободу в порядке выполнения операций, ко­ торые математически коммутативны и ассоциативны, но которые с вычислительной точки зрения могут и не оказаться ассоциативными. Это изменение затрагивает только вы числения вещественной арифметики, выполняющиеся на пределе точности, и ситуа­ ции, ко гда возможно переполнение.

В языке не определена обработка переполнения, деления на нуль и других исключи­ тельных ситуаций, возникающих при вычислении выражений. В большинстве сущест­ вующих реализаций С переполнение игнорируется при вычислении целочисленных вы­ ражений со знаком и присваивании, но в целом такое поведение выполняющей системы стандартом не гарантируется. Обработка деления на нуль и всех исключительных ситуа­ ций вещественной арифметики может отличаться в разных реализациях; иногда для этой цели существует нестандартная библиотечная функция.

21 2

ПРИЛ ОЖЕНИЕ А

А.7. 1 . Генерирование указателей Если выражение или подвыражение имеет тип "массив элементов Т ' , где Т - неко­ торый тип, то значением этого выражения будет указатель на первый элемент массива, и тип такого выражения заменяется на тип "указатель на Т ' . Замена типа не выполняется, если выражение является операндом одноместной операции &, либо операндом операций + + , - - , s i z e o f , либо левым операндом присваивания или операции "точка" ( . ). Анало­ гично, выражение типа "функция, возвращающая Т', кроме случая использования в ка­ честве операнда &, преобразуется в тип "указатель на функцию, возвращающую Т ' .

А.7 . 2 . Первичные выражения Первичные выражения - это идентификаторы, константы, строки и выражения в скобках. первичное - выражение : идентифика тор константа строка ( выражение

Идентификатор является первичным выражением, если он был должным образом объявлен; как это делается, рассказано ниже. Тип идентификатора указывается в его объявлении. Идентификатор является именующим выражением (lvalue), если он обозна­ чает объект (раздел А.5) и имеет арифметический тип либо тип "структура", "объединение" или "указатель". Константа - первичное выражение. Ее тип зависит от формы записи, рассмотренной в разделе А.2.5. Строковый литерал - первичное выражение. Его базовый тип - "массив элементов типа char" ("массив элементов типа wchar_t" для строк символов расширенного на­ бора), но в соответствии с правилом, приведенным в разделе А.7. 1 , этот тип обычно пре­ образуется в "указатель на char" (wchar_t ) и в результате фактически указывает на первый символ строки. Для некоторых инициализаторов такая замена типа не выполня­ ется (см. раздел А.8.7). Выражение в скобках - первичное выражение, тип и значение которого совпадают с типом и значением этого же выражения без скобок. Наличие или отсутствие скобок не влияет на то, является ли данное выражение именующим (lvalue) или нет.

А. 7 .3. Постфиксные выражения В постфиксных выражениях операции группируются слева направо. постфик сно е - выражение : первично е - выражение постфиксно е - выражение [ выражение ] постфиксно е - выражение ( спис о к - аргументов - выраженийнеов постфик сно е - выражение . идентифика тор постфик сно е - выражение- >идентифика тор постфиксно е - выражение + + постфик сно е - выражение - -

СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

213

список- аргументов - выражений : выражение - присв аив а ние список- аргументов - выра жений , выражение - присв аив а ние

А.7.3. 1 . О б ра щение к элементам массивов Постфиксное выражение, за которым следует выражение в квадратных скобках, представляет собой постфиксное выражение, обозначающее обращение к индексируе­ мому массиву. Одно из этих двух выражений должно иметь тип "указатель на Т', где Т - некоторый тип, а другое - целочисленный тип; результат обращения по индексу будет иметь тип Т. Выражение E l [ Е 2 ] по определению идентично выражению * ( ( E l ) + ( Е2 ) ) . Подробнее об этом написано в разделе А.8.6.2.

А.7. 3 . 2 . Вызовы функций Вызов функции - это постфиксное выражение, которое также называется именую­ щим обозначением функции (jипсtiоп desigпator), за которым следуют круглые скобки со списком (возможно, пустым) разделенных запятыми выражений-присваиваний (см. раздел А. 7 . 1 7), которые представляют собой аргументы этой функции. Если такое постфиксное выражение представляет собой идентификатор, не объявленный в текущей области действия, то считается, что этот идентификатор объявлен неявно - так, как если бы в самом внутреннем блоке, содержащем вызов функции, находилось бы объявление exte rn int идентифика тор ( ) ; Постфиксное выражение (после возможного неявного объявления и генерирования указателя; см. раздел А. 7 . 1 ) должно иметь тип "указатель на функцию, возвращающую Т', где Т - тип возвращаемого функцией значения. В первой версии языка для именующего обозначения функции допускался только тип "функция'', и, чтобы вызвать функцию через указатель на нее, требовалось явно писать знак * . Стандарт ANSI утвердил практику ряда существующих компиляторов, которые разрешают иметь одинаковый синтаксис для обращения к функции непосредственно и через указатель. Возможность применения старого синтаксиса также остается в силе.

Термин аргумент используется для выражения, передаваемого при вызове функции; термин параметр - для обозначения получаемого ею объекта (или его идентификатора) в объявлении или определении функции. В том же смысле иногда используются термины "фактический аргумент (аргумент)" и "формальный аргумент (параметр)". При подготовке вызова функции создаются копии всех ее аргументов; передача аргу­ ментов выполняется строго по значениям. Функции разрешается изменять значения сво­ их параметров, которые являются только копиями аргументов-выражений, но эти изме­ нения не могут повлиять на значения самих аргументов. Однако в функцию можно пере­ дать указатель, чтобы сознательно позволить ей изменить значение объекта, на который указывает этот указатель. Имеются два способа (стиля) объявления функций. В новом стиле типы параметров задаются явно и являются частью типа функции; такое объявление еще называется про­ тотипом функции. В старом стиле типы параметров не указываются. Способы объявле­ ния функций рассматриваются в разделах А.8.6.3 и А. 1 0. 1 . Если объявление функции составлено в старом стиле, то при вызове этой функции в области действия объявления каждый аргумент подвергается расширению типа: цело­ численные аргументы преобразуются по правилам расширения целых типов

214

П РИ Л ОЖ ЕНИЕ А

(раздел А.6. 1 ), а аргументы типа f l oat - в douЫ e . Если количество аргументов не соответствует количеству параметров в определении функции или если тип какого-либо аргумента после расширения не согласуется с типом соответствующего параметра, резуль­ тат вызова будет не определен. Понятие согласованности типов зависит от стиля определе­ ния функции (старого или нового). При старом стиле сравнивается расширенный тип аргу­ мента в вызове и расширенный тип соответствующего параметра; при новом стиле расши­ ренный тип аргумента должен совпасть с типом самого параметра без расширения. Если объявление функции составлено в новом стиле, то при вызове функции в облас­ ти действия объявления аргументы преобразуются так, как при присваивании - с при­ ведением к типу соответствующих параметров прототипа. Количество аргументов долж­ но совпадать с количеством явно заданных параметров, только если список параметров не заканчивается многоточием ( , . . . ). В последнем случае число аргументов должно быть больше числа параметров или равно ему; аргументы на месте многоточия подвер­ гаются автоматическому расширению типа таким образом, как описано в предыдущем абзаце. Если определение функции составлено в старом стиле, то тип каждого параметра в прототипе, в область действия которого входит вызов функции, должен соответство­ вать типу соответствующего параметра в определении функции после его расширения. Эти правила сильно усложнились из-за того, что они призваны обслуживать смешан­ ный стиль объявления и определения функций. По возможности такого стиля следует избегать.

Порядок вычисления аргументов не определяется стандартом ; в разных компилято­ рах он может отличаться. Однако гарантируется, что аргументы и именующее обозначе­ ние функции вычисляются полностью (включая и побочные эффекты) до входа в нее. Любая функция допускает рекурсивный вызов.

А. 7 3 3 О б ра ще н ие к структурам .

.

.

Постфиксное выражение, после которого стоит точка с последующим идентификато­ ром, является постфиксным выражением. Первый операнд (в том числе как результат вычисления выражения) должен быть структурой или объединением, а идентификатор именем элемента (поля) структуры или объединения. Значение этой конструкции пред­ ставляет собой именованный элемент структуры или объединения, а ее тип - тип эле­ мента структуры или объединения. Выражение является именующим (lvalue), если пер­ вое выражение - именующее, а тип второго выражения - не массив. Постфиксное выражение, после которого стоит стрелка (составленная из знаков и > ) с последующим идентификатором, также является постфиксным выражением. Пер­ вый операнд (в том числе как результат вычисления выражения) должен быть указателем на структуру или объединение, а идентификатор - именем элемента структуры или объ­ единения. Результат является именованным элементом структуры (объединения), на кото­ рую указывает указатель, а его тип будет типом элемента структуры (объединения). Резуль­ тат представляет собой именующее выражение (lvalue), если тип элемента - не массив. Таким образом, выражение E l - >MOS означает то же самое, что и выражение ( * E l ) MOS . Структуры и объединения рассматриваются в разделе А.8.3. .

В первом издании книги уж е приводилось правило, по которому имя элемента должно принадлежать структуре или объединению, упомянутому в постфиксном выражении. Правда, там оговаривалось, что это правило не является строго обязательным. Новейшие компиляторы и стандарт ANSI сделали правило обязательным.

СПРАВОЧНОЕ РУКОВОДСТВО ПО Я ЗЫКУ С

215

А.7. 3 .4. Постфиксное инкрементирование Постфиксное выражение, за которым следует знак + + или - - , также представляет со­ бой постфиксное выражение. Значением такого выражения является значение его опе­ ранда. После того как его значение было использовано, операнд увеличивается ( + +) или уменьшается ( - - ) на единицу. Операнд должен быть именующим выражением (lvalue) ; об ограничениях, накладываемых на операнд, и деталях данных операций речь пойдет в разделе А.7.7, посвященном аддитивным операциям, и в разделе А.7. 1 7, где рассматри­ вается присваивание. Сам результат инкрементирования или декрементирования не яв­ ляется именующим выражением .

А. 7 4 Одноместные операции .

.

Выражения с одноместными операциями группируются справа налево. одноме с тно е - выражение : пос тфиксно е - выражение ++ одноме с тно е - выражение - - одноме с тно е - выражение одноме с тная- опер а ция выражение - прив едения - к - типу s i z eo f одноме с тно е - выражение s i z eo f ( имя - типа ) одноме с тна я- опера ция : один и з &

*

+

А. 7 .4. 1 . Префиксные операции инкрементирования Одноместное выражение, перед которым стоит знак + + или - -, также является одно­ местным выражением . Операция увеличивает ( + + ) или уменьшает ( - - ) свой операнд на единицу. Значением выражения служит результат инкрементирования (или декременти­ рования) . Операнд должен быть именующим выражением (lvalue) ; дополнительные ог­ раничения на операнд и подробности выполнения операции см. в разделе А.7.7 об адди­ тивных операциях и в разделе А. 7 . 1 7 о присваивании. Сам результат операции не являет­ ся именующим выражением.

А.7.4.2. Операция взятия адреса Знак одноместной операции ( &) обозначает получение адреса своего операнда. Опе­ ранд должен быть либо именующим выражением (lvalue), не ссылающимся ни на бито­ вое поле, ни на объект, объявленный как reg i s t e r, либо объектом типа "функция". Ре­ зультат - это указатель на объект или функцию, который обозначается выражением­ операндом. Если операнд имеет тип Т, то типом результата является "указатель на Т'.

А. 7 .4. 3 . Операция разыменования (ссылки по указателю) Знак одноместной операции ( * ) обозначает обращение по указателю (разымено­ вание), дающее объект (в том числе функцию) , на который указывает ее операнд. Резуль­ тат является именующим выражением (lvalue), если операнд - указатель на объект арифметического типа, структуру, объединение или указатель. Если тип выражения "указатель на Т', то типом результата будет Т.

21 6

П РИ Л ОЖ ЕНИЕ А

А. 7 .4.4. Операция "одноместн ый плюс" Операнд одноместной операции ( + ) должен иметь арифметический тип ; операция да­ ет в качестве результата значение операнда. Целочисленный операнд преобразуется по правилам расширения целочисленных типов. Типом результата является расширенный тип операнда. Одноместный плюс был добавлен для симметрии с одноместным минусом .

А.7.4.5. Операция "одноместный мин ус" Операнд для одноместного минуса должен иметь арифметический тип; результатом является значение операнда с противоположным знаком. Целочисленный операнд пре­ образуется по правилам расширения целочисленных типов. Отрицательное значение от числа без знака вычисляется путем вычитания приведенного к расширенному типу опе­ ранда из максимального числа этого расширенного типа плюс единица. Единственное исключение: "минус нуль" равен нулю. Типом результата является расширенный тип операнда.

А. 7 .4.6. Операция вычисления дополнения до единицы (поразрядного отрицания) Операнд операции ( - ) должен иметь целочисленный тип; результатом является до­ полнение операнда до единиц по всем разрядам . Выполняется целочисленное расшире­ ние типа операнда. Если операнд не имеет знака, то результат получается путем вычита­ ния его значения из самого большого числа расширенного типа. Если операнд имеет знак, то результат вычисляется путем приведения расширенного операнда к соответст­ вующему типу без знака, применения операции - и обратного приведения его к типу со знаком. Тип результата - расширенный тип операнда.

А. 7 .4. 7. Операция логического отрицания Операнд операции ( ! ) должен иметь арифметический тип или быть указателем. Ре­ зультат операции равен 1, если значение операнда равно нулю, и О в противном случае. Результат имеет тип int .

А .7.4.8. Операция вычисления размера

s i z eo f

Операция s i z e o f вычисляет число байтов, требуемое для хранения объекта того ти­ па, который имеет ее операнд. Операнд представляет собой либо выражение (которое не вычисляется), либо имя типа, записанное в скобках. Применение операции s i z e o f к ти­ пу char дает l ; для массива результат равняется общему количеству байтов в массиве. Размер структуры или объединения равен числу байтов в объекте, включая байты­ заполнители, которые понадобились бы, если бы из объектов данного типа составлялся массив: дело в том, что размер массива из п элементов всегда равняется произведению п на размер отдельного его элемента. Данную операцию нельзя применять к операнду типа "функция", операнду неполного (заранее объявленного, но не полностью определенного) типа или битовому полю. Результат является беззнаковой целочисленной константой, а ее конкретный тип зависит от реализации. В стандартном заголовочном файле < S t dde f . h> (см. приложение Б) этот тип объявлен под именем s i z e t . СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

217

А.7.5. Приведение типов Имя типа, записанное перед одноместным выражением в круглых скобках, вызывает приведение значения этого выражения к указанному типу. выражение - прив едения - к - типу : одноме с тно е - выражение ( имя - типа ) выражение - прив едения - к - типу

Данная конструкция называется приведением типа (type cast). Имена типов перечис­ лены и описаны в разделе А.8.8. Эффект от приведений типов описан в разделе А.6. Вы­ ражение с приведением типа не является именующим (lvalue).

А. 7 . 6 . Мулыипликативные операции Мультипликативные операции *, / и % группируются слева направо. муль типлика тивное- выражение : выражение - прив едения- к - типу муль типлика тивное- выражение * выражение - прив едения- к - типу муль типлика тивное-выражение / выражение - прив едения- к - типу муль типлика тивное- выражение % выражение - прив едения- к - типу

Операнды операций * и / должны быть арифметического типа, а операнды % це­ лочисленного. Над операндами выполняются обычные арифметические преобразования, которые определяют тип результата. Двухместная операция * обозначает умножение. Двухместная операция / дает частное, а % остаток от деления первого операнда на второй; если второй операнд равен О, то результат не определен. В противном случае выражение ( а / Ь ) * Ь + а % Ь равняется а. Если оба операнда неотрицательны, то остаток неотрицателен и меньше делителя; в противном случае гарантируется только то, что абсолютное значение остатка будет меньше абсолютного значения делителя. -

-

А. 7. 7. Адд и тивные операции Адд итивные операции + и - группируются слева направо. Если операнды имеют арифметический тип, то выполняются обычные арифметические преобразования. Для каждой операции существует еще несколько дополнительных возможностей сочетания типов. аддитивно е - выражение : муль типлика тив ное- выражение аддитивное - выражение + муль типлика тивное- выражение аддитивно е - выражение - муль типлика тивное- выражение

Результатом выполнения операции + является сумма его операндов. Указатель на объект в массиве можно складывать со значением любого целочисленного типа. Послед­ нее преобразуется в адресное смещение посредством умножения его на размер объекта, на который ссылается указатель. Сумма является указателем того же типа, что и исход­ ный указатель, но ссылается на другой объект того же массива, отстоящий от исходного на вычисленное смещение. Так, если Р указатель на объект в массиве, то P + l ука­ затель на следующий объект того же массива. Если полученный в результате сложения -

218

-

П РИ Л ОЖ ЕНИЕ А

указатель указывает за пределы массива, то результат будет неопределенным - кроме случая, когда указатель ссылается на участок памяти, находящийся непосредственно за концом массива. Возможность указывать на элемент, расположенный сразу за концом массива, является новой. Тем сам ы м узаконена ставшая уже стандартной конструкция для циклического перебора элементов массива.

Результатом выполнения операции - (минус) является разность операндов. Из указа­ теля можно вычитать число или выражение любого целочисленного типа с теми же пре­ образованиями и при тех же условиях, что и в сложении. Если к двум указателям на объекты одного и того же типа применить операцию вы­ читания, то в результате получится целочисленное значение со знаком, представляющее собой расстояние между указываемыми объектами; так, указатели на два соседних объ­ екта отличаются на единицу. Тип результата зависит от реализации; в стандартном заго­ ловочном файле < S t dde f . h > такой тип определен под именем ptrdi f f _ t . Значение не определено, если указатели не указывают на объекты одного и того же массива; одна­ ко если Р указывает на последний элемент массива, то выражение ( Р+ 1 ) Р имеет зна­ чение 1 . -

А. 7 .8. Операции сдвига Операции сдвига < < и > > группируются слева направо. Каждый операнд обеих опе­ раций должен иметь целочисленный тип, и каждый из них подвергается расширению ти­ па. Результат имеет тот же тип, что и левый операнд после расширения. Результат не оп­ ределен, если правый операнд отрицателен либо больше или равен количеству битов в типе левого выражения. выражение - с о - сдвигом : аддитивно е - выражение выражение - со - сдвигом < < аддитив но е - выражение выражение - с о - сдвигом > > аддитивно е - выражение

Значение E l < < E 2 равно значению El (воспринимаемому как набор битов), сдвину­ тому влево на Е2 битов; при отсутствии переполнения такая операция эквивалентна ум­ ножению на 2 Е2 • Значение E l > > E2 равно значению E l , сдвинутому вправо на Е2 бито­ вых позиций. Если El - величина без знака или неотрицательное значение, то сдвиг впра­ во эквивалентен делению на 2 Е2 ; в противном случае результат зависит от реализации.

А.7.9. Операции отношения (сравнения) Операции отношения группируются слева направо, н о это свойство малополезно; со­ гласно грамматике языка выражение а < Ь < с воспринимается как ( а < Ь ) < с, а результат вычисления а < Ь может быть равен только О или 1 . выражение - о тношения : выражение - со - сдвигом выражение - о тношения < выражение - с о - сдвигом выражение - о тношения > выражение - с о - сдвигом выражение - о тношения < = выражение - с о - сдвигом выражение - о тношения >= выражение - с о - сдвигом

СПРАВОЧНО Е РУКОВОДСТВО ПО Я ЗЫКУ С

219

Операции < (меньше), > (больше), < = (меньше или равно) и > = (больше или равно) дают результат О, если указанное отношение ложно, и 1 , если оно истинно. Результат имеет тип i n t . Над арифметическими операндами выполняются обычные арифметиче­ ские преобразования. Можно сравнивать указатели на объекты одного и того же типа (без учета модификаторов); результат будет зависеть от относительного расположения указываемых объектов в адресном пространстве. Определено только сравнение указате­ лей на разные части одного и того же объекта: • если два указателя указывают на один и тот же простой объект, то они равны; •

если они указывают на элементы одной структуры, то указатель на элемент с бо­ лее поздним объявлением в структуре - больше;



если указатели указывают на элементы одного и того же объединения, то они равны;



если указатели указывают на элементы некоторого массива, то сравнение этих указателей эквивалентно сравнению соответствующих индексов.

Если Р указывает на последний элемент массива, то P + l больше, чем Р, хотя P + l . указывает з а границы массива. В остальных случаях результат сравнения не определен. Эти правил а несколько ослабили ограничения, установленные в первой редакции язы­ ка, поскольку позволяют сравнивать указатели на различные элементы структуры и объединения . О ни также узаконили сравнение с участием указателя на место, располо­ женное непосредственно за концом массива.

А. 7 . 1 О. Операции проверки равенства выражение -ра в енств а : выражение - о тношения выражение -ра в енства = = выражение - о тношения выражение -рав енства 1 = выражение - о тношения

Операции = = (равно) и ! = (не равно) аналогичны операциям отношения, но имеют более низкий приоритет. (Таким образом, выражение а < Ь = = с < d равно 1 тогда и только тогда, когда отношения а < Ь и с < d одновременно истинны или ложны.) Операции проверки равенства подчиняются тем же правилам, что и операторы отно­ шения, но дают и дополнительные возможности: можно сравнить указатель с целочис­ ленным константным выражением, значение которого равно нулю, или с указателем на vo i d (см. раздел А.6.6).

А. 7 . 1 1 . Операция поразрядного и выражение - с - И : выражение -ра в енства выражение - с - И & выражение -ра в енств а

Выполняются обычные арифметические преобразования; результат представляет со­ бой поразрядное (побитовое) логическое произведение (И) операндов. Операция приме­ няется только к целочисленным операндам.

220

П РИЛ ОЖЕНИЕ А

А.7. 1 2 . Операция поразрядного исключающего или

выражение - с -исключающим- ИЛИ : выражение - с - И выражение - с -исключающим-ИЛИ

А

выражение - с - И

Выполняются обычные арифметические преобразования; результат получается по­ разрядным (побитовым) применением операции исключающего ИЛИ к операндам . Опе­ рация применяется только к целочисленным операндам .

А. 7 . 1 3 . Операция поразрядного включающего или

выражение - с - в ключающим- ИЛИ : выражение - с -исключающим-ИЛИ выражение - с - включающим- ИЛИ 1 выражение - с - исключающим- ИЛИ

Выполняются обычные арифметические преобразования ; результат получается по­ разрядным (побитовым) применением к операндам операции включающего ИЛИ. Опера­ ция применяется только к целочисленным операндам .

А. 7. 1 4 . Операция логического и выражение - с -логическим- И : выражение - с - в ключающим- ИЛИ выражение - с - логическим- И & & выр ажение - с - в ключающим-ИЛИ

Операции & & группируются слева направо. Операция & & дает результат 1 , если оба операнда не равны нулю, и О в противном случае. В отличие от &, операция && гаранти­ рует вычисление выражения слева направо: вначале вычисляется первый операнд со всеми побочными эффектами ; если он равен О, то значение выражения будет равно О. В противном случае вычисляется правый операнд, и если он равен О, то значение выра­ жения будет равно нулю, в противном случае - единице. Операнды могут иметь разные типы, но каждый из них должен быть либо величиной арифметического типа, либо указателем. Результат имеет тип in t.

А. 7 . 1 5 . Операция логического или выражение - с -логическим- ИЛИ : выражение - с -логическим- И выражение - с -логиче ским-ИЛИ

1 1 выр ажение - с - логическим- И

Операции 1 1 группируются слева направо. Операция дает результат 1, если хотя бы один из операндов не равен нулю, и О в противном случае. В отличие от 1 , операция 1 1 гарантирует порядок вычислений слева направо: вначале вычисляется первый операнд со всеми побочными эффектами; если он не равен О, то значение выражения будет равно 1 . СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

22 1

В противном случае вычисляется правый операнд, и если он не равен О, то значение вы­ ражения будет равно единице, в противном случае - нулю. Операнды могут иметь разные типы, но каждый из них должен быть либо величиной арифметического типа, либо указателем. Результат имеет тип i nt .

А.7. 1 6. Операция выбора по условию услов но е - выражение : выражение - с -логическим- ИЛИ выражение - с -логиче ским- ИЛИ ? выражение : условное - выражение

Вначале вычисляется первое выражение, включая все побочные эффекты; если оно не равно О, то результатом будет значение второго выражения, в противном случае - зна­ чение третьего выражения. Вычисляется только один из двух последних операндов: вто­ рой или третий. Если второй и третий операнды - арифметические, то выполняются обычные арифметические преобразования, приводящие их к некоторому общему типу, который и будет типом результата. Если оба операнда имеют тип vo i d, или являются структурами/объединениями одного и того же типа, или представляют собой указатели на объекты одного и того же типа, то результат будет иметь тот же тип, что и операнды. Если один из операндов имеет тип "указатель", а другой является константой О, то О при­ водится к типу "указатель", и этот же тип будет иметь результат. Если один операнд яв­ ляется указателем на vo id, а второй - указателем другого типа, то последний преобра­ зуется в указатель на vo i d, который и будет типом результата. При сравнении типов указателей модификаторы типов объектов (раздел А.8.2), на ко­ торые эти vказатели ссылаются, во внимание не принимаются, но тип результата насле­ дует модификаторы из обеих ветвей условного выражения.

А. 7. 1 7. Выражения с присваиванием Существует несколько операций присваивания; все они группируются справа налево. выражение - присв а ив а ния : условное - выражение одноме с тно е - выражение зна к - присв аив а ния выражение - присв аивания зна к - присв аив а ния : один и з

*=

/=

%=

+=

=

&=

=

1=

Все операции присваивания требуют именующего выражения (lvalue) в качестве ле­ вого операнда, причем это выражение должно быть модифицируемым. Это значит, что оно не может быть массивом, или функцией, или иметь неполный тип. Кроме того, тип левого операнда не может иметь модификатора cons t ; если он является структурой или объединением, то они и их вложенные структуры/объединения не должны содержать элементов с модификаторами con s t . Тип выражения присваивания совпадает с типом его левого операнда, а значение равно значению левого операнда после завершения при­ сваивания. В простом присваивании со знаком значение выражения в правой части замещает объект, к которому обращается именующее выражение (lvalue). При этом должно вы­ полняться одно из следующих условий: =

222

П РИЛ ОЖЕ НИ Е А



оба операнда имеют арифметический тип (правый операнд в процессе присваивания приводится к типу левого операнда) ; оба операнда являются структурами или объединениями одного и того же типа;



один операнд - указатель, а другой - указатель на vo id;



левый операнд - указатель, а правый - константное выражение со значением О ;



оба операнда - указатели н а функции или объекты, имеющие одинаковый тип, за исключением возможного отсутствия con s t или vo l at i l e в типе правого опе­ ранда.



Выражение El оп= Е2 эквивалентно выражению El = E l оп ( Е2 ) с той разницей, что E l вычисляется только один раз.

А. 7 1 8 О перация "запятая" .

.

выражение : выражение - присв аив а ния выражение , выражение - присв аив а ния

Два выражения, разделенные запятой, вычисляются слева направо, и значение левого выражения отбрасывается. Тип и значение результата совпадают с типом и значением правого операнда. Вычисление всех побочных эффектов левого операнда завершается перед началом вычисления правого операнда. В контекстах, в которых запятая имеет особое значение, например в списках аргументов функций (раздел А.7.3 .2) или в списках инициализаторов (раздел А.8.7), где в качестве синтаксических единиц должны фигури­ ровать выражения присваивания, оператор запятая может стоять только в группирующих круглых скобках. Например, в следующее выражение входят три аргумента, второй из которых имеет значение 5 : f ( а , ( t = З , t+ 2 ) , с )

А. 7. 1 9. Константные выражения С синтаксической точки зрения константное выражение - это выражение с ограни­ ченным подмножеством операций: конста нтное - выражение : условное - выражение

В ряде контекстов разрешаются только выражения, эквивалентные константам: после меток c a s e в операторе s w i t ch, при задании границ массивов и длин битовых полей, в качестве значений перечислимых констант, в инициализаторах, а также в некоторых вы­ ражениях директив препроцессора. Константные выражения не могут содержать присваивания, операции инкрементиро­ вания и декрементирования, вызовы функций и операции "запятая"; эти ограничения не распространяются на операнды операции s i z e o f . Если требуется целочисленное кон­ стантное выражение, то его операнды должны состоять из целых, перечислимых, сим­ вольных и вещественных констант. Операции приведения должны преобразовывать ве­ личины только к целочисленным типам, а любая вещественная константа должна явно приводиться к целому типу. Из этого следует, что в константном выражении не может СПРАВОЧНОЕ РУКОВОДСТВО ПО Я ЗЫКУ С

223

быть массивов, операций обращения по указателю, взятия адреса и обращения к полям структуры. (Однако операция s i z e o f может иметь операнды любого вида.) Для константных выражений в инициализаторах допускается большая свобода. Опе­ рандами могут быть константы любого типа, а к внешним или статическим объектам, а также к внешним и статическим массивам, индексируемым константными выражениями, можно применять одноместную операцию &. Одноместная операция & может также при­ меняться неявно - при использовании массива без индекса или функции без списка ар­ гументов. Вычисление инициализирующего значения должно давать константу либо ад­ рес ранее объявленного внешнего или статического объекта плюс-минус константа. Меньшая свобода допускается для целочисленных константных выражений в дирек­ тиве # i f : не разрешаются выражения с s i z e o f , константы перечислимых типов и опе­ рации приведения типа (см. раздел А. 1 2.5).

А . 8 . Объ я вл е н ия Объявление задает способ интерпретации идентификатора, но не обязательно резер­ вирует память, ассоциируемую с этим идентификатором . Объявления, которые действи­ тельно резервируют память, называются определениями. Объявления имеют следующую форму: о бъявление : специфика торы- о бъявления спис о к - иниц - описа телейнеоб

Описатели в

содержат объявляемые идентификаторы; представляют собой последовательности из специфи­

спис ок - иниц - опис а телей

специфика торы - о бъявления

каторов типа и класса памяти. специфика торы- о бъявления : специфик а т ор - кла сса - памяти спе цифика торы- о бъявлениянеоб специфика тор - типа специфика торы- о бъявлениянеоб модифика тор - типа специфика торы- объявлениянеоб спис о к - иниц - описа телей : иниц - описа тель спис о к - иниц - описа телей , иниц - описа тель иниц - описа тель : описа тель описа тель = инициализ а т ор

Описатели будут рассмотрены ниже, в разделе А.8.5; они содержат объявляемые имена. Объявление должно либо содержать по крайней мере один описатель, либо же его спецификатор типа должен объявлять метку структуры/объединения или задавать эле­ менты перечисления; пустое объявление недопустимо.

А.8. 1 . Спецификаторы класса памяти Определение спецификатора класса памяти таково: специфика тор - кла сса - памяти : au to

224

П РИЛ ОЖЕНИЕ А

regi s t e r s ta t i c extern typede f

Значение этих классов памяти рассматривалось в разделе А.4. Спецификаторы auto и reg i s t e r присваивают объявляемым объектам автомати­ ческий класс памяти; эти спецификаторы можно применять только внутри функции. Объявления с auto и reg i s t e r одновременно являются определениями и резервируют память. Спецификатор regi s t e r эквивалентен auto, но при этом подсказывает ком­ пилятору, что объявленные объекты используются в программе особенно часто. В реги­ страх можно фактически разместить лишь небольшое количество объектов, причем только определенного типа; конкретные ограничения зависят от реализации. К объекту, объявленному как regi s t e r, нельзя применять одноместную операцию & как явно, так и неявно. Новым в стандарте я вляется правило , согласно которому вычислять адрес объекта класса regi s t e r нельзя, а класса a u t o - можн о .

Спецификатор s t a t i c присваивает объявляемым объектам статический класс памя­ ти; он может использоваться и внутри, и вне функций. Внутри функции этот специфика­ тор инициирует выделение памяти для объекта и служит определением; его использова­ ние вне функций будет рассмотрено в разделе А. 1 1 .2. Объявление со спецификатором ext e rn внутри функции заявляет, что для объекта где-то в другом месте программы выделена память; в разделе А. 1 1 .2 говорится о том, как этот спецификатор работает вне функций. Спецификатор typede f не резервирует никакой памяти и назван спецификатором класса памяти только для соблюдения стандартного синтаксиса; об этом спецификаторе будет сказано в разделе А.8.9. Объявление может содержать не больше одного спецификатора класса памяти . Если спецификатор в объявлении отсутствует, то действуют следующие правила: •

объекты, объявленные внутри функций, имеют класс aut o ;



функции, объявленные внутри функций, имеют класс ext e rn;



объекты и функции, объявляемые вне функций, считаются статическими с внеш­ ним связыванием (см. разделы А. 1 О, А. 1 1 ).

А.8 . 2 . Спецификаторы типа Спецификаторы типа имеют следующее формальное определение: специфика тор- типа : vo i d char sho r t int l ong f l oat dou Ы e s i gned uns i gned

СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

225

специфика тор - с труктуры- или- о бъ единения специфика тор - пер е числения типоопределяюще е - имя

Вместе с int допускается использование еще одного из слов l ong или sho r t ; слово int при этом можно опускать без изменения смысла. Слово l ong может употребляться вместе с douЫ e . Со словами char, int и вариантами последнего ( s hort, l ong) раз­ решается употреблять одно из слов s i gne d или uns i gned. Любое из них может ис­ пользоваться и без i n t , которое в таком случае подразумевается по умолчанию. Специ­ фикатор s i gned бывает полезен, когда требуется гарантировать наличие знака у объек­ тов типа char. Его можно применять и к другим целочисленным типам, хотя в этом случае он совершенно лишний. За исключением описанных выше случаев, объявление не может содержать больше одного спецификатора типа. Если в объявлении нет ни одного спецификатора типа, то подразумевается тип i nt . Для указания особых свойств объявляемых объектов предназначаются такие модифи­ каторы: модифика тор - типа : cons t vo l a t i l e

Модификаторы типа могут употребляться с любым спецификатором типа. Разреша­ ется инициализировать объект с модификатором cons t , однако присваивать ему что­ либо впоследствии запрещается. Смысл модификатора vo l a t i l e зависит исключи­ тельно от реализации; стандартной универсальной семантики у цего нет. Свойства cons t и volat i l e введены стандартом ANSI. Модификатор const приме няется для объявления объектов, размещаемых в памяти, открытой только для чтения, а также для расширения возможностей оптимизации. Назначение модифи катора vol a t i l e запретить оптимизацию, которая могла бы быть выполнена в противном случае. Например, в с истем ах, где ресурсы устр ойств ввода вывода отображены на ад­ ресное пространство оперативной памяти, указатель на регистр устройства можно объ­ явить с ключевым словом vo l a t i l e , чтоб ы запретить компилятору сокращать кажу­ щиеся ему избыточными обращения через указатель. Компилятор может игнорировать указанные модификаторы, однако обязан диагностировать явные попытки изменения значений соn s t объекто в ­

-

-

-

.

А.8.3 . Объявления структур и объединений Структура - это объект, состоящий и з последовательности именованных элементов различных типов. Объединение - это объект, который в каждый момент времени со­ держит один из нескольких элементов различных типов. Объявления структур и объеди­ нений имеют один и тот же вид. специфика тор - с труктуры- или - о бъ единения : s t ruct - или - uni on идентифика тор необ { список - объявлений - структуры s t ru c t - или - un i on идентифика тор

}

s t ru c t - или - un i on : s t ruc t union

226

П РИЛ ОЖЕ НИЕ д

Конструкция список - о бъявлений - с труктуры представляет собой последова­ тельность объявлений элементов структуры или объединения: список - о бъявлений- с труктуры: объявление - с труктуры список - о бъявлений- структуры о бъявление - с труктуры объявление - с труктуры: список- специфика торов -модифика торов списо к - описа телей- структуры

список- специфика т оров-модифика торов : специфика тор - типа списо к - специфика торов -модифика торовнеоб модифика тор - типа списо к - специфика торов -модифика торовнеоб список- описа телей- структуры: описа тель - с труктуры список- описа телей- структуры ,

описа тель - с труктуры

Обычно описа тель - с труктуры является просто описателем элемента структуры или объединения. Элемент структуры может также состоять из заданного количества би­ тов. Такой элемент называется битовым полем или просто полем. Его длина отделяется от имени поля двоеточием. описа тель - структуры: описатель описа тельнеоб : конс тантное - выражение

Спецификатор типа, имеющий приведенную ниже форму, объявляет указанный иден­ тификатор меткой (tag) структуры или объединения, задаваемой списком элементов: s t ruct -или- un i on идентифика тор

{ спис ок - объявлений- структуры }

Последующее объявление в той же или вложенной области действия может ссылаться на тот же тип, используя в спецификаторе только метку без списка: s t ruc t -или- un i on идентифика тор ;

Если спецификатор с меткой, но без списка фигурирует там, где его метка не объяв­ лена, создается неполный тип. Объекты неполного структурного типа могут упоминаться в контексте, где не требуется знать их размер, - например, в объявлениях (но не опре­ делениях) для описания указателя или создания нового типа с помощью typede f , но никаким иным образом. Тип становится полным при появлении последующего специфи­ катора с этой меткой, содержащего также список объявлений. Даже в спецификаторах со списком объявляемый тип структуры или объединения является неполным внутри списка и становится завершенным только после появления символа } , заканчивающего специ­ фикатор. Структура не может содержать элементов неполного типа. Следовательно, невозмож­ но объявить структуру или объединение, которые содержат сами себя. Однако, кроме присвоения имени типу структуры или объединения, метка позволяет еще и определять структуры, обращающиеся сами к себе. Структуры или объединения могут содержать указатели на самих себя, поскольку можно объявлять указатели на неполные типы. В отношении объявлений следующего вида действует особое правило: s t ruc t -или- un i on идентифика тор;

СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫ КУ С

227

Это объявление создает структурный тип, но не содержит списка объявлений и опи­ сателя. Даже если иде н тифика т ор является меткой структуры или объединения, уже объявленной во внешней области действия (раздел А. 1 1 . 1 ), такое объявление делает его меткой структуры или объединения нового, неполного типа в текущей области действия. Это маловразумительное правило - новое в стандарте ANSI. Оно предназначено для работы со взаимно рекурсивными структурами, объявленными во внутренней области действия , метки которых тем не менее могли быть уже объявлены ранее во внешней области действия.

Спецификатор структуры или объединения со списком, но без метки создает уни­ кальный тип, на который можно ссылаться непосредственно только в объявлении, ча­ стью которого он является. Имена элементов и меток не вступают в конфликт друг с другом или с обычными пе­ ременными. Имя элемента не может фигурировать дважды в одной и той же структуре или объединении, однако один и тот же элемент можно использовать в разных структу­ рах или объединениях. В первом издании этой книги имена элементов структур и объединений не ассоцииро­ вались со своими родительскими объектами. Однако в компиляторах эта ассоциация стала обычной задолго до появления стандарта ANSI.

Элемент структуры или объединения, не являющийся битовым полем, может иметь любой объектный тип. Поле (которое не обязано иметь описатель и, следовательно, мо­ жет быть безымянным) имеет тип int, uns igned i nt или s i gned int и интерпре­ тируется как объект целочисленного типа с длиной, указанной в битах. Считается ли по­ ле типа int знаковым или беззнаковым - зависит от реализации языка. Соседние эле­ менты-поля структур упаковываются в системно-зависимые единицы памяти в зависящем от реализации направлении. Если следующее за полем другое поле не поме­ щается в частично заполненную единицу (ячейку) памяти, то оно может оказаться разде­ ленным между двумя ячейками, либо же ячейка может быть дополнена пустыми битами. Безымянное поле нулевой ширины обязательно приводит к такому дополнению, так что следующее поле начинается строго на границе очередной ячейки памяти. Стандарт ANSI делает поля еще более зависимыми от реализации, чем в первой редак­ ции книги. Чтобы правильно хранить битовые поля в "завися щем от реализации" виде, желательно ознакомиться с правилами данной реализации я зыка. Структуры с битовы­ ми полями мо гут служить системно- переносимым методом для попытки уменьшить размеры памяти под структуру ( вероятно, ценой удлинения кода программы и времени обращения к полям) или непереносимым методом для описания распределения памяти на битовом уровне. Во втором случае необходимо понимать правила и соглашения кон­ кретной реал изации языка.

Элементы структуры имеют адреса, возрастающие в порядке объявления элементов. Элементы структуры, не являющиеся битовыми полями, выравниваются по границам ад­ ресуемых ячеек памяти в зависимости от своего типа; таким образом, в структуре могут быть безымянные дыры. Если указатель на структуру приводится к типу указателя на ее первый элемент, результат указывает на первый элемент. Объединение можно представить себе как структуру, все элементы которой начина­ ются со смещения О и размеры которой достаточны для хранения любого из элементов. В каждый момент времени в объединении хранится не больше одного элемента. Если указатель на объединение приводится к типу указателя на один из элементов, результат указывает на этот элемент.

228

П РИЛ ОЖЕНИЕ А

Вот простой пример объявления структуры: s t ruct t node { char tword [ 2 0 ] ; int count ; s t ruct t node * l e f t ; s t ru c t tnode * r ight ;

};

Эта структура содержит массив из 20 символов, число типа int и два указателя на такую же структуру. Имея это объявление, можно далее объявлять объекты таким образом: s t ruc t t node s ,

* sp ;

где в объявляется структурой указанного типа, а sp указателем на такую структуру. При наличии приведенных определений следующее выражение будет ссылкой на эле­ мент count в структуре, на которую указывает sp: -

sp - > count

А это выражение представляет собой указатель на левое поддерево в структуре

в:

s . left

Наконец, следующее выражение дает первый символ и з массива t word, элемента правого поддерева структуры s : s . r i ght - > t word [ O ]

Вообще говоря, можно использовать значение только того элемента объединения, ко­ торому последний раз присваивалось значение. Однако есть одно особое правило, облег­ чающее работу с элементами объединения : если объединение содержит несколько струк­ тур, начинающихся с общей для них последовательности данных, и если объединение в текущий момент содержит одну из этих структур, то к общей части данных разрешается обращаться через любую из указанных структур (при этом гарантируется корректность результата). Так, следующий фрагмент кода вполне правомерен : un i on { s t ru c t {

} n;

int type ;

s t ru c t { int t ype ; int int node ; } ni ; s t ru c t { int type ; f l oat f l oatnode ; nf ; и;

u . n f . type = FLOAT ; u . nf . f l oatnode 3 . 14 ; =

if

( u . n . type = = FLOAT ) . . . s in ( u . nf . f l oatnode )

СПРАВОЧНОЕ РУКОВОДСТВО ПО Я ЗЫКУ С

229

А.8.4. Перечислимые типы (перечисления} Перечисления - это уникальные типы, значения которых целиком состоят и з множе­ ства именованных констант (перечислимых элементов). Вид спецификатора перечисле­ ния заимствован у структур и объединений. специфика тор - перечисления : enum идентифика торнеаб { список- пере числимых enum идентифика тор

}

спис о к - пер е числимых : пере числимо е спис ок- пере числимых , пере числимое пере числимо е : идентифика тор идентифика тор = константн о е - выражение Идентификаторы, входящие в список перечислимых, объявляются константами типа

int и могут употребляться везде, где требуются константы. Если в этом списке нет ни одного элемента со знаком = , то значения констант начинаются с О и увеличиваются на 1 по мере чтения объявления слева направо. Перечислимый элемент со знаком = присваи­ вает соответствующему идентификатору указанное значение; последующие идентифика­ торы продолжают увеличиваться от заданного значения. Им ена перечислимых, используемые в одной области действия, должны отличаться друг от друга и от имен обычных переменных, но их значения не обязаны отличаться. Роль идентифика тора в специфика т оре - пер е числения аналогична роли мет­ ки структуры в специфика торе - структуры. он является именем конкретного пере­ числимого типа. Правила определения специфика торов - пере числения с метками и без, а также для списков совпадают с правилами для спецификаторов структур или объе­ динений, с той разницей, что перечислимые типы не бывают неполными . Метка специ ­ фика тора - пер е числения без списка элементов должна иметь соответствующий спе­ цификатор со списком в пределах области действия. В

первой версии языка перечислимые типы отсутствовали, но они применяются на практике уже много лет.

А.8. 5 . Описатели Описатели имеют следующий формальный синтаксис: описа тель : ука з а тельнеаб с о б с тв енно - а писа тель собств енно - описа тель : идентифика тор ( описа тель ) с о б с тв енно - описа тель собств енно - описа тель собств енно - описа тель

константно е - выражениенеоб ] список- типов - параме тров ) спис ок- идентифика т оровнеоб )

ука з а тель :

230

П РИЛ ОЖ ЕНИЕ А

* *

список-модифика торов - типанеоб список-модифика тор ов - типанеоь ука з а тель

спис ок-модифика торов - типа : модифика тор - типа список-модифика тор ов - типа модифика тор - типа

Построение описателей имеет много общего с построением обращений по указате­ лям, вызовов функций и обращений к элементам массивов; в частности, группировка выполняется по тем же правилам.

А.8.6. Смысл и содержание описателей Список описателей располагается сразу после цепочки спецификаторов типа и класса памяти. Главный элемент любого описателя - это объявляемый им уникальный иден­ тификатор, который фигурирует в качестве первого варианта в грамматическом правиле собств енно - описа теля. Спецификаторы класса памяти относятся непосредственно к идентификатору, а его тип зависит от формы описателя. Описатель следует восприни­ мать как утверждение: если в выражении идентификатор фигурирует в той же форме, что и в описателе, то он обозначает объект указанного типа. Если рассматривать только те части спецификаторов объявлений и описателей, кото­ рые определяют тип (см. раздел А.8.2), то объявление имеет общий вид "Т D", где Т ­ тип, а D - описатель. Тип, придаваемый идентификатору в различных формах описате­ ля, можно описать индуктивно через эту запись. В объявлениях вида Т D, где D - просто идентификатор без дополнительных эле­ ментов, типом идентификатора будет т. В объявлениях Т D, где D имеет показанную ниже форму, тип идентификатора в D l будет тем же, что и в D: Dl )

Скобки не изменяют тип, но могут повлиять на результаты его привязки к идентифи­ каторам в сложных описателях.

А.8.6. 1 . Описатели указателей Рассмотрим объявление вида Т D, где D имеет следующую форму: *

список-модифика торов - типанеоб D l

Пусть тип идентификатора в объявлении Т D l - "ра сширитель - типа Т". Тогда типом идентификатора D будет "ра сширитель - типа список -модифика торов типа указатель на Т". Модификаторы типа, стоящие после символа * , относятся к са­ мому указателю, а не к объекту, на который он указывает. Для примера рассмотрим объявление int * ар [ ] ; где ар [ ] играет роль D l ; объявление " i nt ар [ ] " дает переменной ар (см. ниже) тип "массив элементов типа int", список модификаторов типа здесь пуст, а расширитель типа - "массив элементов типа . . . ". Следовательно, на самом деле объявление ар озна­ чает "массив указателей на int".

СПРАВ ОЧНОЕ РУКО В ОДСТВ О ПО ЯЗЫКУ С

23 1

Вот еще примеры объявлений: int i , * p i , * c onst cpi const int c i 3 , * pc i ;

=

&i ;

=

В них объявляется целая переменная i и указатель на целую переменную p i . Значе­ ние указателя c p i нельзя изменить впоследствии; cpi всегда будет указывать на одно и то же место, даже если значение, на которое он указывает, изменится. Целая величина ci - константа, ее изменить нельзя (хотя можно инициализировать, как в данном слу­ чае). Тип указателя p c i - "указатель на c o n s t int"; сам указатель можно изменить, чтобы он указывал на другое место в памяти, но значение, на которое он будет указы­ вать, через обращение к р е i изменить нельзя.

д.8 . 6 . 2 . Описатели массивов Рассмотрим объявление Т D, где D имеет вид Dl [ конс т а н тн о е - выражениенеоб] Пусть тип идентификатора объявления Т D l - "ра сширит ель - типа Т", тогда типом идентификатора D будет "р а сширит елъ - типа массив элементов типа Т". Если константное выражение присутствует, оно должно быть целочисленным и больше О. Ес­ ли константное выражение, задающее количество элементов в массиве, отсутствует, то массив имеет неполный тип. Массив можно составлять из объектов арифметического типа, указателей, структур и объединений, а также других массивов (создавая таким образом многомерные массивы). Любой тип, из которого создается массив, должен быть полным; он не может быть структурой или массивом неполного типа. Это значит, что для многомерного массива пропустить можно только первую размерность. Неполный тип массива становится пол­ ным либо в другом, полном, объявлении этого массива (раздел А. 1 0.2), либо при его инициализации (раздел А.8.7). Например, следующая запись объявляет массив чисел ти­ па f l oa t и массив указателей на числа типа f l o a t : f l oat fa [ 1 7 ] ,

* a fp [ 1 7 ] ;

Аналогично, следующее объявление создает статический трехмерный массив целых чисел размером Зх5х7 : s t a t i c int хЗ d [ З ] [ 5 ] [ 7 ] ;

Точнее говоря, х З d является массивом из трех элементов, каждый из которых есть массив из пяти элементов, содержащих по 7 значений типа i n t . Любое из выражений, х З d, х З d [ i ] , х З d [ i ] [ j ] , хЗ d [ i ] [ j ] [ k ] , может фигурировать в другом выраже­ нии - лишь бы контекст был правильный . Первые три выражения имеют тип "массив". а последнее - тип i n t . Если конкретнее, х З d [ i ] [ j ] - это массив из 7 целых чисел, а х З d [ i ] - массив из пяти массивов, по 7 целых чисел в каждом. Операция индексирования массива определена так, что выражение E l [ Е2 ] иден­ тично * ( Е 1 + Е2 ) . Следовательно, несмотря на асимметричность записи, индексирова­ ние - коммутативная операция. Учитывая правила преобразования для операции + и массивов (разделы А.6.6, А.7. 1 , А.7.7), можно утверждать, что если E l - массив, а Е 2 - целочисленная величина, то E l [ Е 2 ] обозначает Е 2 -й элемент массива E l . Так, в нашем примере х З d [ i ] [ j ] [ k ] означает то же самое, что и * ( х З d [ i ] [ j ] + k ) . Первое подвыражение, хЗ d [ i ] [ j ] , согласно разделу А. 7. 1 , приво­ дится к типу "указатель на массив целых чисел"; согласно разделу А.7 .7, сложение

232

П РИЛ ОЖЕНИЕ А

включает умножение на размер объекта типа i nt . Из этих же правил следует, что масси­ вы запоминаются построчно (последние индексы пробегаются быстрее) и что первый индекс в объявлении помогает определить количество памяти, занимаемой массивом, но не играет никакой другой роли в вычислении адреса элемента массива.

А.8. 6.3. Описатели функций Рассмотрим новый стиль объявления функций Т D, где D имеет вид D l ( спис ок- типов - параме тров) "р а сширит ель - типа Т", тогда Пусть тип идентификатора объявления Т D l типом идентификатора в D будет "р а сширит ель - типа функция с аргументами спи ­ с о к - тип ов - параме тр о в , возвращающая т " . Параметры имеют следующий синтаксис: -

список- типов - параме тров : спис ок- параме тров список-параме тров список - параме тров : объявление - параме тра спис ок- параме тр ов , о бъявление - параме тра объявление - параме тра : специфика торы- о бъявления описа тель специфика торы- объявления а б с тра ктный- описа тельнеоб

В новом стиле объявления функций список параметров явно задает их типы. В част­ ном случае, если функция вообще не имеет параметров, в ее описателе на месте списка типов указывается одно ключевое слово vo id. Если список типов параметров заканчи­ 11 , то функция может иметь больше аргументов, чем коли­ вается многоточием 11 , чество явно описанных параметров (см. раздел А.7 .3.2). Типы параметров, являющихся массивами функций, заменяются на указатели в соот­ ветствии с правилами преобразования параметров (раздел А. 1 0. 1 ). Единственный специ­ фикатор класса памяти, который разрешается использовать в объявлении параметра, это regi s t e r, однако он игнорируется, если описатель функции не является заголов­ ком ее определения. Аналогично, если описатели в объявлениях параметров содержат идентификаторы, а описатель функции не является заголовком определения функции, то эти идентификаторы немедленно устраняются из текущей области действия. Абстракт­ ные описатели, не содержащие идентификаторов, рассматриваются в разделе А.8.8. В старом стиле вторая форма объявления функции имела вид Т D, где D представляет собой •





D l ( спис ок - идентифика тор овнеоб )

Пусть тип идентификатора объявления Т Dl - "р а сширитель - типа Т " , тогда ти­ пом идентификатора в D будет "р а сширит ель - типа функция неуказанных аргумен­ тов, возвращающая Т". Параметры, если они есть, имеют следующий синтаксис: список-идентифика торов : идентифика тор спис ок-идентифика торов , идентифика тор

СПРАВОЧНОЕ РУКОВОДСТВ О ПО Я ЗЫКУ С

233

В старом стиле, если описатель функции не используется в качестве заголовка опре­ деления функции (раздел А. 1 0. 1 ), список идентификаторов должен отсутствовать. Ника­ кой информации о типах параметров в объявлениях не содержится. Для примера рассмотрим объявление int f ( ) ,

* fp i ( ) ,

( *pf i ) ( ) ;

где объявляется функция f , возвращающая число типа int, функция fp i , возвращаю­ щая указатель на int, и указатель p f i на функцию, возвращающую int. Ни для одной функции в объявлении не указаны типы параметров; все функции объявлены в старом стиле. А вот как выглядит объявление в новом стиле: int s t rc py ( char * de s t ,

const char * s ource ) ,

rand ( vo id ) ;

где s t rcpy - функция с двумя аргументами, возвращающая значение типа int ; пер­ вый аргумент - указатель на значение типа char, а второй - указатель на неизменяе­ мую строку символов. Имена параметров играют роль удобных комментариев. Вторая функция, rand, аргументов не имеет и возвращает число типа in t. Описатели функций с прототипами параметров - наиболее важное нововведение стан­ дарта ANSI. В сравнении со "старым стилем", принятым в первой редакции языка, они позволяют контролировать на предмет ошибок и приводить к нужным типам аргумен­ ты во всех вызовах. За это пришлось заплатить некоторую цену в виде путаницы в про­ цессе введения нового стиля и необходимости согласования обеих форм. Чтобы обес­ печить совместимость, потребовалось ввести некоторые неизящные синтаксические конструкции, а именно vo id для явного указания на отсутствие параметров. 11 применительно к функциям с переменным количеством аргу­ Многоточие 11 , ментов - также новинка, которая вместе с макросами из стандартного заголовочного файла < s t darg . h> наконец обеспечивает официальную поддержку ранее запрещен­ ного, хотя и неофициально допускавшегося механизма. Указанные синтаксические конструкции заимствованы из языка С++. •





А.8.7. Инициализация Начальное значение для объявляемого объекта можно указать с помощью иниц ­ о писа т еля. Инициализатору, представляющему собой выражение или список инициа­ лизаторов в фигурных скобках, предшествует знак = . Этот список может завершаться за­ пятой для удобства и единообразия формата. инициализа тор : выражение - присв а ив а ния { список- инициализа торов } { список- инициализ а торов ,

}

список- инициализа торов : инициализа тор список- инициализа тор ов , инициализ а тор

В инициализаторе статического объекта или массива все выражения должны быть константными (см. раздел А.7 . 1 9). Если инициализатор объекта или массива, объявлен­ ного с модификаторами aut o или reg i s t e r, является списком в фигурных скобках, то входящие в него выражения также должны быть константными. Однако для автоматиче-

234

П РИ Л ОЖЕНИЕ А

ского объекта с одним инициализирующим выражением оно не обязано быть констант­ ным, а просто должно иметь соответствующий объекту тип. В первой редакции языка не допускалась инициализация автоматических структур, объединений и массивов. Стандарт ANSI разрешает ее, но только с помощью констант­ ных конструкций, если инициализатор нельзя представить одним простым выражением.

Статический объект, не инициализированный явно, инициализируется так, как если бы ему (или его элементам) присваивалась константа О. Начальное значение автоматиче­ ского объекта, не инициализированного явным образом , не определено. Инициализатор указателя или объекта арифметического типа - это одно выражение (возможно, заключенное в фигурные скобки), которое присваивается объекту. Инициализатор структуры - это либо выражение того же структурного типа, либо заключенный в фигурные скобки список инициализаторов ее элементов, заданных по по­ рядку. Безымянные битовые поля игнорируются и не инициализируются. Если инициа­ лизаторов в списке меньше, чем элементов в структуре, то оставшиеся элементы ини­ циализируются нулями. Инициализаторов не должно быть больше, чем элементов. Инициализатор массива - это список инициализаторов его элементов, заключенный в фигурные скобки. Если размер массива не известен, то он считается равным числу инициализаторов, при этом его тип становится полным. Если размер массива фиксиро­ ван, то число инициализаторов не должно превышать числа его элементов; если инициа­ лизаторов меньше, чем элементов, то оставшиеся элементы инициализируются нулями. Особым случаем является инициализация массива символов. Ее можно выполнять с помощью строкового литерала; символы инициализируют элементы массива в том по­ рядке, в каком они заданы в строковом литерале. Точно так же с помощью литерала из расширенного набора символов (см. раздел А.2.6) можно инициализировать массив типа wchar _ t. Если размер массива не известен, то он определяется числом символов в строке, включая и завершающий нулевой символ; если размер массива известен, то чис­ ло символов в строке, не считая завершающего нулевого символа, не должно превышать его размера. Инициализатором объединения может быть либо выражение того же типа, либо заключенный в фигурные скобки инициализатор его первого элемента. В

первой редакции языка не позволялось инициализировать объединения. Правило "первого элемента" несколько неуклюже, но зато практически не поддается обобщен­ ной трактовке без введения новых синтаксических конструкций. Кроме введения явно­ го способа инициализации объединений, пусть даже примитивным способом, этим пра­ вилом стандарт ANSI проясняет еще и семантику статических объединений, не инициа­ лизируемых явно.

Структуры или массивы обобщенно называются составными объектами (aggregates). Если составной объект содержит элементы составного типа, то правила инициализации применяются рекурсивно. Фигурные скобки в некоторых случаях ини­ циализации можно опускать. Так, если инициализатор элемента составного объекта, ко­ торый сам является составным объектом, начинается с левой фигурной скобки, то этот подобъект инициализируется последующим списком разделенных запятыми инициализа­ торов; считается ошибкой, если количество инициализаторов подобъекта превышает ко­ личество его элементов. Если, однако, инициализатор подобъекта не начинается с левой фигурной скобки, то чтобы его инициализировать, отсчитывается соответствующее чис­ ло элементов из списка; следующие элементы инициализируются оставшимися инициа­ лизаторами объекта, частью которого является данный подобъект. СПРАВ ОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

235

Рассмотрим пример : = { 1, 3, 5 } ; х [J

int

Здесь объявляется и инициализируется одномерный массив х из трех элементов, по­ скольку размер не указан, а список состоит из трех инициализаторов. Еще пример: { f l oa t у [ 4 ] [ 3 ] { 1, 3, 5 }, { 2, 4, 6 }, { 3, 5, 7 } , }; Эта конструкция представляет собой инициализацию с полным набором фигурных скобок: числа 1 , 3 и 5 инициализируют первую строку в массиве у [ О ] , т.е. у [ О ] [ О ] у [ О ] [ 1 ] и у [ О ] [ 2 ] . Аналогично инициализируются следующие две строки : у [ 1 ] и у [ 2 ] . Инициализаторов не хватило на весь массив, поэтому элементы строки у [ 3 ] бу­ дут нулевыми. Точно такой же результат был бы получен с помощью следующего объяв­ ления: f l oa t у [ 4 ] [ 3 ] 1, 3, 5, 2, 4, 6, 3, 5, 7 =

,

=

};

Инициализатор для у начинается с левой фигурной скобки, но инициализатор для у [ О ] скобки не содержит, поэтому из списка будут взяты три элемента. Аналогично бу­ дут взяты по три элемента для у [ 1 ] , а затем для у [ 2 ] . И еще один пример: f l oa t у [ 4 ] [ 3 ] = { { 1 }, { 2 }, { 3 }, { 4 }

};

Здесь инициализируется первый столбец матрицы у (рассматриваемой как двумер­ ный массив), а все остальные элементы остаются нулевыми. Наконец, вот пример инициализации массива символов: " Syntax e rror on l ine % s \n " ; char msg [ ] Этот массив символов инициализируется с помощью строки; в его размере учитыва­ ется и завершающий нулевой символ \ О . =

А. 8 . 8 . Имена типов В ряде контекстов (например, при явном приведении к типу, при указании типов па­ раметров в объявлениях функций, в аргументе операции s i z e o f ) возникает потребность в применении имени типа данных. Эта потребность реализуется с помощью имени типа, определение которого синтаксически почти совпадает с объявлением объекта того же типа, в котором только опущено имя объекта. имя - типа : спис о к - спе цифика тор о в -модифика торов а б с трактный- о пи са тельнеоб а б страктный- описа тель : ука з а тель ука з а тельнеоб с о б с тв енно - а б с трактный- опис а тель с о б с тв е нно - а б с тр а ктный- описа тель :

236

П РИ Л ОЖ ЕНИЕ А

( а б страктный- описа тель ) собств енно - а б с тра ктный- опис а т ельнеоб [ константн о е - выражениенеов ] собств енно - а бстрактный - аписа тель необ ( список - типов - параме тровнеоб )

Можно указать ровно одно место в абстрактном описателе, где мог бы находиться идентификатор, если бы данная конструкция была описателем в полноценном объявле­ нии объекта. Именованный тип совпадает с типом этого предполагаемого идентификато­ ра. Вот примеры: int int int int int int

* * [З] (*) [] * () ( * [ ] ) ( vo i d )

Здесь объявляются соответственно типы "целое число" ( i nt), "указатель н а i nt", "массив из трех указателей на int", "указатель на массив из неизвестного количества элементов типа int", "функция неизвестного количества параметров, возвращающая указатель на int", "массив неизвестного размера, состоящий из указателей на функции без параметров, каждая из которых возвращает int".

А.8 . 9 . Объявление typ e de f Объявления, в которых спецификатором класса памяти является typede f , не объяв­ ляют объектов - они определяют идентификаторы, представляющие собой имена типов. Эти идентификаторы называются типоопределяющими именами. типоопределяюще е - имя : идентифика тор

Объявление с ключевым словом typede f ассоциирует тип с каждым именем из сво­ их описателей (см. раздел А.8.6). С этого момента типоопределяющее имя становится синтаксически эквивалентным ключевому слову спецификатора типа и обозначает свя­ занный с ним тип. Рассмотрим следующие объявления: typede f l ong B l ockno , * B l ockpt r ; typede f s t ru c t { douЫ e r , thet a ;

} Comp l ex ;

После них становятся допустимыми следующие объявления: B l ockno Ь ; ext e rn B l ockp t r Ьр ; Comp l ex z , * zp ;

Переменная Ь имеет тип l ong, Ьр - тип "указатель на l ong"; z -структура задан­ ного вида; zp - указатель на такую структуру. Объявление typede f не вводит новых типов, оно только дает имена типам, которые могли бы быть заданы и другим способом. Например, Ь имеет тот же тип, что и любой другой объект типа l ong. Типоопределяющие имена могут быть переопределены в более внутренней области действия, но при условии, что в них присутствует непустой набор спецификаторов типа. Возьмем , например, конструкцию ext ern B l ockno ;

СПРАВ ОЧНО Е РУКОВОДСТВ О ПО ЯЗЫКУ С

237

Такое объявление не переопределяет B l ockno. Переопределить его можно, напри­ мер, следующим образом: exte rn int B l ockno ;

А.8. 1 О. Эквивалентность типов Два списка спецификаторов типа эквивалентны, если они содержат одинаковый на­ бор спецификаторов типа с учетом умолчаний (например, l ong подразумевает l ong int). Структуры, объединения и перечисления с разными метками считаются разными, а каждое объединение, структура или перечисление без метки представляет со­ бой уникальный тип. Два типа считаются совпадающими, если их абстрактные описатели (см. раз­ дел А.8.8) окажутся одинаковыми с точностью до эквивалентности типов, после замены всех типоопределяющих имен соответствующими типами и отбрасывания идентифика­ торов параметров функций. При сравнении учитываются размеры массивов и типы па­ раметров функции.

А . 9 . Операторы За исключением оговоренных случаев, операторы выполняются в том порядке, в ка­ ком они записаны. Операторы не имеют вычисляемых значений и выполняются, чтобы выполнить определенные действия. Все операторы можно разбить на несколько групп: опера тор : опера тор - с -ме ткой опера тор-выражение составной- опера тор опера тор - выб ора опера тор - цикла опера тор - перехода

А. 9 . 1 . Операторы с метками Операторы могут иметь метки-префиксы. опера тор - с -ме ткой : идентифика тор : опера тор c a s e константно е - выражение : опера тор de f au l t : опера тор

Метка, состоящая из идентификатора, одновременно служит объявлением этого иден­ тификатора. Единственное назначение идентификатора-метки - указать место перехода для оператора goto. Областью действия идентификатора-метки является текущая функ­ ция. Так как метки имеют свое собственное пространство имен, они не конфликтуют с дру­ гими идентификаторами и не могут быть переопределены (раздел А. 1 1 . 1 ). Метки блоков c a s e и de faul t используются в операторе swi t c h (раздел А.9.4). Константное выражение в c a s e должно быть целочисленным. Сами по себе метки никак не изменяют порядка выполнения программы.

238

П РИ Л ОЖЕ НИ Е д

А. 9 .2. Операторы-выражения Наиболее употребительный вид оператора - это оператор-выражение. опера тор - выражение : выражениенеоб ;

Чаще всего оператор-выражение - это присваивание или вызов функции. Все дейст­ вия, реализующие побочный эффект выражения, завершаются до начала выполнения следующего оператора. Если выражение в операторе опущено, он называется пустым (null); пустой оператор часто используется для обозначения пустого тела оператора цик­ ла или в качестве места перехода для метки.

А.9 .3. Составные операторы Чтобы в местах, где п о синтаксису полагается один оператор, можно было выполнить несколько, предусматривается возможность задания составного оператора (который также называют блоком). Тело определения функции - это также составной оператор. составной- опера тор : { спис ок - о бъявлениЙнеоб список- опера торовнеоб

}

список- о бъявлений : о бъявление список- о бъявлений о бъявление список- опер а т оров : опера тор список- опера торов опера тор

Если идентификатор из списка - о бъявлений находился в более широкой области действия, чем блок, то действие внешнего объявления при входе в данный блока приос­ танавливается (раздел А. 1 1 . 1 ), а после выхода из него - возобновляется. Внутри блока идентификатор можно объявить только один раз. Для каждого пространства имен эти правила действуют независимо (раздел А. 1 1 ) ; идентификаторы из разных пространств имен всегда различны. Инициализация автоматических объектов выполняется при каждом входе в блок и продолжается по мере перебора описателей. При переходе по метке внутрь блока ника­ кие инициализации не выполняются. Инициализация статических объектов выполняется только один раз перед запуском программы.

А.9. 4 . Операторы выбора Операторы выбора направляют выполнение программы по одному из нескольких альтернативных путей. опера тор - выбора : i f ( выражение ) опера тор i f ( выражение ) опера тор e l s e опер а т ор swi tch ( выражение ) опера тор

СПРАВОЧНОЕ РУКО В ОДСТВ О ПО ЯЗЫКУ С

239

Оба вида операторов i f содержат выражение, которое должно иметь арифметиче­ ский тип или тип указателя. Сначала вычисляется выражение со всеми его побочными эффектами, и результат сравнивается с О. Если он не равен нулю, выполняется первый подоператор. В случае совпадения с О для второго вида i f выполняется второй подопе­ ратор. Связанная со словом e l s e неоднозначность разрешается тем, что e l s e относят к последнему не имеющему своего e l s e оператору i f , расположенному на одном с этим e l s e уровне вложенности блоков. Оператор swi t c h вызывает передачу управления одному из нескольких операторов в зависимости от значения выражения, которое должно иметь целочисленный тип. Вхо­ дящий в состав swi t c h подоператор обычно составной. Любой оператор внутри него может быть помечен одной или несколькими метками c a s e (см. раздел А.9. 1 ). Управ­ ляющее выражение подвергается расширению целочисленного типа (см. раздел А.6. 1 ), а константы в c a s e приводятся к расширенному типу. После такого преобразования ника­ кие две с а s е-константы в одном операторе s w i t c h не должны иметь одинаковых зна­ чений. С оператором s w i t c h может быть связано не больше одной метки de f au l t . Операторы swi t c h можно вкладывать друг в друга; метки c a s e и de f au l t относятся к самому внутреннему оператору swi t c h из всех, которые их содержат. Оператор swi t c h выполняется следующим образом. Вычисляется выражение со всеми побочными эффектами, и результат сравнивается с каждой константой из блоков c a s e . Если одна из с а s е -констант равна значению выражения, управление передается оператору с соответствующей меткой. Если не обнаружено совпадения ни с одной из констант в блоках c a s e , управление передается оператору с меткой de faul t, если та­ ковая имеется; в противном случае ни один из подоператоров s w i t c h не выполняется. В

первой редакции языка требовалось, чтобы и управляющее выражение константы в блоках c a s e имели тип i nt .

s w i t ch, и

А. 9 . 5 . Операторы ци кл а Операторы цикла служат для организации циклов - повторяющихся последователь­ ностей операторов . опер а т ор - цикла : whi l e ( выражение ) опер а т ор do опера тор whi l e ( выражение ) ; f o r ( выражениенеоб ; выражениенеоб ; выражениенеоб )

опер а т ор

В операторах wh i l e и do выполнение вложенного опера тора (тела цикла) повто­ ряется до тех пор, пока значение в ыр а жения не станет равно О; выр а же ние должно иметь арифметический тип или быть указателем. В операторе whi l e вычисление выра­ жения со всеми побочными эффектами и проверка условия выполняется перед каждым выполнением тела, а в операторе do эта проверка выполняется после. В операторе for первое выражение вычисляется один раз, тем самым осуществляя инициализацию цикла. На тип этого выражения никакие ограничения не накладываются. Второе выражение должно иметь арифметический тип или тип указателя; оно вычисля­ ется перед каждой итерацией (проходом) цикла. Как только его значение становится равным О , цикл for прекращает свою работу. Третье выражение вычисляется после ка­ ждой итерации и таким образом выполняет повторную инициализацию цикла. Никаких ограничений на его тип нет. Побочные эффекты всех трех выражений заканчиваются

240

П РИ Л ОЖЕНИЕ А

сразу по завершении их вычисления. Если тело цикла f o r не содержит cont inue, то следующие две конструкции эквивалентны: for ( выражениеl ; выражение2 ; выра жениеЗ ) опер а т ор выражениеl ; whi l e ( выражение2 ) опера тор выражениеЗ ;

{

Любое из трех выражений цикла f o r можно опустить. Считается, что отсутствие второго выражения равносильно неявному сравнению ненулевой константы с нулем.

А.9 . 6 . О ператоры перехода Операторы перехода выполняют безусловную передачу управления. опера тор - перехода : goto идентифика тор cont inue

;

Ьreak ; re turn выражениенеаб

В операторе goto указанный иде н тифика т ор должен быть меткой (см. раз­ дел А.9. 1 ), расположенной в текущей функции. Управление передается оператору с мет­ кой. Оператор cont i nue может находиться только внутри цикла. Он вызывает переход к следующей итерации самого внутреннего цикла, содержащего его. Говоря более точно, в каждой из следующих конструкций оператор cont inue, не вложенный в более внут­ ренний блок, эквивалентен go t o cont in: do { for ( . . . ) { whi l e ( . . . ) { c ont i n :

}

;

c on t in : } whi l e

c ont i n :

(. . .) ;

}

;

Оператор br eak можно использовать только в операторах цикла или в s w i t ch. Он завершает работу самого внутреннего цикла или swi t ch, содержащего данный оператор break, после чего управление переходит к следующему оператору после только что за­ вершенного тела. С помощью оператора return функция возвращает управление в программу, откуда она была вызвана. Если после return стоит выражение, то его значение возвращается вызвавшей программе. Значение выражения приводится к типу, возвращаемому функци­ ей, по тем же правилам, что и в процессе присваивания. Ситуация, когда нормальный ход операций приводит в конец функции (т.е. к послед­ ней закрывающей фигурной скобке), равносильна выполнению оператора re turn без выражения. В этом случае возвращаемое значение не определено.

СПРАВОЧ Н О Е РУКОВОДСТВ О ПО ЯЗЫКУ С

241

А. 1 О . В не ш н ие объявл е н и я Материал, подготовленный в качестве входных данных для компилятора С, называет­ ся единицей трансляции. Такая единица состоит из последовательности внешних объявле­ ний, каждое из которых представляет собой либо объявление, либо определение функции. единица - трансляции : внешне е - о бъявление единица - трансляции внешне е - объявление внешне е - о бъявление : определение - функции о бъявление

Область действия (видимости) внешних объявлений простирается до конца единицы трансляции, в которой они объявлены, точно так же, как область действия объявлений в блоке распространяется до конца этого блока. Синтаксис внешнего объявления не отли­ чается от синтаксиса любого другого объявления с одним исключением: исходный код функции можно определять только на этом внешнем уровне.

А. 1 0. 1 . О пределения функций Определение функции имеет следующий вид: определение - функции : специфика торы- о бъявлениянеоб описа тель список- о бъявленийнеоб с о с та вной- опера тор

Из спецификаторов класса памяти в специфика тора х - объявления разрешаются только ext ern и s t a t i c ; различия между ними рассматриваются в разделе А. 1 1 .2. Функция может возвращать значения арифметического типа, структуры, объедине­ ния, указатели и vo i d, но не "функции" и не "массивы". Описатель в объявлении функ­ ции должен явно указывать на то, что описываемый идентификатор имеет тип "функция", т.е. иметь одну из следующих двух форм (см. раздел А.8.6.3): с о б с тв енно - описа тель ( список- типов - параме тров ) собств енно - описа тель ( списо к - идентифика тор овнеоб )

Здесь с о б с тв е нн о - апи с а т ель есть просто идентификатор либо идентификатор, заключенный в круглые скобки. Заметим, что тип "функция" для описателя нельзя полу­ чить посредством typede f . Первая форма соответствует определению функции в новом стиле, для которого ха­ рактерно объявление параметров в списке - типов - параме тр о в вместе с их типами: после описателя не должно быть списка - объявлений. Если спи с о к - типов параме тров не состоит из единственного слова vo i d, показывающего отсутствие па­ раметров у функции, то в каждом описателе в списке - типов - параме тров обязан присутствовать идентификатор. Если спи с о к - типов - параме тров заканчивается символами " , . . . ", то в вызове функции может быть больше аргументов, чем парамет­ ров в ее определении. В таком случае, чтобы обращаться к дополнительным аргументам, следует пользоваться механизмом макроопределения va_ arg из заголовочного файла < s t da rg . h > , описанного в приложении Б. Функции с переменным количеством аргу­ ментов должны иметь по крайней мере один именованный параметр.

242

ПРИЛ ОЖЕНИЕ А

Вторая форма записи - это определение функции в старом стиле. Спис ок ­ содержит имена параметров, а список - о бъявлений приписыва­ ет им типы. В списке - о бъявлений разрешено объявлять только именованные пара­ метры, инициализация запрещается, и из спецификаторов класса памяти допускается только reg i s t e r. В обоих стилях определения функций параметры считаются как бы объявленными в самом начале составного оператора, образующего тело функции, и совпадающие с ними имена здесь объявляться не должны (хотя, как и любые идентификаторы, их можно пе­ реобъявить в более внутренних блоках). Объявление параметра "массив типа ... " тракту­ ется как "указатель на тип . . . ". Аналогично, объявление параметра с типом "функция, воз­ вращающая тип . . . " трактуется как "указатель на функцию, возвращающую тип . . . ". В мо­ мент вызова функции ее аргументы соответствующим образом преобразуются и присваиваются параметрам (см. раздел А.7.3 .2). идентифика торов

Новый стиль определения функций введен стандартом ANSI. Внесены также неболь­ шие изменения в операции расширения типа; в первой редакции языка параметры типа f l oat следовало воспринимать как douЬle. Различие между f l oat и douЫ e станови­ лось заметным лишь тогда, когда внутри функции генерировался указатель на параметр.

Ниже приведен законченный пример определения функции в новом стиле: int Ь , int с )

int max ( int а ,

{

int m ; m = (а > Ь) ? а: Ь; return ( m > с ) ? m

:

с;

где int - спецификатор объявления; max ( i nt а , int Ь , i nt с ) - описатель функции; { . . . } - блок, задающий ее код. Определение той же функции в старом стиле выглядит следующим образом : int max ( a , Ь , с ) int а , Ь , с ;

{

/* . . . */

Здесь уже max ( а , Ь , раметров.

с) -

описатель, а in t а , Ь ,

с-

список объявлений па­

А. 1 0.2. Внешние объявления Внешние объявления задают характеристики объектов, функций и других идентифи­ каторов. Термин "внешний" здесь используется, чтобы подчеркнуть расположение объ­ явлений вне функций; напрямую с ключевым словом ext e rn ("внешний") он не связан. Класс памяти для объекта с внешним объявлением либо вообще не указывается, либо указывается в виде ext e rn или s t a t i c . В одной единице трансляции для одного идентификатора может содержаться не­ сколько внешних объявлений, если они согласуются друг с другом по типу и способу связывания и если для этого идентификатора существует не более одного определения. Два объявления объекта или функции считаются согласованными по типу в соответ­ ствии с правилами, рассмотренными в разделе А.8 . 1 0. Кроме того, если объявления отСПРАВОЧНО Е РУКОВОДСТВО ПО Я ЗЫКУ С

243

личаются лишь тем, что в одном из них тип структуры, объединения или перечисления неполон (см. раздел А.8.3), а в другом соответствующий ему тип с той же меткой полон, то такие типы считаются согласованными . Если два типа массива отличаются тем, что один - полный (см. раздел А.8.6.2), а другой - неполный, то такие типы также счита­ ются согласованными при условии их совпадения во всем остальном. Наконец, если один тип определяет функцию в старом стиле, а другой - ту же функцию в новом стиле (с объявлениями параметров), то такие типы также считаются согласованными. Если первое внешнее объявление функции или объекта содержит спецификацию s t a t i c , то объявленный идентификатор имеет внутреннее связывание; в противном случае - внешнее связывание. Способы связывания рассматриваются в разделе А. 1 1 .2. Внешнее объявление объекта считается определением, если оно имеет инициализа­ тор. Внешнее объявление, в котором нет инициализатора и спецификатора ext e rn, на­ зывается предварительным определением. Если в единице трансляции появится опреде­ ление объекта, то все его предварительные определения станут считаться просто лишни­ ми объявлениями. Если никакого определения для этого объекта в единице трансляции не будет найдено, все его предварительные определения будут считаться одним опреде­ лением с инициализатором О . Каждый объект должен иметь ровно одно определение. Для объекта с внутренним связыванием это правило относится к каждой отдельной единице трансляции, поскольку объекты с внутренним связыванием в каждой единице уникальны. В случае объектов с внешним связыванием указанное правило действует в отношении всей программы. Хотя правило "одного определения" формулируется несколько иначе, чем в первой ре­ дакции языка, по существу оно совпадает с прежним. Некоторые реализации его ослаб­ ляют, более широко трактуя понятие предварительного определения. В другом вари ан­ те указанного правила, распространенном в системах Uпix и признанном в стандарте как общепринятое расширение, все предварительные определения объектов с внешним связыванием из всех единиц трансляции программы рассматриваются вместе, а не от­ дельно в каждой единице. Если где-то в программе обнаруживается полное определе­ ние, то предварительные определения становятся просто объявлениями, но если ника­ кого полного определения нет, то все предварительные определения становятся одним определением с инициализатором О.

А . 1 1 . Область де йствия и связывание Каждый раз компилировать всю программу нет необходимости: ее исходный текст можно хранить в нескольких файлах, представляющих собой единицы трансляции, а ра­ нее скомпилированные функции можно загружать из библиотек. Связи между функция­ ми программы могут осуществляться через вызовы и обращение к внешним данным. Следовательно, существуют два вида областей действия (видимости) объектов: во­ первых, лексическая область действия идентификатора, т.е. область в тексте програм­ мы, где компилятором воспринимаются все его характеристики ; во-вторых, область дей­ ствия, ассоциируемая с объектами и функциями, имеющими внешнее связывание. Внеш­ нее связывание определяет связи, устанавливаемые между идентификаторами из раз­ дельно компилируемых единиц трансляции.

244

ПРИЛ ОЖЕНИЕ А

А. 1 1 . 1 . Лексическая область действия Идентификаторы относятся к нескольким пространствам имен, никак не связанным друг с друтом. Один и тот же идентификатор может использоваться в разных смыслах даже в одной области действия, если в разных употреблениях он принадлежит к разным пространствам имен. Ниже перечислены классы объектов, имена которых представляют собой отдельные независимые пространства. •

Объекты, функции, типоопределяющие (typede f ) имена и константы перечислимых типов.



Метки операторов для перехода.



Метки структур, объединений и перечислимых типов.



Элементы каждой отдельной структуры или объединения. Сформулированные правила в нескольких аспектах отличаются от прежних, описанных в первом издании этого справочного руководства. Метки операторов не имели раньше собственного пространства; метки структур и объединений (а в некоторых реализациях и перечислим ых типов) имели отдельные пространства. Помещение меток структур, объединений и перечислений в одном общем пространстве - зто дополнительное ог­ раничение, которого раньше не существовало. Наиболее существенное отклонение от первой редакции состоит в том, что каждая отдельная структура или объединение соз­ дает свое собственное пространство имен для своих элементов. Таким образом, одно и то же имя может использоваться в нескольких различных структурах. Это правило ши­ роко применяется уже в течение многих лет.

Лексическая область действия идентификатора объекта ( или функции), объявленного во внешнем объявлении, начинается с места, где заканчивается его описатель, и распро­ страняется до конца единицы трансляции, в которой он объявлен. Область действия па­ раметра в определении функции начинается с начала блока, представляющего собой тело функции, и распространяется на всю функцию; область действия параметра в объявлении функции заканчивается в конце этого объявления. Область действия идентификатора, объявленного в начале блока, начинается от места, где заканчивается его описатель, и продолжается до конца этого блока. Областью действия для метки перехода является вся функция, где эта метка встречается. Область действия метки структуры, объединения или перечисления, а также константы перечислимого типа начинается от ее появления в спецификаторе типа и продолжается до конца единицы трансляции (для объявления на внешнем уровне) или до конца блока (для объявления внутри функции). Если идентификатор явно объявлен в начале блока, в частности тела функции, то лю­ бое объявление того же идентификатора, находящееся снаружи этого блока, временно перестает действовать при входе в блок вплоть до его конца.

А. 1 1 . 2. Связывание В пределах одной единицы трансляции все объявления одного и того ж е идентифика­ тора с внутренним связыванием относятся к одному и тому же оСъекту или функции, причем этот объект уникален для данной единицы трансляции. В случае внешнего связы ­ вания все объявления одного и того же идентификатора относятся к одному и тому же объекту, но уже в пределах всей программы.

СПРАВ ОЧ Н О Е РУКОВОДСТВО ПО Я ЗЫКУ С

245

Как говорилось в разделе А. 1 0.2, если первое внешнее объявление идентификатора имеет спецификатор s t a t i c , оно дает идентификатору внутреннее связывание, а если такого спецификатора нет, то внешнее. Если объявление находится внутри блока и не содержит ext e rn, то соответствующий идентификатор не имеет связывания и уникален для данной функции. Если объявление содержит ext e rn и блок находится в области действия внешнего объявления этого идентификатора, то последний имеет тот же способ связывания, что и во внешнем объявлении, и относится к тому же объекту (функции). Однако если ни одного внешнего объявления для этого идентификатора нет, то его спо­ соб связывания - внешний.

А. 1 2 . П репроце ссор Препроцессор языка С выполняет макроподстановку, условную компиляцию и вклю­ чение именованных файлов. Строки, начинающиеся со знака # (перед которым разреше­ ны символы пустого пространства), задают препроцессору инструкции-директивы. Их синтаксис не зависит от остальной части языка; они могут фигурировать где угодно и оказывать влияние (независимо от области действия) вплоть до конца единицы трансля­ ции. Границы строк принимаются во внимание: каждая строка анализируется отдельно (но есть и возможность сцеплять строки; подробнее об этом в разделе А. 1 2.2). Лексема­ ми для препроцессора являются все лексемы языка и последовательности символов, за­ дающие имена файлов, как, например, в директиве # i nc lude (раздел А. 1 2.4). Кроме то­ го, любой символ, не определенный каким-либо другим способом, также воспринимает­ ся как лексема. Влияние символов пустого пространства, отличающихся от пробелов и горизонтальных табуляций, внутри строк препроцессора не определено. Сама препроцессорная обработка выполняется в несколько логически последователь­ ных этапов. В отдельных реализациях некоторые этапы объединены вместе. 1.

Прежде всего, комбинации из трех символов, описанные в разделе А. 1 2. 1 , заме­ няются их эквивалентами. Между строками исходного кода вставляются символы конца строки, если этого требует операционная система.

2.

Пары символов, состоящие из обратной косой черты с последующим символом конца строки, удаляются из текста; тем самым строки "склеиваются" (раздел А. 1 2.2).

3. Программа разбивается на лексемы, разделенные символами пустого пространст­ ва. Комментарии заменяются единичными пробелами. Затем выполняются дирек­ тивы препроцессора и макроподстановки (разделы А. 1 2.3-А. 1 2. 1 0). 4. Управляющие последовательности в символьных константах и строковых лите­

ралах (разделы А.2.5.2, А.2.6) заменяются символами, которые они обозначают. Строковые литералы, записанные вплотную, сцепляются (подвергаются конкате­ нации). 5.

246

Результат транслируется. Затем компонуются внешние связи с другими програм­ мами и библиотеками посредством сбора всех необходимых программ и данных воедино, а также подключения ссылок на внешние функции и объекты к их опре­ делениям.

ПРИЛ ОЖЕНИЕ А

А. 1 2 . 1 . Комбинации из трех символов Множество символов, которыми набираются исходные тексты программ на С, со­ держится в семибитовом наборе ASCII, но является надмножеством инвариантного ко­ дового набора ISO 646- 1 983 (ISO 646- 1 983 Invariant Code Set). Чтобы дать возможность программам пользоваться сокращенным набором символов, все перечисленные ниже комбинации трех символов при препроцессорной обработке заменяются на соответст­ вующие им единичные символы. Замена выполняется до начала любой другой обработки. ??=

#

?? (

??


!

??-

?? '

??

Никакие другие замены, кроме указанных, не выполняются. Комбинации из трех символов (trigraphs) введены в яз ы к стандартом ANSI.

А. 1 2 . 2 . Слияние строк Каждая строка, заканчивающаяся обратной косой чертой, сцепляется в единое целое со следующей, поскольку символ \ и следующий за ним символ конца строки удаляются. Это делается перед разбивкой текста на лексемы.

А. 1 2.3. М акроопределения и их раскрытие Управляющая строка следующего вида заставляет препроцессор заменять иденти ­ фика тор н а последо в а тель но сть -лексем везде далее п о тексту программы : #de f i ne идентифика тор последова тельность-лексем

При этом символы пустого пространства в начале и в конце последовательности лек­ сем выбрасываются. Повторная строка #de f i ne с тем же идентификатором считается ошибкой, если последовательности лексем не идентичны (несовпадения в символах пус­ того пространства не играют роли). Строка следующего вида, где между первым идентифика тором и открывающей крутлой скобкой не должно быть символов пустого пространства, представляет собой макроопределение с параметрами, задаваемыми списком - идентифика торов : #de f i ne идентифика тор ( списо к - идентифика торов) по следова тельно с ть -лексем

Как и в первой форме, символы пустого пространства в начале и в конце последова­ тельности лексем выбрасываются, и макрос может быть повторно определен только с идентичным по количеству и именам списком параметров и с той же последовательно­ стью лексем. Управляющая строка следующего вида приказывает препроцессору "забыть" опреде­ ление, данное идентифика тору: #unde f идентифика тор

Применение директивы #unde f к не определенному ранее идентификатору не счита­ ется ошибкой.

СПРАВОЧНОЕ РУКО В ОДСТВ О ПО ЯЗЫКУ С

247

Если макроопределение было задано во второй форме, то любая следующая далее в тексте программы цепочка символов, состоящая из идентификатора макроса (возможно, с последующими символами пустого пространства), открывающей скобки, списка лек­ сем, разделенных запятыми, и закрывающей скобки, представляет собой вызов макроса. Аргументами вызова макроса являются лексемы, разделенные запятыми, причем запя­ тые, взятые в кавычки или вложенные скобки, в разделении аргументов не участвуют. Во время группировки аргументов раскрытие макросов в них не выполняется. Количество аргументов в вызове макроса должно соответствовать количеству параметров макрооп­ ределения. После выделения аргументов из текста символы пустого пространства, окру­ жающие их, отбрасываются. Затем в замещающей последовательности лексем макроса каждый идентификатор-параметр, не взятый в кавычки, заменяется на соответствующий ему фактический аргумент из текста. Если в замещающей последовательности перед па­ раметром не стоит знак # , если и ни перед ним, ни после него нет знака # # , то лексемы аргумента проверяются на наличие в них макровызовов; если таковые есть, то до под­ становки аргумента в нем выполняется раскрытие соответствующих макросов . На процесс подстановки влияют два специальных знака операций. Во-первых, если перед параметром в замещающей строке лексем вплотную стоит знак #, то вокруг соот­ ветствующего аргумента ставятся строковые кавычки ( " ), а потом идентификатор пара­ метра вместе со знаком # заменяется получившимся строковым литералом. Перед каж­ дым символом " или \ , встречающимся вокруг или внутри строковой или символьной константы, автоматически вставляется обратная косая черта. Во-вторых, если последовательность лексем в макроопределении любого вида со­ держит знак # # , то сразу после подстановки параметров он вместе с окружающими его символами пустого пространства отбрасывается, благодаря чему сцепляются соседние лексемы, образуя тем самым новую лексему. Результат не определен при генерировании таким образом недопустимых лексем языка или в случае, когда получающийся текст за­ висит от порядка применения операции # # . Кроме того, знак ## не может стоять ни в начале, ни в конце замещающей последовательности лексем. В макросах обоих видов замещающая последовательность лексем повторно просмат­ ривается в поиске новых de f inе-идентификаторов. Однако если какой-либо идентифи­ катор уже был заменен в текущем процессе раскрытия, повторное появление такого идентификатора не вызовет его замены; он останется нетронутым. Даже если развернутая строка макровызова начинается со знака #, она не будет воспринята как директива препроцессора. стандарте ANSI процесс раскрытия макросов определен более точно, чем в первом издании книги. Наиболее важные изменения - зто введение операций # и # # , которые позволяют брать аргументы в кавычки и выполнять конкатенацию. Некоторые из новых правил, особенно касающиеся конкатенации, могут показаться странноватыми. (См. приведенные ниже примеры .)

В

Описанные возможности можно, например, применять для введения "буквальных констант": # de f ine TAB S I ZE 1 0 0 int t aЬ l e [ TABS I Z E ] ;

Следующее определение задает макрос, возвращающий абсолютное значение разно­ сти его аргументов: (Ь) - (а) ) # de f ine ABSD I F F ( a , Ь ) ( (а) > (Ь) ? (а) - (Ь)

248

ПРИЛ ОЖЕНИЕ А

В отличие от функции, делающей то же самое, аргументы и возвращаемое значение здесь могут иметь любой арифметический тип и даже быть указателями. Кроме того, ар­ гументы, каждый из которых может оказывать побочный эффект, вычисляются дважды: один раз при проверке условия и второй раз при вычислении результата. Пусть имеется такое макроопределение: #de f ine t emp f i l e ( di r )

#di r " / % s "

Макровызов temp f i l e ( / u s r / tmp ) даст при раскрытии следующее: 11 / u s r / t mp " 11 / % s 11

Далее эти две строки будут сцеплены в одну. Вот еще один пример макроопределения: #de f ine c a t ( x , y ) х # # у

Вызов c a t ( var , 1 2 3 ) сгенерирует результат va r 1 2 3 . Однако вызов c a t ( c a t ( 1 , 2 ) , 3 ) не даст желаемого результата, поскольку операция ## помешает правильному раскрытию аргументов внешнего вызова c a t . В результате получится сле­ дующая цепочка лексем: cat ( 1 , 2 ) 3 где ) 3 - результат сцепления последней лексемы первого аргумента с первой лексемой второго аргумента - сам не является допустимой лексемой. Но можно задать второй уровень макроопределения в таком виде: # de f ine xcat ( x , y )

cat ( x , y)

Теперь все пройдет благополучно, и вызов х е а t ( х с а t ( 1 , 2 ) , 3 ) действительно даст результат 12 3 , потому что в раскрытии самого макроса xcat не участвует знак # # . Аналогично, макровызов ABS D I F F ( ABSD I FF ( а , Ь ) , с ) при раскрытии даст ожидаемый, полностью развернутый результат.

А. 1 2. 4 . В кл ючение файлов Следующая управляющая строка заменяется препроцессором на все содержимое файла с именем имя - фа йла : # i nc lude < имя - файла >

Среди символов, составляющих имя - ф а йла , не должно быть знака > и конца стро­ ки ; результат не определен, если имя - фа йла содержит любой из символов " , ' , \ или / * . Порядок поиска указанного файла по его имени в ресурсах системы зависит от реа­ лизации. Аналогично выполняется и управляющая строка # i nc l ude " имя - файла "

Вначале поиск файла выполняется по тем же правилам, по каким компилятор ищет сам файл исходного кода (это определение нестрого и зависит от реализации), а в случае неудачи - так же, как в # i nc lude первого типа. Результат остается неопределенным, если имя файла содержит " , \ или / *, а вот использование знака > разрешается . Третья директива не совпадает ни с одной из предыдущих форм: # i nc lude п о следов а тель н о с ть - лексем

Она интерпретируется путем раскрытия по сл едов а т ель но с ти - л е к с ем как нор­ мального текста, который в результате всех макроподстановок должен дать СПРАВ ОЧ Н О Е РУКОВОДСТВО ПО Я ЗЫКУ С

249

# inc 1 ude < > или # inc 1 ude 11 11 • Полученная таким образом директива далее будет обрабатываться в соответствии с полученной формой. Допускается вложение данных директив, т.е. файлы, включаемые с помощью # i nc lude, также могут содержать директивы # i nc l ude . •

.









А. 1 2. 5 . Условная компиляция Части программы могут компилироваться условно, если они оформлены в соответст­ вии со следующей синтаксической схемой: условная- конструкция - препроце ссора : с трока - i f текст блоки - е l i f блок - еl sенеоб # endi f с тр ока - i f : # i f конста нтно е - выражение # i f de f идентифика тор # i fnde f идентифика тор блоки- еl i f : c тpoкa - e l i f текст блоки- еl i fнеоб c тpoкa - el i f : # e l i f конс та нтно е - выражение блок - еl sе : с тр ока - еl sе текс т с трока - еl sе : #else

Каждая и з директив ( с трока - i f, c тpoкa - e l i f, с трока - е l s е и #endi f ) запи­ сывается в отдельной строке. Константные выражения в директиве # i f и последующих строках # e l i f вычисляются по порядку, пока не будет обнаружено выражение с нену­ левым значением; текст, следующий за строкой с нулевым значением, отбрасывается. Текст, расположенный за директивой с ненулевым значением, обрабатывается обычным образом. Под словом "текст" здесь имеется в виду любой текстовый материал, включая строки препроцессора, не входящие в условную структуру; текст может быть и пустым. Если строка # i f или # e l i f с ненулевым значением выражения найдена и ее текст об­ работан, то последующие строки # e l i f и # e l s e вместе со своими текстами отбрасы­ ваются. Если все выражения имеют нулевые значения и присутствует строка # e l s e , то следующий за ней текст обрабатывается обычным образом. Тексты неактивных ветвей условных конструкций игнорируются; выполняется только проверка вложенности услов­ ных конструкций. Константные выражения в # i f и # e l i f подвергаются обычной макроподстановке. Более того, перед раскрытием макросов выражения следующего вида всегда заменяются константами l L , если указанный идентифика тор определен, и O L , если не определен: de f ined идентифика тор de f ined ( идентифика тор )

250

П Р ИЛ ОЖЕН ИЕ А

Все идентификаторы, оставшиеся после макроподстановки, заменяются на O L . Нако­ нец, предполагается, что любая целая константа всегда имеет суффикс L, т.е. все ариф­ метические операции работают только с операндами типа l ong или uns igned l ong. Получившееся константное выражение (см. раздел А.7 . 1 9) должно подчиняться неко­ торым ограничениям: быть целочисленным, не содержать перечислимых констант, пре­ образований типов и операций s i z e o f . Имеются также следующие управляющие директивы: # i fde f идентифика тор # i fnde f идентифика тор

Они эквивалентны соответственно строкам # i f de f in ed идентифика тор # i f ! de f ined идентифика тор Директивы # e l i f не было в первой версии языка, хотя она и использовалась в некото­ рых препроцессорах. Конструкция de f ined также новая. -

А. 1 2 .6. Нумерация строк Для удобства работы других препроцессоров, генерирующих программы на С, введе­ ны следующие директивы: # 1 ine константа 11 имя - файла 11 # l ine константа

Эти директивы предписывают компилятору считать, что указанные в них десятичное целое число и идентификатор являются соответственно номером следующей строки и именем текущего файла. Если имя файла отсутствует, то ранее запомненное имя не изменя­ ется. Раскрытие макровызовов в директиве # l ine выполняется до ее интерпретации.

А. 1 2 .7. Генерирование сообщений об ошибках Директива препроцессора следующего вида приказывает ему выдать диагностическое сообщение, включающее заданную последовательность лексем: # e rror п о следова тельно с ть -лексемнеоб

А. 1 2.8. Директива

#pragma

Управляющая строка следующего вида предписывает препроцессору выполнить не­ которую операцию, зависящую от реализации: #pragma последова тельность -лексемнеоб

Неопознанная директива этого вида просто игнорируется.

А. 1 2 .9. Пустая директива Строка препроцессора следующего вида не вызывает никаких операций: #

СПРАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

251

А. 1 2 . 1 0. Заранее определенные имена Существует несколько заранее определенных препроцессором идентификаторов, ко­ торые он заменяет специальной информацией. Эти идентификаторы (и выражения с опе­ рацией препроцессора de f ined) нельзя переопределять или отменять директивой #unde f . _L I NE _ F I LE

Номер текуще й строки исходного текста (деся тичная константа) И м я компилируемого фа й ла ( сим в ольная строка) " Ммм

_DATE_

Дата компил я ции в в иде

_T I ME_

Врем я компил я ции в виде

sтDc

дд

гггг "

" ч ч : мм : с е "

(сим в ольная строка)

(символьная строка)

Константа 1. Предполагается , что этот идентификатор определен со значением 1 только в тех реализаци я х , которые следуют стандарту

Директивы #e rror и #p ragma впервые введены стандартом ANS I . Заранее опреде­ ленные макросы препроцессора также до сих пор не были стандартизированы, хотя и использовались в некоторых реализациях.

А . 1 3 . Грамма тика

Ниже приведена сводка грамматических правил, которые уже рассматривались в дан­ ном приложении. Они имеют то же содержание, но даны в другом порядке. В этой грамматике не приводятся определения таких терминальных (фундамен­ тальных, базовых) символов, как цело численна я - к о н с т а н т а , симв ольна я ­ к о н с т а нта, в еще с тв е нна я - к о н с т а н т а , идентифика т ор, с тр о ка и к о нс та н ­ та - п ер е числимого - тип а . Слова и символы, набранные полужирныи моноши ­ ринныи латинским шрифтом, являются терминальными символами и используются точ­ но в таком виде, как записаны. Данную грамматику можно механически преобразовать во входные данные для автоматического генератора синтаксических анализаторов. Кро­ ме добавления синтаксической разметки, предназначенной для указания альтернатив в правилах подстановки, для этого потребуется раскрыть конструкции "один из", а также (в зависимости от правил генератора анализаторов) ввести дубликаты каждого правила с индексом н е о б : записать один вариант правила с символом, а второй без него. Если вне­ сти еще одно изменение, а именно удалить правило тип о о пр еделяюще е имя : идентифик а т ор и объявить тип о о пр еделяюще е - имя терминальным симво­ лом, данная грамматика сможет служить входными данными для генератора синтаксиче­ ских анализаторов УАСС. Ей присуще лишь одно противоречие, вызванное неоднознач­ ностью конструкции i f - e l s e . единиц а тра нсляции : -

внешне е - о бъявление единица - трансляции внешне е - объявление внешне е - объявление : определение - функции объявление

252

П РИЛОЖЕ НИЕ А

определение - функции : специфика торы- о бъявлениянеов описа тель список - о бъявлениЙнеов со ­ ставной- опера тор объявление : специфика торы- о бъявления списо к - иниц - описа телейнеов список - о бъявлений : о бъявление список - о бъявлений о бъявление специфика торы- о бъявления : спе цифика тор - кла сса - памяти специфика торы- объявлениянеоб специфика тор - типа специфика торы- о бъявлениянеов модифика тор - типа спе цифика торы- объявлениянеоб специфика тор - кла сса - памяти : один из auto

regi s t e r

static

ex t e rn

typede f

специфика тор - типа : один из vo id char s ho r t int long f l oat douЬ l e s i gned uns i gned специфика тор - с труктуры-или - о бъ единения

специфика тор - пер е числения типо определяюще е - имя модифика тор - типа : один из cons t

vo l a t i l e

специфика тор - с труктуры- или - о бъ единения : s t ru c t - или- uni on идентифика торнеов { список - о бъявлений- структуры s t ru c t -или - un i on идентифика тор

}

s t ru c t -или- un i on : один и з s t ruc t

un ion

список- объявлений- с труктуры : о бъявление - с труктуры список- объявлений- с труктуры объявление - с труктуры список-иниц - описа телей : иниц - описа тель списо к - иниц - описа телей , иниц - описа тель иниц - описа тель : описа тель описа тель = инициализ а тор объявление - с труктуры : список- специфика торов -модифика торов список - описа телей- структуры списо к - специфика торов-модифика торов : специфика тор - типа списо к - специфика торов -модифика торовнеов модифика тор - типа список - специфика торов-модифика тор овнеоб списо к - описа телей- с труктуры : описа тель - структуры списо к - описа телей- с труктуры ,

СП РАВОЧНО Е РУКОВОДСТВО ПО ЯЗЫКУ С

описа тель - с труктуры

253

описа тель - с труктуры : описа тель описа тельнеоб : константн о е - выражение специфика тор - пере числения : enum идентифика ТОРнеоб {

списо к - пер е числимых

}

enum идентифика тор список- пер е числимых : пере числимо е спис о к - пер е числимых , пере числимо е : идентифика тор идентифика тор

пере числимое

константное - выражение

описа тель : ука з а тельнеоб с о б с тв енно - описа тель с о б с тв енн о - описа тель : иде н тифика тор ( описа тель )

]

с о б с тв енно - описа тель

конста нтно е - выражениенеоб

с о б с тв енно - описа тель

список - типов - параме тров )

с о б с т в енно - описа тель

спис о к - идентифика торовнеоб )

ука за тель : * спис о к -модифика торов - типанеоб * списо к - модифика торов - типанеоб ука з а тель список- модифика торов - типа : модифика тор - типа списо к -модифика торов - типа модифика тор - типа спис о к - типов - параме тров : списо к - параме тров спис о к - параме тров , спис о к - параме тров : о бъявление - параме тра спис о к - параме тров , объявление - параме тра о бъявление - параме тра : специфика торы- объявления описа тель специфика торы- объявления а б с трактный- описа тельнеоб списо к - идентифика торов : идентифика тор спис о к - идентифика торов ,

идентифика тор

инициализ а тор : выражение - присв аив а ния { список- инициализ а торов } { спис о к - инициализ а торов , спис о к - инициализ а торов : инициализ а тор спис о к - инициализ а торов ,

254

}

инициализ а тор

П РИЛОЖЕ НИЕ А

имя - типа : список - специфика торов-модифика торов а б с трактный- аписа тельнеоб а б с трактный- описа тель : ука з а тель ука за тельнеоб с о б с тв енно - а б с трактный- описа тель собств е нно - а б с тра ктный- описа тель : ( а б с трактный- опис а т ель )

]

собственно - а бстра ктный - о пи са тельнеоб

конста нтное - выражениенеоб

собственно - а бстрактный- описа тельнеоб

списо к - типов - параметровнеоб

типоопределяюще е - имя : идентифика тор опера тор : опера тор - с -ме ткой опера тор-выражение с о с т а в ной- опера тор опера тор - выб ора опера тор - цикла опера тор - перехода опера тор - с -ме ткой : идентифика тор :

опера тор

с а в е константно е - выражение de f au l t

:

опер а т ор

опер а т ор

опер а т ор - выражение : выражениенеоб ; с о с та вной- опера тор : { список - о бъявленийнеоб список- опер а т оровнеоб

}

список- опера торов : опера тор список- опера торов опера тор опера тор - выбора : i f ( выражение ) if

( выражение )

swi tch

опера тор опера тор е l в е опера тор

( выражение )

опера тор - цикла : whi l e ( выражение ) do опера тор whi l e for

опера тор

опер а т ор

( выражение ) ;

( выражениенеоб

выражениенеоб

выражениенеоб )

опера тор

опера тор - перехода : goto идентифика тор cont inue ; break ; r e turn выраже ниенеоб выражение : выражение - присв аив а ния выражение , выражение - присв аив а ния

СП РАВОЧНО Е РУКОВОДСТВО ПО ЯЗЫКУ С

255

выражение - присв аив а ния : условно е - выражение одноме с тно е - выражение зна к - присв а ив а ния выражение - присв аивания зна к - присв аив а ния : один и з *=

/=

%=

+=

=

услов но е - выражение : выражение - с -логиче ским- ИЛИ выражение - с -логиче ским-ИЛИ ? выражение

&=

1=

услов но е - выражение

конста нтно е - выражение : условно е - выражение выражение - с -логиче ским- ИЛИ : выражение - с -логиче ским- И выражение - с -логиче ским- ИЛИ

1 1 выражение - с -логическим- И

выражение - с -логиче ским - И : выражение - с - в ключающим-ИЛИ выражение - с -логиче ским- И && выражение - с - в ключающим-ИЛИ выражение - с - в ключающим- ИЛИ : выражение - с -исклю чающим-ИЛИ выражение - с - в ключающим-ИЛИ 1 выражение - с - исключающим- ИЛИ : выражение - с - И выражение - с - исключающим-ИЛИ

выражение - с -исключающим-ИЛИ

л

выражение - с - И

выр ажение - с - И : выражение -рав енства выражение - с - И & выражение -ра в енств а выражение -ра в енств а : выражение - о тношения выражение -ра в енств а выражение -рав енства

выражение - о тношения

! = выражение - о тношения

выражение - о тношения : выражение - со - сдвигом выражение - о тношения < выражение - со - сдвигом выражение - о тношения > выражение - с о - сдвигом выражение - о тношения < = выражение - со - сдвигом выражение - о тношения >= выражение - со - сдвигом выражение - с о - сдвигом : аддитив но е - выражение выражение - с о - сдвигом выражение - со - сдвигом

> > аддитивно е - выражение < < аддитивное - выражение

аддитивно е - выражение : муль типлика тивное- выражение аддитивно е - выражение + муль типлика тивное- выражение аддитив н о е - выражение - муль типлика тивное- выражение муль типлика тивное- выражение : выражение - прив едения- к - типу

256

П РИЛОЖЕ НИЕ А

муль типлика тивное- выражение * выражение - прив едения- к - типу муль типлика тивное- выражение / выражение - прив едения- к - типу муль типлика тив ное-выражение % выражение - прив едения - к - типу выражение - прив едения- к - типу : одноме стно е - выражение ( имя - типа ) выражение - прив едения- к - типу одноме стно е - выражение : постфиксное-выражение ++ одноме с тно е - выражение - - одноме с тно е - выражение зна к - одноме стной- опера ции выражение - прив едения- к - типу s i z eo f одноме с тно е - выражение s i z eo f ( имя - типа ) зна к - одноме с тной- опера ции : один и з &

*

+

постфиксно е - выражение : первично е - выражение постфиксно е - выражение выражение ] постфиксно е - выражение список - аргументов - выраженийнеоб постфиксно е - выражение идентифика тор постфиксно е - выражение - > идентифика тор постфиксно е - выражение + + постфиксно е - выражение первично е - выражение : идентифика тор конста нта с трока ( выражение списо к - аргументов - выражений : выражение - присв аив а ния спис о к - аргументов - выражений , выражение - присв аив а ния константа : цело численна я- конста нта симв ольная- конста нта в еще ственная- конста нта конста нта - пере числимого- типа

Ниже приведено формальное описание грамматики языка препроцессора, опреде­ ляющее структуру управляющих строк (директив). Для автоматического генерирования синтаксического анализатора это описание непригодно. Грамматика включает символ т е к с т, который обозначает обычный текст программы, безусловные управляющие строки препроцессора или его законченные условные конструкции. управляющая- строка : #de f ine идентифика тор последова тельно с ть -лексем #de f i ne идентифика тор ( идентифика тор , . . . , идентифика тор ) по ­ следова тель но с ть -лексем #unde f идентифика тор # inc lude

СП РАВОЧНОЕ РУКОВОДСТВО ПО ЯЗЫКУ С

257

# inc lude " имя - файла " # inc lude по следов а тельно с ть - лексем # l ine конста нта " имя - файла " # l ine константа # e rror п о следова тель н о с ть -лекс емнеоб #pragma п о следов а тель н о с ть - лексемнеоб # услов на я - конструкция- препроце с с ора услов ная - конструкция - препроце с с ора : с трока - i f текс т блоки - е l i f блок - еl в енеоб # endi f с трока - i f : # i f конста нтно е - выражение # i fde f идентифика тор # i fnde f идентифика тор блоки- е l i f : c тpoкa - e l i f текст блоки- е l i fнеоб c тpoкa - e l i f : # e l i f константно е - выражение блок - е l s е : с трока - е l s е текст с трока - е l s е : #else

258

П РИЛОЖЕНИЕ А

Приложение Б

Стан д артная б и б л и отека В этом приложении кратко описана библиотека, определенная стандартом ANSI. Стандартная библиотека С не является частью собственно языка, однако всякая среда, поддерживающая язык С, обязана предоставить программисту объявления функций, ти­ пов и макросов, содержащиеся в этой библиотеке. Здесь опущены описания некоторых функций, имеющих слишком ограниченное применение или легко синтезируемых на ос­ нове других; не рассматриваются многобайтные символы, а также вопросы локализации, т.е. свойства библиотеки, зависящие от конкретного языка, страны или культурной среды. Функции, типы и макросы стандартной библиотеки объявлены в стандартных заголо­ вочных файлах:

< f l oat . h >

< S tdarg . h >

< S tdl ib . h >

< Ctype . h >

< l im i t s . h >

< S e t j mp . h >

< S tdde f . h >

< s t r i ng . h >

< e rrno . h >

< loca l e . h>

< s i gnal . h >

< S tdio . h >

< t ime . h >

Обращение к заголовочному файлу (включение) выполняется следующим образом : # inc l ude < за голов о чный_ фа йл>

Заголовочные файлы можно включа