Главная              Рефераты - Информатика

Язык С - реферат

Аннотация.

Язык “C”(произносится “си”) - это универсальный язык программирования, для которого характерны экономичность выражения, современный поток управления и структуры данных, богатый набор операторов. Язык “C” не является ни языком “очень высокого уровня”, ни “большим” языком, и не предназначается для некоторой специальной области применения. но отсутствие ограничений и общность языка делают его более удобным и эффективным для многих задач, чем языки, предположительно более мощные.

Язык “C”, первоначально предназначавшийся для написания операционной системы “UNIX” на ЭВМ DEC PDP-11, был разработан и реализован на этой системе Деннисом Ричи. Операционная система, компилятор с языка “C” и по существу все прикладные программы системы “UNIX” (включая все программное обеспечение, использованное при подготовке этой книги) написаны на “C”. Коммерческие компиляторы с языка “C” существуют также на некоторых других ЭВМ, включая IBM SYSTEM/370, HONEYWELL 6000, INTERDATA 8/32. Язык “C”, однако, не связан с какими-либо определенными аппаратными средствами или системами, и на нем легко писать программы, которые можно пропускать без изменений на любой ЭВМ, имеющей “C”-компилятор.

Эта книга предназначена для того, чтобы помочь читателю научиться программировать на языке “C”. Она сдержит учебное введение, цель которого - позволить новым пользователям начать программировать как можно быстрее, отдельные главы по всем основным особенностям языка и справочное руководство. Обучение построено в основном на чтении, написании и разборе примеров, а не голой формулировке правил. Примеры, приводимые в книге, по большей части являются законченными реальными программами, а не отдельными фрагментами. Все примеры были проверены непосредственно с текста книги, где они напечатаны в виде, пригодном для ввода в машину. Кроме указаний о том, как сделать использование языка более эффективным, мы также пытались, где это возможно, проиллюстрировать полезные алгоритмы и принципы хорошего стиля и разумной разработки.

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

По нашему опыту , “C” показал себя приятным, выразительным и разносторонним языком на широком множестве разнообразных программ. Его легко выучить , и он не теряет своих качеств с ростом опыта программиста. Мы надеемся , что эта книга поможет вам хорошо его использовать.

Вдумчивая критика и предложения многих наших друзей и коллег очень много добавили как для самой книги, так и для нашего удовольствия при ее написании. В частности, Майк Биапси, Джим Блю, Стью Фельдман, Доуг Мак-Илрой, Билл Рум, Боб Розин и Ларри Рослер тщательно прочитали множество вариантов. Мы также обязаны Элю Ахо, Стиву Борну, Дэву Двораку, Чаку Хэлею, Дебби Хэлей, Мариону Харрису, Рику Холту, Стиву Джонсону, Джону Машею, Бобу Митцу, Ральфу Мьюа, Питеру Нельсону, Эллиоту Пинсону, Биллу Плагеру, Джерри Спиваку, Кену Томпсону и Питеру Вейнбергеру за полезные замечания на различных этапах и Майку Лоску и Джо Осанна за неоценимую помощь при печатании книги. Брайен В. Керниган Деннис М. Ричи


Содержание.

Аннотация

0.1. Введение

1. Учебное введение

1.1. Начинаем

1.2. Переменные и арифметика

1.3. Оператор FOR

1.4. Символические константы

1.5. Набор полезных программ

1.5.1. Ввод и вывод символов

1.5.2. Копирование файла

1.5.3. Подсчет символов

1.5.4. Подсчет строк

1.5.5. Подсчет слов

1.6. Массивы

1.7. Функции

1.8. Аргументы - вызов по значению

1.9. Массивы символов

1.10. Область действия: внешние переменные

1.11. Резюме

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

2.1. Имена переменных

2.2. Типы и размеры данных

2.3. Константы

2.3.1. Символьная константа

2.3.2. Константное выражение

2.3.3. Строчная константа

2.4. Описания

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

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

2.7. Преобразование типов

2.8. Операции увеличения и уменьшения

2.9. Побитовые логические операции

2.10. Операции и выражения присваивания

2.11. Условные выражения

2.12. Старшинство и порядок вычисления

3. Поток управления

3.1. Операторы и блоки

3.2. IF - ELSE

3.3. ELSE - IF

3.4. Переключатель

3.5. Циклы - WHILE и FOR

3.6. Цикл DO - WHILE

3.7. Оператор BREAK

3.8. Оператор CONTINUE

3.9. Оператор GOTO и метки

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

4.1. Основные сведения

4.2. Функции, возвращающие нецелые значения

4.3. Еще об аргументах функций

4.4. Внешние переменные

4.5. Правила, определяющие область действия

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

4.6. Статические переменные

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

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

4.9. Инициализация

4.10. Рекурсия

4.11. Препроцессор языка “C”

4.11.1. Включение файлов

4.11.2. Mакроподстановка

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

5.1. Указатели и адреса

5.2. Указатели и аргументы функций

5.3. указатели и массивы

5.4. Адресная арифметика

5.5. указатели символов и функции

5.6. Указатели - не целые

5.7. Многомерные массивы

5.8. Массивы указателей; указатели указателей

5.9. Инициализация массивов указателей

5.10. Указатели и многомерные массивы

5.11. Командная строка аргументов

5.12. Указатели на функции

6. Структуры

6.1. Основные сведения

6.2. Структуры и функции

6.3. Массивы структур

6.4. Указатели на структуры

6.5. Структуры, ссылающиеся на себя

6.6. Поиск в таблице

6.7. Поля

6.8. Объединения

6.9. Определение типа

7. Ввод и вывод

7.1. Обращение к стандартной библиотеке

7.2. Стандартный ввод и вывод - функции GETCHAR и PUTCHAR

7.3. Форматный вывод - функция PRINTF

7.4. Форматный ввод - функция SCANF

7.5. Форматное преобразование в памяти

7.6. Доступ к файлам

7.7. Обработка ошибок - STDERR и EXIT

7.8. Ввод и вывод строк

7.9. Несколько разнообразных функций

7.9.1. Проверка вида символов и преобразования

7.9.2. Функция UNGETC

7.9.3. Обращение к системе

7.9.4. Управление памятью

8. Интерфейс системы UNIX

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

8.2. Низкоуровневый ввод/вывод - операторы READ и WRITE

8.3. Открытие, создание, закрытие и расщепление (UNLINK)

8.4. Произвольный доступ - SEEK и LSEEK

8.5. Пример - реализация функций FOPEN и GETC

8.6. Пример - распечатка справочников

8.7. Пример - распределитель памяти

9. Приложение а: справочное руководство по языку 'C'.

9.1. Введение

10. Лексические соглашения

10.1. Комментарии

10.2. Идентификаторы (имена)

10.3. Ключевые слова

10.4. Константы

10.4.1. Целые константы

10.4.2. Явные длинные константы

10.4.3. Символьные константы

10.4.4. Плавающие константы

10.5. Строки

10.6. Характеристики аппаратных средств

11. Синтаксическая нотация

12. Что в имени тебе моем?

13. Объекты и L-значения

14. Преобразования

14.1. Символы и целые

14.2. Типы FLOAT и DOUBLE

14.3. Плавающие и целочисленные величины

14.4. Указатели и целые

14.5. Целое без знака

14.6. Арифметические преобразования

15. Выражения

15.1. Первичные выражения

15.2. Унарные операции

15.3. Мультипликативные операции

15.4. Аддитивные операции

15.5. Операции сдвига

15.6. Операции отношения

15.7. Операции равенства

15.8. Побитовая операция 'и'

15.9. Побитовая операция исключающего 'или'

15.10. Побитовая операция включающего 'или'

15.11. Логическая операция 'и'

15.12. Операция логического 'или'

15.13. Условная операция

15.14. Операция присваивания

15.15. Операция запятая

16. Описания

16.1. Спецификаторы класса памяти

16.2. Спецификаторы типа

16.3. Описатели

16.4. Смысл описателей

16.5. Описание структур и объединений

16.6. Инициализация

16.7. Имена типов

16.8. TYPEDEF

17. Операторы

17.1. Операторное выражение

17.2. Составной оператор (или блок)

17.3. Условные операторы

17.4. Оператор WHILE

17.5. Оператор DO

17.6. Оператор FOR

17.7. Оператор SWITCH

17.8. Оператор BREAK

17.9. Оператор CONTINUE

17.10. Оператор возврата

17.11. Оператор GOTO

17.12. Помеченный оператор

17.13. Пустой оператор

18. Внешние определения

18.1. Внешнее определение функции

18.2. Внешние определения данных

19. Правила, определяющие область действия

19.1. Лексическая область действия

19.2. Область действия внешних идентификаторов

20. Строки управления компилятором

20.1. Замена лексем

20.2. Включение файлов

20.3. Условная компиляция

21. Неявные описания

22. Снова о типах

22.1. Структуры и объединения

22.2. Функции

22.3. Массивы, указатели и индексация

22.4. Явные преобразования указателей

23. Константные выражения

24. Соображения о переносимости

25. Анахронизмы

26. Сводка синтаксических правил

26.1. Выражения

26.2. Описания

26.3. Операторы

26.4. Внешние определения

26.5. Препроцессор

27. Присваивание структуры

28. Тип перечисления

29. Таблица изображений непечатных символов языка “C”.


0.1. Введение.

Язык “C” является универсальным языком программирования. Он тесно связан с операционной системой “UNIX” , так как был развит на этой системе и так как “UNIX” и ее программное обеспечение написано на “C”. Сам язык , однако, не

связан с какой-либо одной операционной системой или машиной;

и хотя его называют языком системного программирования, так

как он удобен для написания операционных систем, он с равным

успехом использовался при написании больших вычислительных

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

Язык “C” - это язык относительно “низкого уровня”. В

такой характеристике нет ничего оскорбительного; это просто

означает, что “C” имеет дело с объектами того же вида, что и

большинство ЭВМ, а именно, с символами, числами и адресами.

Они могут объединяться и пересылаться посредством обычных

арифметических и логических операций, осуществляемых реаль-

ными ЭВМ.

В языке “C” отсутствуют операции, имеющие дело непос-

редственно с составными объектами, такими как строки симво-

лов, множества, списки или с массивами, рассматриваемыми как

целое. Здесь, например, нет никакого аналога операциям PL/1,

оперирующим с целыми массивами и строками. Язык не предос-

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

кроме статического определения и механизма стеков, обеспечи-

ваемого локальными переменных функций; здесь нет ни

“куч”(HEAP), ни “сборки мусора”, как это предусматривается в

АЛГОЛЕ-68. Наконец, сам по себе “C” не обеспечивает никаких

возможностей ввода-вывода: здесь нет операторов READ или

WRITE и никаких встроенных методов доступа к файлам. Все эти

механизмы высокого уровня должны обеспечиваться явно вызыва-

емыми функциями.

Аналогично, язык “C” предлагает только простые, после-

довательные конструкции потоков управления: проверки, циклы,

группирование и подпрограммы, но не мультипрограммирование,

параллельные операции, синхронизацию или сопрограммы.

Хотя отсутствие некоторых из этих средств может выгля-

деть как удручающая неполноценность (“выходит, что я должен

обращаться к функции, чтобы сравнить две строки символов

?!”), но удержание языка в скромных размерах дает реальные

преимущества. Так как “C” относительно мал, он не требует

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

Компилятор с “C” может быть простым и компактным. Кроме то-

го, компиляторы легко пишутся; при использовании современной

технологии можно ожидать написания компилятора для новой ЭВМ

за пару месяцев и при этом окажется, что 80 процентов прог-

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

существующих компиляторов. Это обеспечивает высокую степень

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

ния, имеющиеся в “C”, непосредственно поддерживаются боль-

шинством существующих ЭВМ, библиотека, необходимая во время

прогона изолированных программ, оказывается очень маленькой.

На PDP -11, например, она содержит только программы для

32-битового умножения и деления и для выполнения программ

ввода и вывода последовательностей. Конечно, каждая реализа-

ция обеспечивает исчерпывающую, совместимую библиотеку функ-

ций для выполнения операций ввода-вывода, обработки строк и

распределения памяти, но так как обращение к ним осуществля-

ется только явно, можно , если необходимо, избежать их вызо-

ва; эти функции могут быть компактно написаны на самом “C”.

· 8 -

Опять же из-за того , что язык “C” отражает возможности

современных компьютеров, программы на “C” оказываются доста-

точно эффективными, так что не возникает побуждения писать

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

тельным примером этого является сама операционная система

“UNIX”, которая почти полностью написана на “C”. Из 13000

строк программы системы только около 800 строк самого низко-

го уровня написаны на ассемблере. Кроме того, по существу

все прикладное программное обеспечение системы “UNIX” напи-

сано на “C”; подавляющее большинство пользователей системы

“UNIX”(включая одного из авторов этой книги) даже не знает

языка ассемблера PDP-11.

Хотя “C” соответствует возможностям многих ЭВМ, он не

зависит от какой-либо конкретной архитектуры машины и в силу

этого без особых усилий позволяет писать “переносимые” прог-

раммы, т.е. программы, которые можно пропускать без измене-

ний на различных аппаратных средствах. В наших кругах стал

уже традицией перенос программного обеспечения, разработан-

ного на системе “UNIX”, на системы ЭВМ: HONEYWELL, IBM и

INTERDATA. Фактически компиляторы с “C” и программное обес-

печение во время прогона программ на этих четырех системах,

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

сии фортрана американского национального института стандар-

тов (ANSI). Сама операционная система “UNIX” теперь работает

как на PDP-11, так и на INTERDATA 8/32. За исключением прог-

рамм, которые неизбежно оказываются в некоторой степени ма-

шинно-зависимыми, таких как компилятор, ассемблер и отлад-

чик. Написанное на языке “C” программное обеспечение иден-

тично на обеих машинах. Внутри самой операционной системы

7000 строк программы, исключая математическое обеспечение

языка ассемблера ЭВМ и управления операциями ввода-вывода,

совпадают на 95 процентов.

Программистам, знакомым с другими языками, для сравне-

ния и противопоставления может оказаться полезным упоминание

нескольких исторических, технических и философских аспектов

“C”.

Многие из наиболее важных идей “C” происходят от гораз-

до более старого, но все еще вполне жизненного языка BCPL ,

разработанного Мартином Ричардсом. Косвенно язык BCPL оказал

влияние на “C” через язык “B”, написанный Кеном Томпсоном в

1970 году для первой операционной системы “UNIX” на ЭВМ

PDP-7.

Хотя язык “C” имеет несколько общих с BCPL характерных

особенностей, он никоим образом не является диалектом пос-

леднего. И BCPL и “B” - “безтипные” языки; единственным ви-

дом данных для них являются машинное слово, а доступ к дру-

гим объектам реализуется специальными операторами или обра-

щением к функциям. В языке “C” объектами основных типов дан-

ных являются символы, целые числа нескольких размеров и чис-

ла с плавающей точкой. Кроме того, имеется иерархия произ-

водных типов данных, создаваемых указателями, массивами,

структурами, объединениями и функциями.

· 9 -

Язык “C” включает основные конструкции потока управле-

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

рование операторов, принятие решений (IF), циклы с проверкой

завершения в начале (WHILE, FOR) или в конце (DO) и выбор

одного из множества возможных вариантов (SWITCH). (Все эти

возможности обеспечивались и в BCPL, хотя и при несколько

отличном синтаксисе; этот язык предчувствовал наступившую

через несколько лет моду на структурное программирование).

В языке “C” имеются указатели и возможность адресной

арифметики. Аргументы передаются функциям посредством копи-

рования значения аргумента , и вызванная функция не может

изменить фактический аргумент в вызывающей программе. Если

желательно добиться “вызова по ссылке”, можно неявно пере-

дать указатель, и функция сможет изменить объект, на который

этот указатель указывает. Имена массивов передаются указани-

ем начала массивов, так что аргументы типа массивов эффек-

тивно вызываются по ссылке.

К любой функции можно обращаться рекурсивно, и ее ло-

кальные переменные обычно “автоматические”, т.е. Создаются

заново при каждом обращении. Описание одной функции не может

содержаться внутри другой, но переменные могут описываться в

соответствии с обычной блочной структурой. Функции в “C” -

программе могут транслироваться отдельно. переменные по от-

ношению к функции могут быть внутренними, внешними, но из-

вестными только в пределах одного исходного файла, или пол-

ностью глобальными. Внутренние переменные могут быть автома-

тическими или статическими. Автоматические переменные для

большей эффективности можно помещать в регистры, но объявле-

ние регистра является только указанием для компилятора и ни-

как не связано с конкретными машинными регистрами.

Язык “C” не является языком со строгими типами в смысле

паскаля или алгола 68. Он сравнительно снисходителен к пре-

образованию данных, хотя и не будет автоматически преобразо-

вывать типы данных с буйной непринужденностью языка PL/1.

Существующие компиляторы не предусматривают никакой проверки

во время выполнения программы индексов массивов, типов аргу-

ментов и т.д.

В тех ситуациях, когда желательна строгая проверка ти-

пов, используется специальная версия компилятора. Эта прог-

рамма называется LINT очевидно потому, она выбирает кусочки

пуха из вашей программы. Программа LINT не генерирует машин-

ного кода, а делает очень строгую проверку всех тех сторон

программы, которые можно проконтролировать во время компиля-

ции и загрузки. Она определяет несоответствие типов, несов-

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

зом неинициализированные переменные, потенциальные трудности

переносимости и т.д. Для программ,которые благополучно про-

ходят через LINT, гарантируется отсутствие ошибок типа при-

мерно с той же полнотой, как и для программ, написанных,

например, на АЛГОЛЕ-68. Другие возможности программы LINT

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

· 10 -

Наконец, язык “C”, подобно любому другому языку, имеет

свои недостатки. Некоторые операции имеют неудачное старшин-

ство; некоторые разделы синтаксиса могли бы быть лучше; су-

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

талями. Тем не менее язык “C” зарекомендовал себя как исклю-

чительно эффективный и выразительный язык для широкого раз-

нообразия применений программирования.

Содержание книги организовано следующим образом. Глава

1 является учебным введением в центральную часть языка “C”.

Цель - позволить читателю стартовать так быстро,как только

возможно, так как мы твердо убеждены, что единственный спо-

соб изучить новый язык - писать на нем программы. При этом ,

однако, предполагается рабочее владение основными элементами

программирования; здесь не объясняется, что такое ЭВМ или

компилятор, не поясняется смысл выражений типа N=N+1. Хотя

мы и пытались, где это возможно, продемонстрировать полезную

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

справочным руководством по структурам данных и алгоритмам;

там, где мы вынуждены были сделать выбор, мы концентрирова-

лись на языке.

В главах со 2-й по 6-ю различные аспекты “C” излагаются

более детально и несколько более формально, чем в главе 1,

хотя ударение по-прежнему делается на разборе примеров за-

конченных, полезных программ, а не на отдельных фрагментах.

В главе 2 обсуждаются основные типы данных, операторы и

выражения. В главе 3 рассматриваются управляющие операторы:

IF-ELSE ,WHILE ,FOR и т.д. Глава 4 охватывает функции и

структуру программы - внешние переменные, правила определен-

ных областей действия описания и т.д. В главе 5 обсуждаются

указатели и адресная арифметика. Глава 6 содержит подробное

описание структур и объединений.

В главе 7 описывается стандартная библиотека ввода-вы-

вода языка “C”, которая обеспечивает стандартный интерфейс с

операционной системой. Эта библиотека ввода-вывода поддержи-

вается на всех машинах, на которых реализован “C”, так что

программы, использующие ее для ввода, вывода и других сис-

темных функций, могут переноситься с одной системы на другую

по существу без изменений.

В главе 8 описывается интерфейс между “C” - программами

и операционной системой “UNIX”. Упор делается на ввод-вывод,

систему файлов и переносимость. Хотя некоторые части этой

главы специфичны для операционной системы “UNIX”, програм-

мисты, не использующие “UNIX”, все же должны найти здесь по-

лезный материал, в том числе некоторое представление о том,

как реализована одна версия стандартной библиотеки и предло-

жения для достижения переносимости программы.

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

“C”. Оно является “официальным” изложением синтаксиса и се-

мантики “C” и (исключая чей-либо собственный компилятор)

окончательным арбитром для всех двусмысленностей и упущений

в предыдущих главах.

· 11 -

Так как “C” является развивающимся языком, реализован-

ным на множестве систем, часть материла настоящей книги мо-

жет не соответствовать текущему состоянию разработки на ка-

кой-то конкретной системе. Мы старались избегать таких проб-

лем и предостерегать о возможных трудностях. В сомнительных

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

для системы “UNIX” PDP-11 , так как она является средой для

большинства программирующих на языке “C”. В приложении а

также описаны расхождения в реализациях языка “C” на основ-

ных системах.

1. Учебное введение.

Давайте начнем с быстрого введения в язык “C”. Наша

цель - продемонстрировать существенные элементы языка на ре-

альных программах, не увязая при этом в деталях, формальных

правилах и исключениях. В этой главе мы не пытаемся изложить

язык полностью или хотя бы строго (разумеется, приводимые

примеры будут корректными). Мы хотим как можно скорее довес-

ти вас до такого уровня, на котором вы были бы в состоянии

писать полезные программы, и чтобы добиться этого, мы сосре-

дотачиваемся на основном: переменных и константах, арифмети-

ке, операторах передачи управления, функциях и элементарных

сведениях о вводе и выводе. Мы совершенно намеренно оставля-

ем за пределами этой главы многие элементы языка “C”, кото-

рые имеют первостепенное значение при написании больших

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

богатого набора операторов языка “C”, несколько операторов

передачи управления и несметное количество деталей.

Такой подход имеет, конечно, свои недостатки. Самым су-

щественным является то, что полное описание любого конкрет-

ного элемента языка не излагается в одном месте, а поясне-

ния, в силу краткости, могут привести к неправильному истол-

кованию. Кроме того, из-за невозможности использовать всю

мощь языка, примеры оказываются не столь краткими и элегант-

ными, как они могли бы быть. И хотя мы старались свести эти

недостатки к минимуму, все же имейте их ввиду.

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

будут неизбежно повторять некоторые части этой главы. Мы на-

деемся, что такое повторение будет скорее помогать, чем раз-

дражать.

Во всяком случае, опытные программисты должны оказаться

в состоянии проэкстраполировать материал данной главы на

свои собственные программистские нужды. Начинающие же должны

в дополнение писать аналогичные маленькие самостоятельные

программы. И те, и другие могут использовать эту главу как

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

ния, начинающиеся с главы 2.

1.1. Hачинаем. Единственный способ освоить новый язык

программирования - писать на нем программы. Первая програм-

ма, которая должна быть написана, - одна для всех языков:

напечатать слова : HELLO, WORLD.

Это - самый существенный барьер; чтобы преодолеть его,

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

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

ваша выдача. Если вы научились справляться с этими техничес-

кими деталями, все остальное сравнительно просто.

· 12 -

Программа печати “HELLO, WORLD” на языке “C” имеет вид:

MAIN ()

{

PRINTF(“HELLO, WORLD\N”);

}

Как пропустить эту программу - зависит от используемой

вами системы. В частности, на операционной системе “UNIX” вы

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

оканчивается на “.C” , например, HELLO.C , и затем скомпили-

ровать ее по команде

CC HELLO.C

Если вы не допустили какой-либо небрежности , такой как

пропуск символа или неправильное написание, компиляция прой-

дет без сообщений и будет создан исполняемый файл с именем

а.OUT . Прогон его по команде

A.OUT

приведет к выводу

HELLO, WORLD

На других системах эти правила будут иными; проконсуль-тируйтесь с местным авторитетом.

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

Пропустите эту программу на вашей системе. Попробуйте

не включать различные части программы и посмотрите какие со-

общения об ошибках вы при этом получите.

Теперь некоторые пояснения к самой программе. Любая

“C”-программа, каков бы ни был ее размер, состоит из одной

или более “функций”, указывающих фактические операции

компьютера, которые должны быть выполнены. Функции в языке

“C” подобны функциям и подпрограммам фортрана и процедурам

PL/1, паскаля и т.д. В нашем примере такой функцией является

MAIN. Обычно вы можете давать функциям любые имена по вашему

усмотрению, но MAIN - это особое имя; выполнение вашей прог-

раммы начинается сначала с функции MAIN. Это означает, что

каждая программа должна в каком-то месте содержать функцию с

именем MAIN. Для выполнения определенных действий функция

MAIN обычно обращается к другим функциям, часть из которых

находится в той же самой программе, а часть - в библиотеках,

содержащих ранее написанные функции.

· 13 -

Одним способом обмена данными между функциями является

передача посредством аргументов. Круглые скобки, следующие

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

маIN - функция без аргументов, что указывается как (). Опе-

раторы, составляющие функцию, заключаются в фигурные скобки

{ и }, которые аналогичны DO-END в PL/1 или BEGIN-END в ал-

голе, паскале и т.д. Обращение к функции осуществляется ука-

занием ее имени, за которым следует заключенный в круглые

скобки список аргументов. здесь нет никаких операторов CALL,

как в фортране или PL/1. Круглые скобки должны присутство-

вать и в том случае, когда функция не имеет аргументов.

Строка

PRINTF(“HELLO, WORLD\N”);

является обращением к функции, которое вызывает функцию

с именем PRINTF и аргуметом “HELLO, WORLD\N”. Функция PRINTF

является библиотечной функцией, которая выдает выходные дан-

ные на терминал (если только не указано какое-то другое мес-

то назначения). В данном случае печатается строка символов,

являющаяся аргументом функции.

Последовательность из любого количества символов, зак-

люченных в удвоенные кавычки “...”, называется 'символьной

строкой' или 'строчной константой'. Пока мы будем использо-

вать символьные строки только в качестве аргументов для

PRINTF и других функций.

Последовательность \N в приведенной строке является

обозначением на языке “C” для 'символа новой строки', кото-

рый служит указанием для перехода на терминале к левому краю

следующей строки. Если вы не включите \N (полезный экспери-

мент), то обнаружите, что ваша выдача не закончится перехо-

дом терминала на новую строку. Использование последователь-

ности \N - единственный способ введения символа новой строки

в аргумент функции PRINTF; если вы попробуете что-нибудь

вроде

PRINTF(“HELLO, WORLD

“);

то “C”-компилятор будет печатать злорадные диагностические

сообщения о недостающих кавычках.

Функция PRINTF не обеспечивает автоматического перехода

на новую строку, так что многократное обращение к ней можно

использовать для поэтапной сборки выходной строки. Наша пер-

вая программа, печатающая идентичную выдачу, с точно таким

же успехом могла бы быть написана в виде

MAIN()

{

PRINTF(“HELLO, “);

PRINTF(“WORLD”);

PRINTF(“\N”);

}

· 14 -

Подчеркнем, что \N представляет только один символ. Ус-

ловные 'последовательности', подобные \N , дают общий и до-

пускающий расширение механизм для представления трудных для

печати или невидимых символов. Среди прочих символов в языке

“C” предусмотрены следующие: \т - для табуляции, \B - для

возврата на одну позицию, \” - для двойной кавычки и \\ для

самой обратной косой черты.

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

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

зойдет, если в строке, являющейся аргументом функции PRINTF

будет содержаться \X, где X - некоторый символ, не входящий

в вышеприведенный список.

1.2. Переменные и арифметика.

Следующая программа печатает приведенную ниже таблицу

температур по Фаренгейту и их эквивалентов по стоградусной

шкале Цельсия, используя для перевода формулу

C = (5/9)*(F-32).

0 -17.8

20 -6.7

40 4.4

60 15.6

... ...

260 126.7

280 137.8

300 140.9

Теперь сама программа:

/* PRINT FAHRENHEIT-CELSIUS TABLE

FOR F = 0, 20, ..., 300 */

MAIN()

{

INT LOWER, UPPER, STEP;

FLOAT FAHR, CELSIUS;

LOWER = 0; /* LOWER LIMIT OF TEMPERATURE

TABLE */

UPPER =300; /* UPPER LIMIT */

STEP = 20; /* STEP SIZE */

FAHR = LOWER;

WHILE (FAHR <= UPPER) {

CELSIUS = (5.0/9.0) * (FAHR -32.0);

PRINTF(“%4.0F %6.1F\N”, FAHR, CELSIUS);

FAHR = FAHR + STEP;

}

}

· 15 -

Первые две строки

/* PRINT FAHRENHEIT-CELSIUS TABLE

FOR F = 0, 20, ..., 300 */

являются комментарием, который в данном случае кратко пояс-

няет, что делает программа. Любые символы между /* и */ иг-

норируются компилятором; можно свободно пользоваться коммен-

тариями для облегчения понимания программы. Комментарии мо-

гут появляться в любом месте, где возможен пробел или пере-

ход на новую строку.

В языке “C” все переменные должны быть описаны до их ис-

пользования, обычно это делается в начале функции до первого

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

то получите диагностическое сообщение от компилятора. Описа-

ние состоит из типа и списка переменных, имеющих этот тип,

как в

INT LOWER, UPPER, STEP;

FLOAT FAHR, CELSIUS;

Тип INT означает, что все переменные списка целые; тип

FLOAT предназначен для чисел с плавающей точкой, т.е. для

чисел, которые могут иметь дробную часть. Точность как INT ,

TAK и FLOAT зависит от конкретной машины, на которой вы ра-

ботаете. На PDP-11, например, тип INT соответствует 16-бито-

вому числу со знаком, т.е. числу, лежащему между -32768 и

+32767. Число типа FLOAT - это 32-битовое число, имеющее

около семи значащих цифр и лежащее в диапазоне от 10е-38 до

10е+38. В главе 2 приводится список размеров для других ма-

шин.

В языке “C” предусмотрено несколько других основных ти-

пов данных, кроме INT и FLOAT:

CHAR символ - один байт

SHORT короткое целое

LONG длинное целое

DOUBLE плавающее с двойной точностью

Размеры этих объектов тоже машинно-независимы; детали

приведены в главе 2. Имеются также массивы, структуры и об-

ъединения этих основных типов, указатели на них и функ-

ции,которые их возвращают; со всеми ними мы встретимся в

свое время.

Фактически вычисления в программе перевода температур

начинаются с операторов присваивания

LOWER = 0;

UPPER =300;

STEP = 20;

FAHR =LOWER;

которые придают переменным их начальные значения. каждый от-

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

Каждая строка таблицы вычисляется одинаковым образом,

так что мы используем цикл, повторяющийся один раз на стро-

ку. В этом назначение оператора WHILE:

WHILE (FAHR <= UPPER) {

....

}

· 16 -

проверяется условие в круглых скобках. Если оно истинно

(FAHR меньше или равно UPPER), то выполняется тело цикла

(все операторы, заключенные в фигурные скобки { и } ). Затем

вновь проверяется это условие и, если оно истинно, опять вы-

полняется тело цикла. Если же условие не выполняется ( FAHR

превосходит UPPER ), цикл заканчивается и происходит переход

к выполнению оператора, следующего за оператором цикла. Так

как в настоящей программе нет никаких последующих операто-

ров, то выполнение программы завершается.

Тело оператора WHILE может состоять из одного или более

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

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

как, например, в

WHILE (I < J)

I = 2 * I;

В обоих случаях операторы, управляемые оператором WHILE,

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

видеть, какие операторы находятся внутри цикла. Такой сдвиг

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

“C” допускается совершенно произвольное расположение опера-

торов в строке, подходящий сдвиг и использование пробелов

значительно облегчают чтение программ. Мы рекомендуем писать

только один оператор на строке и (обычно) оставлять пробелы

вокруг операторов. Расположение фигурных скобок менее сущес-

твенно; мы выбрали один из нескольких популярных стилей. Вы-

берите подходящий для вас стиль и затем используйте его пос-

ледовательно.

Основная часть работы выполняется в теле цикла. Темпера-

тура по Цельсию вычисляется и присваивается переменной

CELAIUS оператором

CELSIUS = (5.0/9.0) * (FAHR-32.0);

причина использования выражения 5.0/9.0 вместо выглядящего

проще 5/9 заключается в том, что в языке “C”, как и во мно-

гих других языках, при делении целых происходит усечение,

состоящее в отбрасывании дробной части результата. Таким об-

разом, результат операции 5/9 равен нулю, и, конечно, в этом

случае все температуры оказались бы равными нулю. Десятичная

точка в константе указывает, что она имеет тип с плавающей

точкой, так что, как мы и хотели, 5.0/9.0 равно 0.5555... .

Мы также писали 32.0 вместо 32 , несмотря на то, что так

как переменная FAHR имеет тип FLOAT , целое 32 автоматически

бы преобразовалось к типу FLOAT ( в 32.0) перед вычитанием.

С точки зрения стиля разумно писать плавающие константы с

явной десятичной точкой даже тогда, когда они имеют целые

значения; это подчеркивает их плавающую природу для просмат-

ривающего программу и обеспечивает то, что компилятор будет

смотреть на вещи так же, как и Вы.

· 17 -

Подробные правила о том, в каком случае целые преобразу-

ются к типу с плаваюшей точкой, приведены в главе 2. Сейчас

же отметим, что присваивание

FAHR = LOWER;

проверка

WHILE (FAHR <= UPPER)

работают, как ожидается, - перед выполнением операций целые

преобразуются в плавающую форму.

Этот же пример сообщает чуть больше о том, как работает

PRINTF. Функция PRINTF фактически является универсальной

функцией форматных преобразований, которая будет полностью

описана в главе 7. Ее первым аргументом является строка сим-

волов, которая должна быть напечатана, причем каждый знак %

указывает, куда должен подставляться каждый из остальных ар-

гументов /второй, третий, .../ и в какой форме он должен пе-

чататься. Например, в операторе

PRINTF(“%4.0F %6.1F\N”, FAHR, CELSIUS);

спецификация преобразования %4.0F говорит, что число с пла-

вающей точкой должно быть напечатано в поле шириной по край-

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

спецификация %6.1F описывает другое число, которое должно

занимать по крайней мере шесть позиций с одной цифрой после

десятичной точки, аналогично спецификациям F6.1 в фортране

или F(6,1) в PL/1. Различные части спецификации могут быть

опущены: спецификация %6F говорит, что число будет шириной

по крайней мере в шесть символов; спецификация %2 требует

двух позиций после десятичной точки, но ширина при этом не

ограничивается; спецификация %F говорит только о том, что

нужно напечатать число с плавающей точкой. Функция PRINTF

также распознает следующие спецификации: %D - для десятично-

го целого, %о - для восьмеричного числа, %х - для шестнадца-

тиричного, %с - для символа, %S - для символьной строки и %%

· для самого символа %.

Каждая конструкция с символом % в первом аргументе функ-

ции PRINTF сочетается с соответствующим вторым, третьим, и

т.д. Аргументами; они должны согласовываться по числу и ти-

пу; в противном случае вы получите бессмысленные результаты.

Между прочим, функция PRINTF не является частью языка

“C”; в самом языке “C” не определены операции ввода-вывода.

Нет ничего таинственного и в функции PRINTF ; это - просто

полезная функция, являющаяся частью стандартной библиотеки

подпрограмм, которая обычно доступна “C”-программам. Чтобы

сосредоточиться на самом языке, мы не будем подробно оста-

навливаться на операциях ввода-вывода до главы 7. В частнос-

ти, мы до тех пор отложим форматный ввод. Если вам надо

ввести числа - прочитайте описание функции SCANF в главе 7,

раздел 7.4. Функция SCANF во многом сходна с PRINTF , но она

считывает входные данные, а не печатает выходные.

· 18 -

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

Преобразуйте программу перевода температур таким обра-

зом, чтобы она печатала заголовок к таблице.

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

Напишите программы печати соответствующей таблицы пере-

хода от градусов цельсия к градусам фаренгейта.

1.3. Оператор FOR.

Как и можно было ожидать, имеется множество различных

способов написания каждой программы. Давайте рассмотрим та-

кой вариант программы перевода температур:

MAIN() /* FAHRENHEIT-CELSIUS TABLE */

{

INT FAHR;

FOR (FAHR = 0; FAHR <= 300; FAHR = FAHR + 20)

PRINTF(“%4D %6.1F\N”, FAHR, (5.0/9.0)*(FAHR-32.0));

}

Эта программа выдает те же самые результаты, но выглядит

безусловно по-другому. Главное изменение - исключение боль-

шинства переменных; осталась только переменная FAHR , причем

типа INT (это сделано для того, чтобы продемонстрировать

преобразование %D в функции PRINTF). Нижняя и верхняя грани-

цы и размер щага появляются только как константы в операторе

FOR , который сам является новой конструкцией, а выражение,

вычисляющее температуру по цельсию, входит теперь в виде

третьего аргумента функции PRINTF , а не в виде отдельного

оператора присваивания.

Последнее изменение является примером вполне общего пра-

вила языка “C” - в любом контексте, в котором допускается

использование значения переменной некоторого типа, вы можете

использовать выражение этого типа. Так как третий аргумент

функции PRINTF должен иметь значение с плавающей точкой,

чтобы соответствовать спецификации %6.1F, то в этом месте

может встретиться любое выражение плавающего типа.

Сам оператор FOR - это оператор цикла, обобщающий опера-

тор WHILE. Его функционирование должно стать ясным, если вы

сравните его с ранее описанным оператором WHILE . Оператор

FOR содержит три части, разделяемые точкой с запятой. Первая

часть

FAHR = 0

выполняется один раз перед входом в сам цикл. Вторая часть -

проверка, или условие, которое управляет циклом:

FAHR <= 300

это условие проверяется и, если оно истинно, то выполняется

тело цикла (в данном случае только функция PRINTF ). Затем

выполняется шаг реинициализации

· 19 -

FAHR =FAHR + 20

и условие проверяется снова. цикл завершается, когда это ус-

ловие становится ложным. Так же, как и в случае оператора

WHILE , тело цикла может состоять из одного оператора или из

группы операторов, заключенных в фигурные скобки. Инициали-

зирующая и реинициализирующая части могут быть любыми от-

дельными выражениями.

Выбор между операторами WHILE и FOR произволен и основы-

вается на том , что выглядит яснее. Оператор FOR обычно удо-

бен для циклов, в которых инициализация и реинициализация

логически связаны и каждая задается одним оператором, так

как в этом случае запись более компактна, чем при использо-

вании оператора WHILE , а операторы управления циклом сосре-

дотачиваются вместе в одном месте.

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

Модифицируйте программу перевода температур таким обра-

зом, чтобы она печатала таблицу в обратном порядке, т.е. От

300 градусов до 0.

1.4. Символические константы.

Последнее замечание, прежде чем мы навсегда оставим

программу перевода температур. Прятать “магические числа”,

такие как 300 и 20, внутрь программы - это неудачная практи-

ка; они дают мало информации тем, кто, возможно, должен бу-

дет разбираться в этой программе позднее, и их трудно изме-

нять систематическим образом. К счастью в языке “C” предус-

мотрен способ, позволяющий избежать таких “магических чи-

сел”. Используя конструкцию #DEFINE , вы можете в начале

программы определить символическое имя или символическую

константу, которая будет конкретной строкой символов. Впос-

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

явления этого имени на соответствующую строку. Фактически

это имя может быть заменено абсолютно произвольным текстом,

не обязательно цифрами

#DEFINE LOWER 0/* LOWER LIMIT OF TABLE */

#DEFINE UPPER 300 /* UPPER LIMIT */

#DEFINE STEP 20 /* STEP SIZE */

MAIN () /* FAHRENHEIT-CELSIUS TABLE */

{

INT FAHR;

FOR (FAHR =LOWER; FAHR <= UPPER; FAHR =FAHR + STEP)

PRINTF(“%4D %6.1F\N”, FAHR, (5.0/9.0)*(FAHR-32));

}

величины LOWER, UPPER и STEP являются константами и поэ-

тому они не указываются в описаниях. Символические имена

обычно пишут прописными буквами, чтобы их было легко отли-

чить от написанных строчными буквами имен переменных. отме-

тим, что в конце определения не ставится точка с запятой.

Так как подставляется вся строка, следующая за определенным

именем, то это привело бы к слишком большому числу точек с

запятой в операторе FOR .

· 20 -

1.5. Набор полезных программ.

Теперь мы собираемся рассмотреть семейство родственных

программ, предназначенных для выполнения простых операций

над символьными данными. В дальнейшем вы обнаружите, что

многие программы являются просто расширенными версиями тех

прототипов, которые мы здесь обсуждаем.

1.5.1. Ввод и вывод символов.

Стандартная библиотека включает функции для чтения и за-

писи по одному символу за один раз. функция GETCHAR() извле-

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

ются, и возвращает этот символ в качестве своего значения.

Это значит, что после

C = GETCHAR()

переменная 'C' содержит следующий символ из входных данных.

Символы обычно поступают с терминала, но это не должно нас

касаться до главы 7.

Функция PUTCHAR© является дополнением к GETCHAR : в

результате обращения

PUTCHAR ©

содержимое переменной 'C' выдается на некоторый выходной но-

ситель, обычно опять на терминал. Обращение к функциям

PUTCHAR и PRINTF могут перемежаться; выдача будет появляться

в том порядке, в котором происходят обращения.

Как и функция PRINTF , функции GETCHAR и PUTCHAR не со-

держат ничего экстраординарного. Они не входят в состав язы-

ка “C”, но к ним всегда можно обратиться.

1.5.2. Копирование файла.

Имея в своем распоряжении только функции GETCHAR и

PUTCHAR вы можете, не зная ничего более об операциях вво-

да-вывода, написать удивительное количество полезных прог-

рамм. Простейшим примером может служить программа посимволь-

ного копирования вводного файла в выводной. Общая схема име-

ет вид:

ввести символ

WHILE (символ не является признаком конца файла)

вывести только что прочитанный символ

ввести новый символ

программа, написанная на языке “C”, выглядит следующим обра-

зом:

MAIN() /* COPY INPUT TO OUTPUT; 1ST VERSION */

{

INT C;

C = GETCHAR();

WHILE (C != EOF) {

PUTCHAR ©;

C = GETCHAR();

}

}

· 21 -

оператор отношения != означает “не равно”.

Основная проблема заключается в том, чтобы зафиксиро-

вать конец файла ввода. Обычно, когда функция GETCHAR натал-

кивается на конец файла ввода, она возвращает значение , не

являющееся действительным символом; таким образом, программа

может установить, что файл ввода исчерпан. Единственное ос-

ложнение, являющееся значительным неудобством, заключается в

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

кое значение фактически является признаком конца файла. Мы

отсрочим решение этого вопроса, использовав символическое

имя EOF для этого значения, каким бы оно ни было. На практи-

ке EOF будет либо -1, либо 0, так что для правильной работы

перед программой должно стоять собственно либо

#DEFINE EOF -1

либо

#DEFINE EOF 0

Использовав символическую константу EOF для представле-

ния значения, возвращаемого функцией GETCHAR при выходе на

конец файла, мы обеспечили, что только одна величина в прог-

рамме зависит от конкретного численного значения.

Мы также описали переменную 'C' как INT , а не CHAR , с

тем чтобы она могла хранить значение, возвращаемое GETCHAR .

как мы увидим в главе 2, эта величина действительно INT, так

как она должна быть в состоянии в дополнение ко всем возмож-

ным символам представлять и EOF.

Программистом, имеющим опыт работы на “C”, программа

копирования была бы написана более сжато. В языке “C” любое

присваивание, такое как

C = GETCHAR()

может быть использовано в выражении; его значение - просто

значение, присваиваемое левой части. Если присваивание сим-

вола переменной 'C' поместить внутрь проверочной части опе-

ратора WHILE , то программа копирования файла запишется в

виде:

MAIN() /* COPY INPUT TO OUTPUT; 2ND VERSION */

{

INT C;

WHILE ((C = GETCHAR()) != EOF)

PUTCHAR©;

}

Программа извлекает символ , присваивает его переменной

'C' и затем проверяет, не является ли этот символ признаком

конца файла. Если нет - выполняется тело оператора WHILE,

выводящее этот символ. Затем цикл WHILE повторяется. когда,

наконец, будет достигнут конец файла ввода, оператор WHILE

завершается, а вместе с ним заканчивается выполнение и функ-

ции MAIN .

· 22 -

В этой версии централизуется ввод - в программе только

одно обращение к функции GETCHAR - и ужимается программа.

Вложение присваивания в проверяемое условие - это одно из

тех мест языка “C”, которое приводит к значительному сокра-

щению программ. Однако, на этом пути можно увлечься и начать

писать недоступные для понимания программы. Эту тенденцию мы

будем пытаться сдерживать.

Важно понять , что круглые скобки вокруг присваивания в

условном выражении действительно необходимы. Старшинство

операции != выше, чем операции присваивания =, а это означа-

ет, что в отсутствие круглых скобок проверка условия != бу-

дет выполнена до присваивания =. Таким образом, оператор

C = GETCHAR() != EOF

эквивалентен оператору

C = (GETCHAR() != EOF)

Это, вопреки нашему желанию, приведет к тому, что 'C'

будет принимать значение 0 или 1 в зависимости от того, на-

толкнется или нет GETCHAR на признак конца файла. Подробнее

об этом будет сказано в главе 2/.

1.5.3. Подсчет символов.

Следующая программа подсчитывает число символов; она

представляет собой небольшое развитие программы копирования.

MAIN() /* COUNT CHARACTERS IN INPUT */

{

LONG NC;

NC = 0;

WHILE (GETCHAR() != EOF )

++NC;

PRINTF(“%1D\N”, NC);

}

Оператор

++NC;

демонстрирует новую операцию, ++, которая означает увеличе-

ние на единицу. Вы могли бы написать NC = NC + 1 , но ++NC

более кратко и зачастую более эффективно. Имеется соответст-

вующая операция—уменьшение на единицу. Операции ++ и—

могут быть либо префиксными (++NC), либо постфиксными

(NC++); эти две формы, как будет показано в главе 2, имеют в

выражениях различные значения, но как ++NC, так и NC++ уве-

личивают NC. Пока мы будем придерживаться префиксных опера-

ций.

· 23 -

Программа подсчета символов накапливает их количество в

переменной типа LONG, а не INT . На PDP-11 максимальное зна-

чение равно 32767, и если описать счетчик как INT , то он

будет переполняться даже при сравнительно малом файле ввода;

на языке “C” для HONEYWELL и IBM типы LONG и INT являются

синонимами и имеют значительно больший размер. Спецификация

преобразования %1D указывает PRINTF , что соответствующий

аргумент является целым типа LONG .

Чтобы справиться с еще большими числами, вы можете ис-

пользовать тип DOUBLE / FLOAT двойной длины/. мы также ис-

пользуем оператор FOR вместо WHILE с тем, чтобы проиллюстри-

ровать другой способ записи цикла.

MAIN() /* COUNT CHARACTERS IN INPUT */

{

DOUBLE NC;

FOR (NC = 0; GETCHAR() != EOF; ++NC)

;

PRINTF(“%.0F\N”, NC);

}

Функция PRINTF использует спецификацию %F как для FLOAT

, так и для DOUBLE ; спецификация %.0F подавляет печать не-

существующей дробной части.

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

та выполняется в проверочной и реинициализационной частях.

Но грамматические правила языка “C” требуют, чтобы оператор

FOR имел тело. Изолированная точка с запятой, соответствую-

шая пустому оператору, появляется здесь, чтобы удовлетворить

этому требованию. Мы выделили ее на отдельную строку, чтобы

сделать ее более заметной.

Прежде чем мы распростимся с программой подсчета симво-

лов, отметим, что если файл ввода не содержит никаких симво-

лов, то условие в WHILE или FOR не выполнится при самом пер-

вом обращении к GETCHAR , и, следовательно , программа вы-

даст нуль, т.е. Правильный ответ. это важное замечание. од-

ним из приятных свойств операторов WHILE и FOR является то,

что они проверяют условие в начале цикла, т.е. До выполнения

тела. Если делать ничего не надо, то ничего не будет сдела-

но, даже если это означает, что тело цикла никогда не будет

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

обращаются с файлами типа “никаких символов”. Операторы

WHILE и FOR помогают обеспечить правильное поведение прог-

рамм при граничных значениях проверяемых условий.

1.5.4. Подсчет строк.

Следующая программа подсчитывает количество строк в

файле ввода. Предполагается, что строки ввода заканчиваются

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

выписанной строке.

· 24 -

MAIN() /* COUNT LINES IN INPUT */

{

INT C,NL;

NL = 0;

WHILE ((C = GETCHAR()) != EOF)

IF (C =='\N')

++NL;

PRINTF(“%D\N”, NL);

}

Тело WHILE теперь содержит оператор IF , который в свою

очередь управляет оператором увеличения ++NL. Оператор IF

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

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

операторов, заключенных в фигурные скобки/. Мы опять исполь-

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

Удвоенный знак равенства == является обозначением в

языке “C” для “равно” /аналогично .EQ. В фортране/. Этот

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

от одиночного =, используемого при присваивании. Поскольку в

типичных “C” - программах знак присваивания встречается при-

мерно в два раза чаще, чем проверка на равенство, то естест-

венно, чтобы знак оператора был вполовину короче.

Любой отдельный символ может быть записан внутри оди-

ночных кавычек, и при этом ему соответствует значение, рав-

ное численному значению этого символа в машинном наборе сим-

волов; это называется символьной константой. Так, например,

'A' - символьная константа; ее значение в наборе символов

ASCII /американский стандартный код для обмена информацией/

равно 65, внутреннему представлению символа а. Конечно, 'A'

предпочтительнее, чем 65: его смысл очевиден и он не зависит

от конкретного машинного набора символов.

Условные последовательности, используемые в символьных

строках, также занимают законное место среди символьных кон-

стант. Так в проверках и арифметических выражениях '\N'

представляет значение символа новой строки. Вы должны твердо

уяснить, что '\N' - отдельный символ, который в выражениях

эквивалентен одиночному целому; с другой стороны “\N” - это

символьная строка, которая содержит только один символ. Воп-

рос о сопоставлении строк и символов обсуждается в главе 2.

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

Напишите программу для подсчета пробелов, табуляций и

новых строк.

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

Напишите программу, которая копирует ввод на вывод, за-

меняя при этом каждую последовательность из одного или более

пробелов на один пробел.

1.5.5. Подсчет слов.

Четвертая программа из нашей серии полезных программ

подсчитывает количество строк, слов и символов, используя

при этом весьма широкое определение, что словом является лю-

бая последовательность символов, не содержащая пробелов, та-

буляций или новых строк. /Это - упрощенная версия утилиты

'WC' системы 'UNIX'/

· 25 -

#DEFINE YES 1

#DEFINE NO 0

MAIN() /* COUNT LINES, WORDS, CHARS IN INPUT */

{

INT C, NL, NW, INWORD;

INWORD = NO;

NL = NW = NC = 0;

WHILE((C = GETCHAR()) != EOF) {

++NC;

IF (C == '\N')

++NL;

IF (C==' ' \!\! C=='\N' \!\! C=='\T')

INWORD = NO;

ELSE IF (INWORD == NO) {

INWORD = YES;

++NW;

}

}

PRINTF(“%D %D %D\N”, NL, NW, NC);

}

Каждый раз, когда программа встречает первый символ

слова, она увеличивает счетчик числа слов на единицу. Пере-

менная INWORD следит за тем, находится ли программа в насто-

ящий момент внутри слова или нет; сначала этой переменной

присваивается “ не в слове”, чему соответствует значение NO.

Мы предпочитаем символические константы YES и NO литерным

значениям 1 и 0, потому что они делают программу более удоб-

ной для чтения. Конечно, в такой крошечной программе, как

эта, это не приводит к заметной разнице, но в больших прог-

раммах увеличение ясности вполне стоит тех скромных дополни-

тельных усилий, которых требует следование этому принципу с

самого начала. Вы также обнаружите, что существенные измене-

ния гораздо легче вносить в те программы, где числа фигури-

руют только в качестве символьных констант.

Строка

NL = NW = NC = 0;

полагает все три переменные равными нулю. Это не

особый случай, а следствие того обстоятельства, что операто-

ру присваивания соответствует некоторое значение и присваи-

вания проводятся последовательно справа налево. Таким обра-

зом, дело обстоит так, как если бы мы написали

NC = (NL = (NW = 0));

операция \!\! Означает OR , так что строка

IF( C==' ' \!\! C=='\N' \!\! C=='\T')

говорит “если с - пробел, или с - символ новой строки, или с

· табуляция ...”./условная последовательность \T является

изображением символа табуляции/.

·
26 -

Имеется соответствующая операция && для AND. Выражения,

связанные операциями && или \!\! , Рассматриваются слева на

право, и при этом гарантируется, что оценивание выражений

будет прекращено, как только станет ясно, является ли все

выражение истинным или ложным. Так, если 'C' оказывается

пробелом, то нет никакой необходимости проверять, является

ли 'C' символом новой строки или табуляции, и такие проверки

действительно не делаются. В данном случае это не имеет

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

сложных ситуациях эта особенность языка весьма существенна.

Этот пример также демонстрирует оператор ELSE языка “C”,

который указывает то действие, которое должно выполняться,

если условие, содержащееся в операторе IF, окажется ложным.

Общая форма такова:

IF (выражение)

оператор-1

ELSE оператор-2

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

занных с конструкцией IF-ELSE. Если выражение истинно, вы-

полняется оператор-1; если нет - выполняется оператор-2.

Фактически каждый оператор может быть довольно сложным. В

программе подсчета слов оператор, следующий за ELSE , явля-

ется опертором IF , который управляет двумя операторами в

фигурных скобках.

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

Как бы вы стали проверять программу подсчета слов ?

Kакие имеются ограничения ?

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

Напишите программу, которая будет печатать слова из фай-

ла ввода, причем по одному на строку.

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

Переделайте программу подсчета слов, используя лучшее

пределение “слова”; считайте, например словом последователь-

ность букв, цифр и апострофов, рачинающуюся с буквы.

1.6. Массивы.

Давайте напишем программу подсчета числа появлений каж-

дой цифры, символов пустых промежутков/пробел, табуляции,

новая строка/ и всех остальных символов. Конечно, такая за-

дача несколько искусственна, но она позволит нам проиллюст-

рировать в одной программе сразу несколько аспектов языка

“C”.

Мы разбили вводимые символы на двенадцать категорий, и

нам удобнее использовать массив для хранения числа появлений

каждой цифры, а не десять отдельных переменных. Вот один из

вариантов программы:

· 27 -

MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */

{

INT C, I, NWHITE, NOTHER;

INT NDIGIT[10];

NWHITE = NOTHER = 0;

FOR (I = 0; I < 10; ++I)

NDIGIT[I] = 0;

WHILE ((C = GETCHAR()) != EOF)

IF (C >= '0' && C <= '9')

++NDIGIT[C-'0'];

ELSE IF(C== ' ' \!\! C== '\N' \!\! C== '\T')

++NWHITE;

ELSE

++NOTHER;

PRINTF(“DIGITS =”);

FOR (I = 0; I < 10; ++I)

PRINTF(“ %D”, NDIGIT[I]);

PRINTF(“\NWHITE SPACE = %D, OTHER = %D\N”,

NWHITE, NOTHER);

}

Описание

INT NDIGIT[10];

объявляет, что NDIGIT является массивом из десяти целых. в

языке “C” индексы массива всегда начинаются с нуля /а не с

1, как в фортране или PL/1/, так что элементами массива яв-

ляются NDIGIT[0], NDIGIT[1],..., NDIGIT[9]. эта особенность

отражена в циклах FOR , которые инициализируют и печатают

массив.

Индекс может быть любым целым выражением, которое, ко-

нечно, может включать целые переменные, такие как I , и це-

лые константы.

Эта конкретная программа сильно опирается на свойства

символьного представления цифр. Так, например, в программе

проверка

IF( C >= '0' && C <= '9')...

определяет, является ли символ в 'C' цифрой, и если это так,

то численное значение этой цифры определяется по формуле / C

· '0'/. Такой способ работает только в том случае, если зна-

чения символьных констант '0', '1' и т.д. Положительны, рас-

положены в порядке возрастания и нет ничего, кроме цифр,

между константами '0' и '9'. К счастью, это верно для всех

общепринятых наборов символов.

· 28 -

По определению перед проведением арифметических опера-

ций, вовлекающих переменные типа CHAR и INT, все они преоб-

разуются к типу INT, TAK что в арифметических выражениях пе-

ременные типа CHAR по существу идентичны переменным типа

INT. Это вполне естественно и удобно; например, C -'0'- это

целое выражение со значением между 0 и 9 в соответствии с

тем, какой символ от '0' до '9' хранится в 'C', и, следова-

тельно, оно является подходящим индексом для массива NDIGIT.

Выяснение вопроса, является ли данный символ цифрой,

символом пустого промежутка или чем-либо еще, осуществляется

последовательностью операторов

IF (C >= '0' && C <= '9')

++NDIGIT[C-'0'];

ELSE IF(C == ' ' \!\! C == '\N' \!\! C == '\T')

++NWHITE;

ELSE

++NOTHER;

Конструкция

IF (условие)

оператор

ELSE IF (условие)

оператор

ELSE

оператор

часто встречаются в программах как средство выражения ситуа-

ций, в которых осуществляется выбор одного из нескольких

возможных решений.

Программа просто движется сверху вниз до тех пор, пока

не удовлетворится какое-нибудь условие; тогда выполняется

соответствующий 'оператор', и вся конструкция завершается.

/Конечно, 'оператор' может состоять из нескольких операто-

ров, заключенных в фигурные скобки/. Если ни одно из условий

не удовлетворяется, то выполняется 'оператор', стоящий после

заключительного ELSE, если оно присутствует. Если последнеE

ELSE и соответствующий 'оператор' опущены (как в программе

подсчета слов), то никаких действий не производится. Между

начальным IF и конечным ELSE может помещаться произвольное

количество групп

ELSE IF (условие)

оператор

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

Оператор SWITCH (переключатель), который рассматривается

в главе 3, представляет другую возможность для записи раз-

ветвления на несколько вариантов. этот оператор особенно

удобен, когда проверяемое выражение является либо просто не-

которым целым, либо символьным выражением, совпадающим с од-

ной из некоторого набора констант. Версия этой программы,

использующая оператор SWITCH, будет для сравнения приведена

в главе 3.

· 29 -

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

Напишите программу, печатающую гистограмму длин слов из

файла ввода. Самое легкое - начертить гистограмму горизон-

тально; вертикальная ориентация требует больших усилий.

1.7. Функции.

В языке “C” функции эквивалентны подпрограммам или функ-

циям в фортране или процедурам в PL/1, паскале и т.д. Функ-

ции дают удобный способ заключения некоторой части вычисле-

ний в черный ящик, который в дальнейшем можно использовать,

не интересуясь его внутренним содержанием. Использование

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

с потенциальной сложностью больших программ. Если функции

организованы должным образом, то можно игнорировать то, как

делается работа; достаточно знание того, что делается. Язык

“C” разработан таким образом, чтобы сделать использование

функций легким, удобным и эффективным. Вам будут часто вст-

речаться функции длиной всего в несколько строчек, вызывае-

мые только один раз, и они используются только потому, что

это проясняет некоторую часть программы.

До сих пор мы использовали только предоставленные нам

функции типа PRINTF, GETCHAR и PUTCHAR; теперь пора написать

несколько наших собственных. так как в “C” нет операции воз-

ведения в степень, подобной операции ** в фортране или PL/1,

давайте проиллюстрируем механику определения функции на при-

мере функции POWER(M,N), возводящей целое м в целую положи-

тельную степень N. Так значение POWER(2,5) равно 32. Конеч-

но, эта функция не выполняет всей работы операции **, пос-

кольку она действует только с положительными степенями не-

больших чисел, но лучше не создавать дополнительных затруд-

нений, смешивая несколько различных вопросов.

Ниже приводится функция POWER и использующая ее основная

программа, так что вы можете видеть целиком всю структуру.

MAIN() /* TEST POWER FUNCTION */

{

INT I;

FOR(I = 0; I < 10; ++I)

PRINTF(“%D %D %D\N”,I,POWER(2,I),POWER(-3,I));

}

POWER(X,N) /* RAISE X N-TH POWER; N > 0 */

INT X,N;

{

INT I, P;

P = 1;

FOR (I =1; I <= N; ++I)

P = P * X;

RETURN (P);

}

· 30 -

Все функции имеют одинаковый вид:

имя (список аргументов, если они имеются)

описание аргументов, если они имеются

{

описания

операторы

}

Эти функции могут быть записаны в любом порядке и нахо-

диться в одном или двух исходных файлах. Конечно, если ис-

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

дать больше указаний при компиляции и загрузке, чем если бы

она находилась в одном, но это дело операционной системы, а

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

ченные сведения о прогоне “C”- программ, не изменились в

дальнейшем, мы будем предполагать, что обе функции находятся

в одном и том же файле.

Функция POWER вызывается дважды в строке

PRINTF(“%D %D %D\N”,I,POWER(2,I),POWER(-3,I));

при каждом обращении функция POWER, получив два аргумента,

вазвращает целое значение, которое печатается в заданном

формате. В выражениях POWER(2,I) является точно таким же це-

лым, как 2 и I. /Не все функции выдают целое значение; мы

займемся этим вопросом в главе 4/.

Аргументы функции POWER должны быть описаны соответству-

ющим образом, так как их типы известны. Это сделано в строке

INT X,N;

которая следует за именем функции.

Описания аргументов помещаются между списком аргументов

и открывающейся левой фигурной скобкой; каждое описание за-

канчивается точкой с запятой. Имена, использованные для ар-

гументов функции POWER, являются чисто локальными и недос-

тупны никаким другим функциям: другие процедуры могут ис-

пользовать те же самые имена без возникновения конфликта.

Это верно и для переменных I и P; I в функции POWER никак не

связано с I в функции MAIN.

Значение, вычисленное функцией POWER, передаются в MAIN

с помощью оператора RETURN, точно такого же, как в PL/1.

внутри круглых скобок можно написать любое выражение. Функ-

ция не обязана возвращать какое-либо значение; оператор

RETURN, не содержащий никакого выражения, приводит к такой

же передаче управления, как “сваливание на конец” функции

при достижении конечной правой фигурной скобки, но при этом

в вызывающую функцию не возвращается никакого полезного зна-

чения.

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

Апишите программу преобразования прописных букв из айла

ввода в строчные, используя при этом функцию OWER©, кото-

рая возвращает значение 'C', если C'- не буква, и значение

соответствующей строчной уквы, если 'C'-буква.

· 31 -

1.8. Аргументы - вызов по значению.

Один аспект в “C” может оказаться непривычным для прог-

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

фортран и PL/1. в языке “C” все аргументы функций передаются

“по значению”. это означает, что вызванная функция получает

значения своих аргументов с помощью временных переменных

/фактически через стек/, а не их адреса. Это приводит к не-

которым особенностям, отличным от тех, с которыми мы сталки-

вались в языках типа фортрана и PL/1, использующих “вызов по

ссылке “, где вызванная процедура работает с адресом аргу-

мента, а не с его значением.

Главное отличие состоит в том, что в “C” вызванная функ-

ция не может изменить переменную из вызывающей функции; она

может менять только свою собственную временную копию.

Вызов по значению, однако, не помеха, а весьма ценное

качество. Оно обычно приводит к более компактным программам,

содержащим меньше не относящихся к делу переменных, потому

что с аргументами можно обращаться как с удобно инициализи-

рованными локальными перемнными вызванной процедуры. Вот,

например, вариант функции POWER использующей это обстоятель-

ство

POWER(X,N) /* RAISE X N-TH POWER; N > 0;

VERSION 2 */

INT X,N;

{

INT P;

FOR (P = 1; N > 0; --N)

P = P * X;

RETURN (P);

}

Аргумент N используется как временная переменная; из не-

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

Переменная I здесь больше не нужна. чтобы ни происходило с N

внутри POWER это никак не влияет на аргумент, с которым пер-

воначально обратились к функции POWER.

При необходимости все же можно добиться, чтобы функция

изменила переменную из вызывающей программы. Эта программа

должна обеспечить установление адреса переменной /техничес-

ки, через указатель на переменную/, а в вызываемой функции

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

латься к фактической переменной косвенно через него. Мы рас-

смотрим это подробно в главе 5.

Когда в качестве аргумента выступает имя массива, то

фактическим значением, передаваемым функции, является адрес

начала массива. /Здесь нет никакого копирования элементов

массива/. С помощью индексации и адреса начала функция может

найти и изменить любой элемент массива. Это - тема следующе-

го раздела.

· 32 -

1.9. Массивы символов.

По-видимому самым общим типом массива в “C” является

массив символов. Чтобы проиллюстрировать использование мас-

сивов символов и обрабатывающих их функций, давайте напишем

программу, которая читает набор строк и печатает самую длин-

ную из них. Основная схема программы достаточно проста:

WHILE (имеется еще строка)

IF (эта строка длиннее самой длинной из

предыдущих)

запомнить эту строку и ее длину

напечатать самую длинную строку

По этой схеме ясно, что программа естественным образом

распадается на несколько частей. Одна часть читает новую

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

части программы управляют этим процессом.

Поскольку все так прекрасно делится, было бы хорошо и

написать программу соответсвующим образом. Давайте сначала

напишем отдельную функцию GETLINE, которая будет извлекать

следующую строку из файла ввода; это - обобщение функции

GETCHAR. мы попытаемся сделать эту функцию по возможности

более гибкой, чтобы она была полезной и в других ситуациях.

Как минимум GETLINE должна передавать сигнал о возможном по-

явлении конца файла; более общий полезный вариант мог бы пе-

редавать длину строки или нуль, если встретится конец файла.

нуль не может быть длиной строки, так как каждая строка со-

держит по крайней мере один символ; даже строка, содержащая

только символ новой строки, имеет длину 1.

Когда мы находим строку, которая длиннее самой длинной

из предыдущих, то ее надо где-то запомнить. Это наводит на

мысль о другой функции, COPY , которая будет копировать но-

вую строку в место хранения.

Наконец, нам нужна основная программа для управления

функциями GETLINE и COPY . Вот результат :

#DEFINE MAXLINE 1000 /* MAXIMUM INPUT

LINE SIZE */

MAIN() /* FIND LONGEST LINE */

{

INT LEN; /* CURRENT LINE LENGTH */

INT MAX; /* MAXIMUM LENGTH SEEN SO FAR */

CHAR LINE[MAXLINE]; /* CURRENT INPUT LINE */

CHAR SAVE[MAXLINE]; /* LONGEST LINE, SAVED */

MAX = 0;

WHILE ((LEN = GETLINE(LINE, MAXLINE)) > 0)

IF (LEN > MAX) {

MAX = LEN;

COPY(LINE, SAVE);

}

IF (MAX > 0) /* THERE WAS A LINE */

PRINTF(“%S”, SAVE);

}

· 33 -

GETLINE(S,LIM) /* GET LINE INTO S,RETURN LENGTH */

CHAR S[];

INT LIM;

{

INT C, I;

FOR(I=0;I<LIM-1 && (C=GETCHAR())!=EOF && C!='\N';++I)

S[I] = C;

IF (C == '\N') {

S[I] = C;

++I;

}

S[I] = '\0';

RETURN(I);

}

COPY(S1, S2) /* COPY S1 TO S2;

ASSUME S2 BIG ENOUGH */

CHAR S1[], S2[];

{

INT I;

I = 0;

WHILE ((S2[I] = S1[I] != '\0')

++I;

}

Функция MAIN и GETLINE общаются как через пару аргумен-

тов, так и через возвращаемое значение. аргументы GETLINE

описаны в строках

CHAR S[];

INT LIM;

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

второй - целым.

Длина массива S не указана, так как она определена в

MAIN . функция GETLINE использует оператор RETURN для пере-

дачи значения назад в вызывающую программу точно так же, как

это делала функция POWER. Одни функции возвращают некоторое

нужное значение; другие, подобно COPY, используются из-за их

действия и не возвращают никакого значения.

Чтобы пометить конец строки символов, функция GETLINE

помещает в конец создаваемого ей массива символ \0 /нулевой

символ, значение которого равно нулю/. Это соглашение ис-

пользуется также компилятором с языка “C”: когда в “C” -

программе встречается строчная константа типа

“HELLO\N”

·
34 -

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

этой строки, и заканчивает его символом \0, с тем чтобы фун-

кции, подобные PRINTF, могли зафиксировать конец массива:

! H ! E ! L ! L ! O ! \N ! \0 !

Спецификация формата %S указывает, что PRINTF ожидает стро-

ку, представленную в такой форме. Проанализировав функцию

COPY, вы обнаружите, что и она опирается на тот факт, что ее

входной аргумент оканчивается символом \0, и копирует этот

символ в выходной аргумент S2. /Все это подразумевает, что

символ \0 не является частью нормального текста/.

Между прочим, стоит отметить, что даже в такой маленькой

программе, как эта, возникает несколько неприятных организа-

ционных проблем. Например, что должна делать MAIN, если она

встретит строку, превышающую ее максимально возможный раз-

мер? Функция GETLINE поступает разумно: при заполнении мас-

сива она прекращает дальнейшее извлечение символов, даже ес-

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

длину и последний символ, функция MAIN может установить, не

была ли эта строка слишком длинной, и поступить затем, как

она сочтет нужным. Ради краткости мы опустили эту проблему.

Пользователь функции GETLINE никак не может заранее уз-

нать, насколько длинной окажется вводимая строка. Поэтому в

GETLINE включен контроль переполнения. в то же время пользо-

ватель функции COPY уже знает /или может узнать/, каков раз-

мер строк, так что мы предпочли не включать в эту функцию

дополнительный контроль.

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

Переделайте ведущую часть программы поиска самой длинной

строки таким образом, чтобы она правильно печатала длины

сколь угодно длинных вводимых строк и возможно больший

текст.

Упржнение 1-15.

Напишите программу печати всех строк длиннее 80 симво-

лов.

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

Напишите программу, которая будет удалять из каждой

строки стоящие в конце пробелы и табуляции, а также строки,

целиком состоящие из пробелов.

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

Напишите функцию REVERSE(S), которая распологает сим-

вольную строку S в обратном порядке. С ее помощью напишите

программу, которая обратит каждую строку из файла ввода.

·
35 -

1.10. Область действия: внешние переменные.

Переменные в MAIN(LINE, SAVE и т.д.) являются внутренни-

ми или локальными по отношению к функции MAIN, потому что

они описаны внутри MAIN и никакая другая функция не имеет к

ним прямого доступа. Это же верно и относительно переменных

в других функциях; например, переменная I в функции GETLINE

никак не связана с I в COPY. Каждая локальная переменная су-

ществует только тогда, когда произошло обращение к соответс-

твующей функции, и исчезает, как только закончится выполне-

ние этой функции. По этой причине такие переменные, следуя

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

Мы впредь будем использовать термин автоматические при ссыл-

ке на эти динамические локальные переменные. /в главе 4 об-

суждается класс статической памяти, когда локальные перемен-

ные все же оказываются в состоянии сохранить свои значения

между обращениями к функциям/.

Поскольку автоматические переменные появляются и исчеза-

ют вместе с обращением к функции, они не сохраняют своих

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

чего им при каждом входе нужно явно присваивать значения.

Если этого не сделать, то они будут содержать мусор.

В качестве альтернативы к автоматическим переменным мож-

но определить переменные, которые будут внешними для всех

функций, т.е. Глобальными переменными, к которым может обра-

титься по имени любая функция, которая пожелает это сделать.

(этот механизм весьма сходен с “COMMON” в фортране и

“EXTERNAL” в PL/1). Так как внешние переменные доступны всю-

ду, их можно использовать вместо списка аргументов для пере-

дачи данных между функциями. Кроме того, поскольку внешние

переменные существуют постоянно, а не появляются и исчезают

вместе с вызываемыми функциями, они сохраняют свои значения

и после того, как функции, присвоившие им эти значения, за-

вершат свою работу.

Внешняя переменная должна быть определена вне всех функ-

ций; при этом ей выделяется фактическое место в памяти. Та-

кая переменная должна быть также описана в каждой функции,

которая собирается ее использовать; это можно сделать либо

явным описанием EXTERN, либо неявным по контексту. Чтобы

сделать обсуждение более конкретным, давайте перепишем прог-

рамму поиска самой длинной строки, сделав LINE, SAVE и MAX

внешними переменными. Это потребует изменения описаний и тел

всех трех функций, а также обращений к ним.

#DEFINE MAXLINE 1000 /* MAX. INPUT LINE SIZE*/

CHAR LINE[MAXLINE]; /* INPUT LINE */

CHAR SAVE[MAXLINE];/* LONGEST LINE SAVED HERE*/

INT MAX;/*LENGTH OF LONGEST LINE SEEN SO FAR*/

· 36 -

MAIN() /*FIND LONGEST LINE; SPECIALIZED VERSION*/

{

INT LEN;

EXTERN INT MAX;

EXTERN CHAR SAVE[];

MAX = 0;

WHILE ( (LEN = GETLINE()) > 0 )

IF ( LEN > MAX ) {

MAX = LEN;

COPY();

}

IF ( MAX > 0 ) /* THERE WAS A LINE */

PRINTF( “%S”, SAVE );

}

GETLINE() /* SPECIALIZED VERSION */

{

INT C, I;

EXTERN CHAR LINE[];

FOR (I = 0; I < MAXLINE-1

&& (C=GETCHAR()) !=EOF && C!='\N'; ++I)

LINE[I] = C;

++I;

}

LINE[I] = '\0'

RETURN(I)

}

COPY() /* SPECIALIZED VERSION */

{

INT I;

EXTERN CHAR LINE[], SAVE[];

I = 0;

WHILE ((SAVE[I] = LINE[I]) !='\0')

++I;

}

Внешние переменные для функций MAIN, GETLINE и COPY оп-

ределены в первых строчках приведенного выше примера, кото-

рыми указывается их тип и вызывается отведение для них памя-

ти. синтаксически внешние описания точно такие же, как опи-

сания, которые мы использовали ранее, но так как они распо-

ложены вне функций, соответствующие переменные являются

внешними. Чтобы функция могла использовать внешнюю переме-

ную, ей надо сообщить ее имя. Один способ сделать это -

включить в функцию описание EXTERN; это описание отличается

от предыдущих только добавлением ключевого слова EXTERN.

· 37 -

В определенных ситуациях описание EXTERN может быть опу-

щено: если внешнее определение переменной находится в том же

исходном файле, раньше ее использования в некоторой конкрет-

ной функции, то не обязательно включать описание EXTERN для

этой переменной в саму функцию. Описания EXTERN в функциях

MAIN, GETLINE и COPY являются, таким образом, излишними.

Фактически, обычная практика заключается в помещении опреде-

лений всех внешних переменных в начале исходного файла и

последующем опускании всех описаний EXTERN.

Если программа находится в нескольких исходных файлах, и

некоторая переменная определена, скажем в файле 1, а исполь-

зуется в файле 2, то чтобы связать эти два вхождения пере-

менной, необходимо в файле 2 использовать описание EXTERN.

Этот вопрос подробно обсуждается в главе 4.

Вы должно быть заметили, что мы в этом разделе при ссыл-

ке на внешние переменные очень аккуратно используем слова

описание и определение. “Определение” относится к тому мес-

ту, где переменная фактически заводится и ей выделяется па-

мять; “описание” относится к тем местам, где указывается

природа переменной, но никакой памяти не отводится.

Между прочим, существует тенденция объявлять все, что ни

попадется, внешними переменными, поскольку кажется, что это

упрощает связи, - списки аргументов становятся короче и пе-

ременные всегда присутствуют, когда бы вам они ни понадоби-

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

них не нуждаетесь. Такой стиль программирования чреват опас-

ностью, так как он приводит к программам, связи данных внут-

ри которых не вполне очевидны. Переменные при этом могут из-

меняться неожиданным и даже неумышленным образом, а програм-

мы становится трудно модифицировать, когда возникает такая

необходимость. Вторая версия программы поиска самой длинной

строки уступает первой отчасти по этим причинам, а отчасти

потому, что она лишила универсальности две весьма полезные

функции, введя в них имена переменных, с которыми они будут

манипулировать.

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

Проверка в операторе FOR функции GETLINE довольно неук-

люжа. Перепишите программу таким образом, чтобы сделать эту

проверку более ясной, но сохраните при этом то же самое по-

ведение в конце файла и при переполнении буфера. Является ли

это поведение самым разумным?

1.11. Резюме

На данном этапе мы обсудили то, что можно бы назвать

традиционным ядром языка “C”. Имея эту горсть строительных

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

размера, и было бы вероятно неплохой идеей, если бы вы за-

держались здесь на какое-то время и поступили таким образом:

следующие ниже упражнения предлагают вам ряд программ нес-

колько большей сложности, чем те, которые были приведены в

этой главе.

· 38 -

После того как вы овладеете этой частью “C”, приступайте

к чтению следующих нескольких глав. Усилия, которые вы при

этом затратите, полностью окупятся, потому что в этих главах

обсуждаются именно те стороны “C”, где мощь и выразитель-

ность языка начинает становиться очевидной.

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

Напишите программу DETAB, которая заменяет табуляции во

вводе на нужное число пробелов так, чтобы промежуток дости-

гал следующей табуляционной остановки. Предположите фиксиро-

ванный набор табуляционных остановок, например, через каждые

N позиций.

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

Напишите программу ENTAB, которая заменяет строки пробе-

лов минимальным числом табуляций и пробелов, достигая при

этом тех же самых промежутков. Используйте те же табуляцион-

ные остановки, как и в DETAB.

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

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

после последнего отличного от пробела символа, стоящего до

столбца N ввода, где N - параметр. убедитесь, что ваша прог-

рамма делает что-то разумное с очень длинными строками и в

случае, когда перед указанным столбцом нет ни табуляций, ни

пробелов.

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

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

ментариев. Не забывайте аккуратно обращаться с “закавыченны-

ми” строками и символьными константами.

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

Напишите программу проверки “C”-программы на элементар-

ные синтаксические ошибки, такие как несоответствие круглых,

квадратных и фигурных скобок. Не забудьте о кавычках, как

одиночных, так и двойных, и о комментариях. (Эта программа

весьма сложна, если вы будете писать ее для самого общего

случая).

· 39 -

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

Переменные и константы являются основными объектами, с

которыми оперирует программа. Описания перечисляют перемен-

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

можно, их начальные значения. Операции определяют, что с ни-

ми будет сделано. выражения объединяют переменные и констан-

ты для получения новых значений. Все это - темы настоящей

главы.

2.1. Имена переменных.

Хотя мы этого сразу прямо не сказали, существуют некото-

рые ограничения на имена переменных и символических конс-

тант. Имена составляются из букв и цифр; первый символ дол-

жен быть буквой. Подчеркивание “_” тоже считается буквой;

это полезно для удобочитаемости длинных имен переменных.

Прописные и строчные буквы различаются; традиционная практи-

ка в “с” - использовать строчные буквы для имен переменных,

а прописные - для символических констант.

Играют роль только первые восемь символов внутреннего

имени, хотя использовать можно и больше. Для внешних имен,

таких как имена функций и внешних переменных, это число мо-

жет оказаться меньше восьми, так как внешние имена использу-

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

дятся в приложении а. Кроме того, такие ключевые слова как

IF, ELSE, INT, FLOAT и т.д., зарезервированы: вы не можете

использовать их в качестве имен переменных. (Они пишутся

строчными буквами).

Конечно, разумно выбирать имена переменных таким обра-

зом, чтобы они означали нечто, относящееся к назначению пе-

ременных, и чтобы было менее вероятно спутать их при написа-

нии.

2.2. Типы и размеры данных.

Языке “C” имеется только несколько основных типов дан-

ных:

CHAR один байт, в котором может находиться один символ из

внутреннего набора символов.

INT Целое, обычно соответствующее естественному размеру це-

лых в используемой машине.

FLOAT С плавающей точкой одинарной точности.

DOUBLE С плавающей точкой двойной точности.

Кроме того имеется ряд квалификаторов, которые можно ис-

пользовать с типом INT: SHORT (короткое), LONG (длинное) и

UNSIGNED (без знака). Квалификаторы SHORT и LONG указывают

на различные размеры целых. Числа без знака подчиняются за-

конам арифметики по модулю 2 в степени N, где N - число би-

тов в INT; числа без знаков всегда положительны. Описания с

квалификаторами имеют вид:

SHORT INT X;

LONG INT Y;

UNSIGNED INT Z;

·
40 -

Cлово INT в таких ситуациях может быть опущено, что

обычно и делается.

Количество битов, отводимых под эти объекты зависит от

имеющейся машины; в таблице ниже приведены некоторые харак-

терные значения.

Таблица 1

!

DEC PDP-11 HONEYWELL IBM 370 INTERDATA !

6000 8/32 !

!

ASCII ASCII EBCDIC ASCII !

!

CHAR 8-BITS 9-BITS 8-BITS 8-BITS !

INT 16 36 32 32 !

SHORT 16 36 16 16 !

LONG 32 36 32 32 !

FLOAT 32 36 32 32 !

DOUBLE 64 72 64 64 !

!

Цель состоит в том, чтобы SHORT и LONG давали возмож-

ность в зависимости от практических нужд использовать раз-

личные длины целых; тип INT отражает наиболее “естественный”

размер конкретной машины. Как вы видите, каждый компилятор

свободно интерпретирует SHORT и LONG в соответствии со свои-

ми аппаратными средствами. Все, на что вы можете твердо по-

лагаться, это то, что SHORT не длиннее, чем LONG.

2.3. Константы.

Константы типа INT и FLOAT мы уже рассмотрели. Отметим

еще только, что как обычная

123.456е-7,

так и “научная” запись

0.12е3

для FLOAT является законной.

Каждая константа с плавающей точкой считается имеющей

тип DOUBLE, так что обозначение “E” служит как для FLOAT,

так и для DOUBLE.

Длинные константы записываются в виде 123L. Обычная це-

лая константа, которая слишком длинна для типа INT, рассмат-

ривается как LONG.

·
41 -

Существует система обозначений для восьмеричных и шест-

надцатеричных констант: лидирующий 0(нуль) в константе типа

INT указывает на восьмеричную константу, а стоящие впереди

0X соответствуют шестнадцатеричной константе. Например, де-

сятичное число 31 можно записать как 037 в восьмеричной фор-

ме и как 0X1F в шестнадцатеричной. Шестнадцатеричные и вось-

меричные константы могут также заканчиваться буквой L, что

делает их относящимися к типу LONG.

2.3.1. Символьная константа.

Символьная константа - это один символ, заключенный в

одинарные кавычки, как, например, 'х'. Значением символьной

константы является численное значение этого символа во внут-

реннем машинном наборе символов. Например, в наборе символов

ASCII символьный нуль, или '0', имеет значение 48, а в коде

EBCDIC - 240, и оба эти значения совершенно отличны от числа

0. Написание '0' вместо численного значения, такого как 48

или 240, делает программу не зависящей от конкретного чис-

ленного представления этого символа в данной машине. Сим-

вольные константы точно так же участвуют в численных опера-

циях, как и любые другие числа, хотя наиболее часто они ис-

пользуются в сравнении с другими символами. Правила преобра-

зования будут изложены позднее.

Некоторые неграфические символы могут быть представлены

как символьные константы с помощью условных последователь-

ностей, как, например, \N (новая строка), \T (табуляция), \0

(нулевой символ), \\ (обратная косая черта), \' (одинарная

кавычка) и т.д. Хотя они выглядят как два символа, на самом

деле являются одним. Кроме того, можно сгенерировать произ-

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

если написать

'\DDD'

где DDD - от одной до трех восьмеричных цифр, как в

#DEFINE FORMFEED '\014' /* FORM FEED */

Символьная константа '\0', изображающая символ со значе-нием 0, часто записывается вместо целой константы 0 , чтобы подчеркнуть символьную природу некоторого выражения.

2.3.2. Константное выражение

Константное выражение - это выражение, состоящее из од-

них констант. Такие выражения обрабатываются во время компи-

ляции, а не при прогоне программы, и соответственно могут

быть использованы в любом месте, где можно использовать кон-

станту, как, например в

· 42 -

#DEFINE MAXLINE 1000

CHAR LINE[MAXLINE+1];

или

SECONDS = 60 * 60 * HOURS;

2.3.3. Строчная константа

Строчная константа - это последовательность, состоящая

из нуля или более символов, заключенных в двойные кавычки,

как, например,

“I AM A STRING” /* я - строка */

или

“” /* NULL STRING / / нуль-строка */

Кавычки не являются частью строки, а служат только для

ее ограничения. те же самые условные последовательности, ко-

торые использовались в символьных константах, применяются и

в строках; символ двойной кавычки изображается как \”.

С технической точки зрения строка представляет собой

массив, элементами которого являются отдельные символы. Что-

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

тор автоматически помещает в конец каждой строки нуль-символ

\0. Такое представление означает, что не накладывается конк-

ретного ограничения на то, какую длину может иметь строка, и

чтобы определить эту длину, программы должны просматривать

строку полностью. При этом для физического хранения строки

требуется на одну ячейку памяти больше, чем число заключен-

ных в кавычки символов. Следующая функция STRLEN(S) вычисля-

ет длину символьной строки S не считая конечный символ \0.

STRLEN(S) /* RETURN LENGTH OF S */

CHAR S[];

{

INT I;

I = 0;

WHILE (S[I] != '\0')

++I;

RETURN(I);

}

Будьте внимательны и не путайте символьную константу со

строкой, содержащей один символ: 'X' - это не то же самое,

что “X”. Первое - это отдельный символ, использованный с

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

х в машинном наборе символов. Второе - символьная строка,

состоящая из одного символа (буква х) и \0.

· 43 -

2.4. Описания

Все переменные должны быть описаны до их использования,

хотя некоторые описания делаются неявно, по контексту. Опи-

сание состоит из спецификатора типа и следующего за ним

списка переменных, имеющих этот тип, как, например,

INT LOWER, UPPER, STEP;

CHAR C, LINE[1000];

Переменные можно распределять по описаниям любым обра-

зом; приведенные выше списки можно с тем же успехом записать

в виде

INT LOWER;

INT UPPER;

INT STEP;

CHAR C;

CHAR LINE[1000];

Такая форма занимает больше места, но она удобна для до-бавления комментария к каждому описанию и для последующих модификаций.

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

ри их описания, хотя здесь имеются некоторые ограничения.

Если за именем переменной следуют знак равенства и констан-

та, то эта константа служит в качестве инициализатора, как,

например, в

CHAR BACKSLASH = '\\';

INT I = 0;

FLOAT EPS = 1.0E-5;

Если рассматриваемая переменная является внешней или

статической, то инициализация проводится только один раз,

согласно концепции до начала выполнения программы. Инициали-

зируемым явно автоматическим переменным начальные значения

присваиваются при каждом обращении к функции, в которой они

описаны. Автоматические переменные, не инициализируемые яв-

но, имеют неопределенные значения, (т.е. мусор). Внешние и

статические переменные по умолчанию инициализируются нулем,

но, тем не менее, их явная инициализация является признаком

хорошего стиля.

Мы продолжим обсуждение вопросов инициализации, когда

будем описывать новые типы данных.

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

Бинарными арифметическими операциями являются +, -, *, /

и операция деления по модулю %. Имеется унарная операция -,

но не существует унарной операции +.

·
44 -

При делении целых дробная часть отбрасывается. Выражение

X % Y

дает остаток от деления X на Y и, следовательно, равно нулю,

когда х делится на Y точно. Например, год является високос-

ным, если он делится на 4, но не делится на 100, исключая

то, что делящиеся на 400 годы тоже являются високосными. По-

этому

IF(YEAR % 4 == 0 && YEAR % 100 != 0 \!\! YEAR % 400 == 0)

год високосный

ELSE

год невисокосный

Операцию % нельзя использовать с типами FLOAT или

DOUBLE.

Операции + и - имеют одинаковое старшинство, которое

младше одинакового уровня старшинства операций *, / и %, ко-

торые в свою очередь младше унарного минуса. Арифметические

операции группируются слева направо. (Сведения о старшинстве

и ассоциативности всех операций собраны в таблице в конце

этой главы). Порядок выполнения ассоциативных и коммутатив-

ных операций типа + и - не фиксируется; компилятор может пе-

регруппировывать даже заключенные в круглые скобки выраже-

ния, связанные такими операциями. таким образом, а+(B+C) мо-

жет быть вычислено как (A+B)+C. Это редко приводит к како-

му-либо расхождению, но если необходимо обеспечить строго

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

точные переменные.

Действия, предпринимаемые при переполнении и антипере-

полнении (т.е. При получении слишком маленького по абсолют-

ной величине числа), зависят от используемой машины.

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

Операциями отношения являются

=> > =< <

все они имеют одинаковое старшинство. Непосредственно за ни-

ми по уровню старшинства следуют операции равенства и нера-

венства:

== !=

которые тоже имеют одинаковое старшинство. операции отноше-

ния младше арифметических операций, так что выражения типа

I<LIM-1 понимаются как I<(LIM-1), как и предполагается.

Логические связки && и \!\! более интересны. Выражения,

связанные операциями && и \!\!, вычисляются слева направо,

причем их рассмотрение прекращается сразу же как только ста-

новится ясно, будет ли результат истиной или ложью. учет

этих свойств очень существенен для написания правильно рабо-

тающих программ. Рассмотрим, например, оператор цикла в счи-

тывающей строку функции GETLINE, которую мы написали в главе

1.

· 45 -

FOR(I=0;I<LIM-1 && (C=GETCHAR()) != '\N' && C != EOF; ++I)

S[I]=C;

Ясно, что перед считыванием нового символа необходимо

проверить, имеется ли еще место в массиве S, так что условие

I<LIM-1 должно проверяться первым. И если это условие не вы-

полняется, мы не должны считывать следующий символ.

Так же неудачным было бы сравнение 'C' с EOF до обраще-

ния к функции GETCHAR : прежде чем проверять символ, его

нужно считать.

Старшинство операции && выше, чем у \!\!, и обе они

младше операций отношения и равенства. Поэтому такие выраже-

ния, как

I<LIM-1 && (C = GETCHAR()) != '\N' && C != EOF

не нуждаются в дополнительных круглых скобках. Но так как

операция != старше операции присваивания, то для достижения

правильного результата в выражении

(C = GETCHAR()) != '\N'

кобки необходимы.

Унарная операция отрицания ! Преобразует ненулевой или

истинный операнд в 0, а нулевой или ложный операнд в 1.

Обычное использование операции ! Заключается в записи

IF( ! INWORD )

Вместо

IF( INWORD == 0 )

Tрудно сказать, какая форма лучше. Конструкции типа ! INWORD

Читаются довольно удобно (“если не в слове”). Но в более

сложных случаях они могут оказаться трудными для понимания.

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

Напишите оператор цикла, эквивалентный приведенному выше

оператору FOR, не используя операции &&.

2.7. Преобразование типов

Если в выражениях встречаются операнды различных типов,

то они преобразуются к общему типу в соответствии с неболь-

шим набором правил. В общем, автоматически производятся

только преобразования, имеющие смысл, такие как, например,

преобразование целого в плавающее в выражениях типа F+I. Вы-

ражения же, лишенные смысла, такие как использование пере-

менной типа FLOAT в качестве индекса, запрещены.

Во-первых, типы CHAR и INT могут свободно смешиваться в

арифметических выражениях: каждая переменная типа CHAR авто-

матически преобразуется в INT. Это обеспечивает значительную

гибкость при проведении определенных преобразований симво-

лов. Примером может служить функция ATOI, которая ставит в

соответствие строке цифр ее численный эквивалент.

· 46 -

ATOI(S) /* CONVERT S TO INTEGER */

CHAR S[];

{

INT I, N;

N = 0;

FOR ( I = 0; S[I]>='0' && S[I]<='9'; ++I)

N = 10 * N + S[I] - '0';

RETURN(N);

}

KAK Уже обсуждалось в главе 1, выражение

S[I] - '0'

имеет численное значение находящегося в S[I] символа, потому

что значение символов '0', '1' и т.д. образуют возрастающую

последовательность расположенных подряд целых положительных

чисел.

Другой пример преобразования CHAR в INT дает функция

LOWER, преобразующая данную прописную букву в строчную. Если

выступающий в качестве аргумента символ не является пропис-

ной буквой, то LOWER возвращает его неизменным. Приводимая

ниже программа справедлива только для набора символов ASCII.

LOWER© /* CONVERT C TO LOWER CASE; ASCII ONLY */

INT C;

{

IF ( C >= 'A' && C <= 'Z' )

RETURN( C + '@' - 'A');

ELSE /*@ Записано вместо 'A' строчного*/

RETURN©;

}

Эта функция правильно работает при коде ASCII, потому что

численные значения, соответствующие в этом коде прописным и

строчным буквам, отличаются на постоянную величину, а каждый

алфавит является сплошным - между а и Z нет ничего, кроме

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

систем IBM 360/370 оказывается несправедливым, в силу чего

эта программа на таких системах работает неправильно - она

преобразует не только буквы.

При преобразовании символьных переменных в целые возни-

кает один тонкий момент. Дело в том, что сам язык не указы-

вает, должны ли переменным типа CHAR соответствовать числен-

ные значения со знаком или без знака. Может ли при преобра-

зовании CHAR в INT получиться отрицательное целое? К сожале-

нию, ответ на этот вопрос меняется от машины к машине, отра-

жая расхождения в их архитектуре. На некоторых машинах

(PDP-11, например) переменная типа CHAR, крайний левый бит

которой содержит 1, преобразуется в отрицательное целое

(“знаковое расширение”). На других машинах такое преобразо-

вание сопровождается добавлением нулей с левого края, в ре-

зультате чего всегда получается положительное число.

· 47 -

Определение языка “C” гарантирует, что любой символ из

стандартного набора символов машины никогда не даст отрица-

тельного числа, так что эти символы можно свободно использо-

вать в выражениях как положительные величины. Но произволь-

ные комбинации двоичных знаков, хранящиеся как символьные

переменные на некоторых машинах, могут дать отрицательные

значения, а на других положительные.

Наиболее типичным примером возникновения такой ситуации

является сучай, когда значение 1 используется в качестве

EOF. Рассмотрим программу

CHAR C;

C = GETCHAR();

IF ( C == EOF )

...

На машине, которая не осуществляет знакового расширения,

переменная 'с' всегда положительна, поскольку она описана

как CHAR, а так как EOF отрицательно, то условие никогда не

выполняется. Чтобы избежать такой ситуации, мы всегда пре-

дусмотрительно использовали INT вместо CHAR для любой пере-

менной, получающей значение от GETCHAR.

Основная же причина использования INT вместо CHAR не

связана с каким-либо вопросом о возможном знаковом расшире-

нии. просто функция GETCHAR должна передавать все возможные

символы (чтобы ее можно было использовать для произвольного

ввода) и, кроме того, отличающееся значение EOF. Следова-

тельно значение EOF не может быть представлено как CHAR, а

должно храниться как INT.

Другой полезной формой автоматического преобразования

типов является то, что выражения отношения, подобные I>J, и

логические выражения, связанные операциями && и \!\!, по оп-

ределению имеют значение 1, если они истинны, и 0, если они

ложны. Таким образом, присваивание

ISDIGIT = C >= '0' && C <= '9';

полагает ISDIGIT равным 1, если с - цифра, и равным 0 в про-

тивном случае. (В проверочной части операторов IF, WHILE,

FOR и т.д. “Истинно” просто означает “не нуль”).

Неявные арифметические преобразования работают в основ-

ном, как и ожидается. В общих чертах, если операция типа +

или *, которая связывает два операнда (бинарная операция),

имеет операнды разных типов, то перед выполнением операции

“низший” тип преобразуется к “высшему” и получается резуль-

тат “высшего” типа. Более точно, к каждой арифметической

операции применяется следующая последовательность правил

преобразования.

· Типы CHAR и SHORT преобразуются в INT, а FLOAT в

DOUBLE.

·
48 -

· Затем, если один из операндов имеет тип DOUBLE, то

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

· В противном случае, если один из операндов имеет тип LONG, то другой преобразуется в LONG, и результат имеет тип LONG.

· В противном случае, если один из операндов имеет тип UNSIGNED, то другой преобразуется в UNSIGNED и результат имеет тип UNSIGNED.

· В противном случае операнды должны быть типа INT, и

результат имеет тип INT.

Подчеркнем, что все переменные типа FLOAT в выражениях пре-

образуются в DOUBLE; в “C” вся плавающая арифметика выполня-

ется с двойной точностью.

Преобразования возникают и при присваиваниях; значение

правой части преобразуется к типу левой, который и является

типом результата. Символьные переменные преобразуются в це-

лые либо со знаковым расширением ,либо без него, как описано

выше. Обратное преобразование INT в CHAR ведет себя хорошо -

лишние биты высокого порядка просто отбрасываются. Таким об-

разом

INT I;

CHAR C;

I = C;

C = I;

значение 'с' не изменяется. Это верно независимо от того,

вовлекается ли знаковое расширение или нет.

Если х типа FLOAT, а I типа INT, то как

х = I;

так и

I = х;

приводят к преобразованиям; при этом FLOAT преобразуется в

INT отбрасыванием дробной части. Тип DOUBLE преобразуется во

FLOAT округлением. Длинные целые преобразуются в более ко-

роткие целые и в переменные типа CHAR посредством отбрасыва-

ния лишних битов высокого порядка.

Так как аргумент функции является выражением, то при пе-

редаче функциям аргументов также происходит преобразование

типов: в частности, CHAR и SHORT становятся INT, а FLOAT

становится DOUBLE. Именно поэтому мы описывали аргументы

функций как INT и DOUBLE даже тогда, когда обращались к ним

с переменными типа CHAR и FLOAT.

Наконец, в любом выражении может быть осуществлено

(“принуждено”) явное преобразование типа с помощью конструк-

ции, называемой перевод (CAST). В этой конструкции, имеющей

вид

(имя типа) выражение

· 49 -

Выражение преобразуется к указанному типу по правилам

преобразования, изложенным выше. Фактически точный смысл

операции перевода можно описать следующим образом: выражение

как бы присваивается некоторой переменной указанного типа,

которая затем используется вместо всей конструкции. Напри-

мер, библиотечная процедура SQRT ожидает аргумента типа

DOUBLE и выдаст бессмысленный ответ, если к ней по небреж-

ности обратятся с чем-нибудь иным. таким образом, если N -

целое, то выражение

SQRT((DOUBLE) N)

до передачи аргумента функции SQRT преобразует N к типу

DOUBLE. (Отметим, что операция перевод преобразует значение

N в надлежащий тип; фактическое содержание переменной N при

этом не изменяется). Операция перевода имрация перевода име-

ет тот же уровень старшинства, что и другие унарные опера-

ции, как указывается в таблице в конце этой главы.

Упражнение 2-2.

Составьте программу для функции HTOI(S), которая преоб-

разует строку шестнадцатеричных цифр в эквивалентное ей це-

лое значение. При этом допустимыми цифрами являются цифры от

1 до 9 и буквы от а до F.

2.8. Операции увеличения и уменьшения

В языке “C” предусмотрены две необычные операции для

увеличения и уменьшения значений переменных. Операция увели-

чения ++ добавляет 1 к своему операнду, а операция уменьше-

ния—вычитает 1. Мы часто использовали операцию ++ для

увеличения переменных, как, например, в

IF(C == '\N')

++I;

Необычный аспект заключается в том, что ++ и—можно

использовать либо как префиксные операции (перед переменной,

как в ++N), либо как постфиксные (после переменной: N++).

Эффект в обоих случаях состоит в увеличении N. Но выражение

++N увеличивает переменную N до использования ее значения, в

то время как N++ увеличивает переменную N после того, как ее

значение было использовано. Это означает, что в контексте,

где используется значение переменной, а не только эффект

увеличения, использование ++N и N++ приводит к разным ре-

зультатам. Если N = 5, то

х = N++;

устанавливает х равным 5, а

х = ++N;

·
50 -

полагает х равным 6. В обоих случаях N становится равным 6.

Операции увеличения и уменьшения можно применять только к

переменным; выражения типа х=(I+J)++ являются незаконными.

В случаях, где нужен только эффект увеличения, а само

значение не используется, как, например, в

IF ( C == '\N' )

NL++;

выбор префиксной или постфиксной операции является делом

вкуса. но встречаются ситуации, где нужно использовать имен-

но ту или другую операцию. Рассмотрим, например, функцию

SQUEEZE(S,C), которая удаляет символ 'с' из строки S, каждый

раз, как он встречается.

SQUEEZE(S,C) /* DELETE ALL C FROM S */

CHAR S[];

INT C;

{

INT I, J;

FOR ( I = J = 0; S[I] != '\0'; I++)

IF ( S[I] != C )

S[J++] = S[I];

S[J] = '\0';

}

Каждый раз, как встечается символ, отличный от 'с', он копи-

руется в текущую позицию J, и только после этого J увеличи-

вается на 1, чтобы быть готовым для поступления следующего

символа. Это в точности эквивалентно записи

IF ( S[I] != C ) {

S[J] = S[I];

J++;

}

Другой пример подобной конструкции дает функция GETLINE,

которую мы запрограммировали в главе 1, где можно заменить

IF ( C == '\N' ) {

S[I] = C;

++I;

}

более компактной записью

IF ( C == '\N' )

S[I++] = C;

· 51 -

В качестве третьего примера рассмотрим функцию

STRCAT(S,T), которая приписывает строку т в конец строки S,

образуя конкатенацию строк S и т. При этом предполагается,

что в S достаточно места для хранения полученной комбинации.

STRCAT(S,T) /* CONCATENATE T TO END OF S */

CHAR S[], T[]; /* S MUST BE BIG ENOUGH */

{

INT I, J;

I = J = 0;

WHILE (S[I] != '\0') / *FIND END OF S */

I++;

WHILE((S[I++] = T[J++]) != '\0') /*COPY T*/

;

}

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

к следующему прохождению цикла постфиксная операция ++ при-

меняется к обеим переменным I и J.

Упражнение 2-3.

Напишите другой вариант функции SQUEEZE(S1,S2), который

удаляет из строки S1 каждый символ, совпадающий с каким-либо

символом строки S2.

Упражнение 2-4.

Напишите программу для функции ANY(S1,S2), которая нахо-

дит место первого появления в строке S1 какого-либо символа

из строки S2 и, если строка S1 не содержит символов строки

S2, возвращает значение -1.

2.9. Побитовые логические операции

В языке предусмотрен ряд операций для работы с битами;

эти операции нельзя применять к переменным типа FLOAT или

DOUBLE.

& Побитовое AND

\! Побитовое включающее OR

^ побитовое исключающее OR

<< сдвиг влево

>> сдвиг вправо

\^ дополнение (унарная операция)

“\” иммитирует вертикальную черту.

Побитовая операция AND часто используется для маскирования

некоторого множества битов; например, оператор

C = N & 0177

· 52 -

передает в 'с' семь младших битов N , полагая остальные рав-

ными нулю. Операция 'э' побитового OR используется для вклю-

чения битов:

C = X э MASK

устанавливает на единицу те биты в х , которые равны единице

в MASK.

Следует быть внимательным и отличать побитовые операции

& и 'э' от логических связок && и \!\! , Которые подразуме-

вают вычисление значения истинности слева направо. Например,

если х=1, а Y=2, то значение х&Y равно нулю , в то время как

значение X&&Y равно единице./почему?/

Операции сдвига << и >> осуществляют соответственно

сдвиг влево и вправо своего левого операнда на число битовых

позиций, задаваемых правым операндом. Таким образом , х<<2

сдвигает х влево на две позиции, заполняя освобождающиеся

биты нулями, что эквивалентно умножению на 4. Сдвиг вправо

величины без знака заполняет освобождающиеся биты на некото-

рых машинах, таких как PDP-11, заполняются содержанием зна-

кового бита /”арифметический сдвиг”/, а на других - нулем

/”логический сдвиг”/.

Унарная операция \^ дает дополнение к целому; это озна-

чает , что каждый бит со значением 1 получает значение 0 и

наоборот. Эта операция обычно оказывается полезной в выраже-

ниях типа

X & \^077

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

что выражение X&\^077 не зависит от длины слова и поэтому

предпочтительнее, чем, например, X&0177700, где предполага-

ется, что х занимает 16 битов. Такая переносимая форма не

требует никаких дополнительных затрат, поскольку \^077 явля-

ется константным выражением и, следовательно, обрабатывается

во время компиляции.

Чтобы проиллюстрировать использование некоторых операций

с битами, рассмотрим функцию GETBITS(X,P,N), которая возвра-

щает /сдвинутыми к правому краю/ начинающиеся с позиции р

поле переменной х длиной N битов. мы предполагаем , что

крайний правый бит имеет номер 0, и что N и р - разумно за-

данные положительные числа. например, GETBITS(х,4,3) возвра-

щает сдвинутыми к правому краю биты, занимающие позиции 4,3

и 2.

GETBITS(X,P,N) /* GET N BITS FROM POSITION P */

UNSIGNED X, P, N;

{

RETURN((X >> (P+1-N)) & \^(\^0 << N));

}

· 53 -

Операция X >> (P+1-N) сдвигает желаемое поле в правый конец

слова. Описание аргумента X как UNSIGNED гарантирует, что

при сдвиге вправо освобождающиеся биты будут заполняться ну-

лями, а не содержимым знакового бита, независимо от того, на

какой машине пропускается программа. Все биты константного

выражения \^0 равны 1; сдвиг его на N позиций влево с по-

мощью операции \^0<<N создает маску с нулями в N крайних

правых битах и единицами в остальных; дополнение \^ создает

маску с единицами в N крайних правых битах.

Упражнение 2-5.

Переделайте GETBITS таким образом, чтобы биты отсчитыва-

лись слева направо.

Упражнение 2-6.

Напишите программу для функции WORDLENGTH(), вычисляющей

длину слова используемой машины, т.е. Число битов в перемен-

ной типа INT. Функция должна быть переносимой, т.е. Одна и

та же исходная программа должна правильно работать на любой

машине.

Упражнение 2-7.

Напишите программу для функции RIGHTROT(N,B), сдвигающей

циклически целое N вправо на B битовых позиций.

Упражнение 2-8.

Напишите программу для функции INVERT(X,P,N), которая

инвертирует (т.е. Заменяет 1 на 0 и наоборот) N битов X, на-

чинающихся с позиции P, оставляя другие биты неизмененными.

2.10. Операции и выражения присваивания.

Такие выражения, как

I = I + 2

в которых левая часть повторяется в правой части могут быть

записаны в сжатой форме

I += 2

используя операцию присваивания вида +=.

Большинству бинарных операций (операций подобных +, ко-

торые имеют левый и правый операнд) соответствует операция

присваивания вида оп=, где оп - одна из операций

+ - * / % << >> & \^ \!

Если е1 и е2 - выражения, то

· 54 -

е1 оп= е2

эквивалентно

е1 = (е1) оп (е2)

за исключением того, что выражение е1 вычисляется только

один раз. Обратите внимание на круглые скобки вокруг е2:

X *= Y + 1

то

X = X * (Y + 1)

не

X = X * Y + 1

В качестве примера приведем функцию BITCOUNT, которая подсчитывает число равных 1 битов у целого аргумента.

BITCOUNT(N) /* COUNT 1 BITS IN N */

UNSIGNED N;

(

INT B;

FOR (B = 0; N != 0; N >>= 1)

IF (N & 01)

B++;

RETURN(B);

)

Не говоря уже о краткости, такие операторы приваивания

имеют то преимущество, что они лучше соответствуют образу

человеческого мышления. Мы говорим: “прибавить 2 к I” или

“увеличить I на 2”, но не “взять I, прибавить 2 и поместить

результат опять в I”. Итак, I += 2. Кроме того, в громоздких

выражениях, подобных

YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2

Tакая операция присваивания облегчает понимание программы,

так как читатель не должен скрупулезно проверять, являются

ли два длинных выражения действительно одинаковыми, или за-

думываться, почему они не совпадают. Такая операция присваи-

вания может даже помочь компилятору получить более эффектив-

ную программу.

Мы уже использовали тот факт, что операция присваивания

имеет некоторое значение и может входить в выражения; самый

типичный пример

· 55 -

WHILE ((C = GETCHAR()) != EOF)

присваивания, использующие другие операции присваивания (+=,

-= и т.д.) также могут входить в выражения, хотя это случа-

ется реже.

Типом выражения присваивания является тип его левого

операнда.

Упражнение 2-9.

В двоичной системе счисления операция X&(X-1) обнуляет

самый правый равный 1 бит переменной X.(почему?) используйте

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

BITCOUNT.

2.11. Условные выражения.

Операторы

IF (A > B)

Z = A;

ELSE

Z = B;

конечно вычисляют в Z максимум из а и в. Условное выражение,

записанное с помощью тернарной операции “?:”, предоставляет

другую возможность для записи этой и аналогичных конструк-

ций. В выражении

е1 ? Е2 : е3

сначала вычисляется выражение е1. Если оно отлично от нуля

(истинно), то вычисляется выражение е2, которое и становится

значением условного выражения. В противном случае вычисляет-

ся е3, и оно становится значением условного выражения. Каж-

дый раз вычисляется только одно из выражения е2 и е3. Таким

образом, чтобы положить Z равным максимуму из а и в, можно

написать

Z = (A > B) ? A : B; /* Z = MAX(A,B) */

Следует подчеркнуть, что условное выражение действитель-

но является выражением и может использоваться точно так же,

как любое другое выражение. Если е2 и е3 имеют разные типы,

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

рассмотренным ранее в этой главе. например, если F имеет тип

FLOAT, а N - тип INT, то выражение

(N > 0) ? F : N

Имеет тип DOUBLE независимо от того, положительно ли N или

нет.

· 56 -

Так как уровень старшинства операции ?: очень низок,

прямо над присваиванием, то первое выражение в условном вы-

ражении можно не заключать в круглые скобки. Однако, мы все

же рекомендуем это делать, так как скобки делают условную

часть выражения более заметной.

Использование условных выражений часто приводит к корот-

ким программам. Например, следующий ниже оператор цикла пе-

чатает N элементов массива, по 10 в строке, разделяя каждый

столбец одним пробелом и заканчивая каждую строку (включая

последнюю) одним символом перевода строки.

OR (I = 0; I < N; I++)

PRINTF(“%6D%C”,A[I],(I%10==9 \!\! I==N-1) ? '\N' : ' ')

Символ перевода строки записывается после каждого десятого

элемента и после N-го элемента. За всеми остальными элемен-

тами следует один пробел. Хотя, возможно, это выглядит муд-

реным, было бы поучительным попытаться записать это, не ис-

пользуя условного выражения.

Упражнение 2-10.

Перепишите программу для функции LOWER, которая переводит

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

IF-ELSE условное выражение.

2.12. Старшинство и порядок вычисления.

В приводимой ниже таблице сведены правила старшинства и ас-

социативности всех операций, включая и те, которые мы еще не

обсуждали. Операции, расположенные в одной строке, имеют

один и тот же уровень старшинства; строки расположены в по-

рядке убывания старшинства. Так, например, операции *, / и %

имеют одинаковый уровень старшинства, который выше, чем уро-

вень операций + и -.

OPERATOR ASSOCIATIVITY

() [] -> . LEFT TO RIGHT

! \^ ++ -- - (TYPE) * & SIZEOF RIGHT TO LEFT

* / % LEFT TO RIGHT

+ - LEFT TO RIGHT

<< >> LEFT TO RIGHT

< <= > >= LEFT TO RIGHT

· 57 -

== != LEFT TO RIGHT

& LEFT TO RIGHT

^ LEFT TO RIGHT

\! LEFT TO RIGHT

&& LEFT TO RIGHT

\!\! LEFT TO RIGHT

?: RIGHT TO LEFT

= += -= ETC. RIGHT TO LEFT

, (CHAPTER 3) LEFT TO RIGHT

Операции -> и . Используются для доступа к элементам струк-

тур; они будут описаны в главе 6 вместе с SIZEOF (размер

объекта). В главе 5 обсуждаются операции * (косвенная адре-

сация) и & (адрес).

Отметим, что уровень старшинства побитовых логических опера-

ций &, ^ и э ниже уровня операций == и !=. Это приводит к

тому, что осуществляющие побитовую проверку выражения, по-

добные

IF ((X & MASK) == 0) ...

Для получения правильных результатов должны заключаться в

круглые скобки.

Как уже отмечалось ранее, выражения, в которые входит

одна из ассоциативных и коммутативных операций (*, +, &, ^,

э), могут перегруппировываться, даже если они заключены в

круглые скобки. В большинстве случаев это не приводит к ка-

ким бы то ни было расхождениям; в ситуациях, где такие рас-

хождения все же возможны, для обеспечения нужного порядка

вычислений можно использовать явные промежуточные перемен-

ные.

В языке “C”, как и в большинстве языков, не фиксируется

порядок вычисления операндов в операторе. Например в опера-

торе вида

X = F() + G();

сначала может быть вычислено F, а потом G, и наоборот; поэ-

тому, если либо F, либо G изменяют внешнюю переменную, от

которой зависит другой операнд, то значение X может зависеть

от порядка вычислений. Для обеспечения нужной последователь-

ности промежуточные результаты можно опять запоминать во

временных переменных.

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

аргументов функции, так что оператор

PRINTF(“%D %D\N”,++N,POWER(2,N));

·
58 -

может давать (и действительно дает) на разных машинах разные

результаты в зависимости от того, увеличивается ли N до или

после обращения к функции POWER. Правильным решением, конеч-

но, является запись

++N;

PRINTF(“%D %D\N”,N,POWER(2,N));

Обращения к функциям, вложенные операции присваивания,

операции увеличения и уменьшения приводят к так называемым

“побочным эффектам” - некоторые переменные изменяются как

побочный результат вычисления выражений. В любом выражении,

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

очень тонкие зависимости от порядка, в котором определяются

входящие в него переменные. примером типичной неудачной си-

туации является оператор

A[I] = I++;

Возникает вопрос, старое или новое значение I служит в ка-

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

и в зависимости от своей интерпретации выдавать разные ре-

зультаты. Тот случай, когда происходят побочные эффекты

(присваивание фактическим переменным), - оставляется на ус-

мотрение компилятора, так как наилучший порядок сильно зави-

сит от архитектуры машины.

Из этих рассуждений вытекает такая мораль: написание

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

методом программирования на любом языке. Конечно, необходимо

знать, чего следует избегать, но если вы не в курсе, как не-

которые вещи реализованы на разных машинах, это неведение

может предохранить вас от неприятностей. (Отладочная прог-

рамма LINT укажет большинство мест, зависящих от порядка вы-

числений.


· 59 -

3. Поток управления

Управляющие операторы языка определяют порядок вычисле-

ний. В приведенных ранее примерах мы уже встречались с наи-

более употребительными управляющими конструкциями языка “C”;

здесь мы опишем остальные операторы управления и уточним

действия операторов, обсуждавшихся ранее.

3.1. Операторы и блоки

Такие выражения, как X=0, или I++, или PRINTF(...),

становятся операторами, если за ними следует точка с запя-

той, как, например,

X = 0;

I++;

PRINTF(...);

В языке “C” точка с запятой является признаком конца опера-

тора, а не разделителем операторов, как в языках типа алго-

ла.

Фигурные скобки /( и /) используются для объединения

описаний и операторов в составной оператор или блок, так что

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

Один явный пример такого типа дают фигурные скобки, в кото-

рые заключаются операторы, составляющие функцию, другой -

фигурные скобки вокруг группы операторов в конструкциях IF,

ELSE, WHILE и FOR.(на самом деле переменные могут быть опи-

саны внутри любого блока; мы поговорим об этом в главе 4).

Точка с запятой никогда не ставится после первой фигурной

скобки, которая завершает блок.

3.2. IF - ELSE

Оператор IF - ELSE используется при необходимости сде-

лать выбор. Формально синтаксис имеет вид

IF (выражение)

оператор-1

ELSE

оператор-2,

Где часть ELSE является необязательной. Сначала вычисля-

ется выражение; если оно “истинно” /т.е. значение выражения

отлично от нуля/, то выполняется оператор-1. Если оно ложно

/значение выражения равно нулю/, и если есть часть с ELSE,

то вместо оператора-1 выполняется оператор-2.

· 60 -

Так как IF просто проверяет численное значение выраже-

ния, то возможно некоторое сокращение записи. Самой очевид-

ной возможностью является запись

IF (выражение)

вместо

IF (выражение !=0)

иногда такая запись является ясной и естественной, но време-

нами она становится загадочной.

То, что часть ELSE в конструкции IF - ELSE является нео-

бязательной, приводит к двусмысленности в случае, когда ELSE

опускается во вложенной последовательности операторов IF.

Эта неоднозначность разрешается обычным образом - ELSE свя-

зывается с ближайшим предыдущим IF, не содержащим ELSE.

Например, в

IF ( N > 0 )

IF( A > B )

Z = A;

ELSE

Z = B;

конструкция ELSE относится к внутреннему IF, как мы и пока-

зали, сдвинув ELSE под соответствующий IF. Если это не то,

что вы хотите, то для получения нужного соответствия необхо-

димо использовать фигурные скобки:

IF (N > 0) {

IF (A > B)

Z = A;

}

ELSE

Z = B;

Tакая двусмысленность особенно пагубна в ситуациях типа

IF (N > 0)

FOR (I = 0; I < N; I++)

IF (S[I] > 0) {

PRINTF(“...”);

RETURN(I);

}

ELSE /* WRONG */

PRINTF(“ERROR - N IS ZERO\N”);

·
61 -

Запись ELSE под IF ясно показывает, чего вы хотите, но ком-

пилятор не получит соответствующего указания и свяжет ELSE с

внутренним IF. Ошибки такого рода очень трудно обнаруживают-

ся.

Между прочим, обратите внимание, что в

IF (A > B)

Z = A;

ELSE

Z = B;

после Z=A стоит точка с запятой. Дело в том, что согласно

грамматическим правилам за IF должен следовать оператор, а

выражение типа Z=A, являющееся оператором, всегда заканчива-

ется точкой с запятой.

3.3. ELSE - IF

Конструкция

IF (выражение)

оператор

ELSE IF (выражение)

оператор

ELSE IF (выражение)

оператор

ELSE

оператор

встречается настолько часто, что заслуживает отдельного

краткого рассмотрения. Такая последовательность операторов

IF является наиболее распространенным способом программиро-

вания выбора из нескольких возможных вариантов. выражения

просматриваются последовательно; если какое-то выражение

оказывается истинным,то выполняется относящийся к нему опе-

ратор, и этим вся цепочка заканчивается. Каждый оператор мо-

жет быть либо отдельным оператором, либо группой операторов

в фигурных скобках.

Последняя часть с ELSE имеет дело со случаем, когда ни

одно из проверяемых условий не выполняется. Иногда при этом

не надо предпринимать никаких явных действий; в этом случае

хвост

ELSE

оператор

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

чтобы засечь “невозможное” условие.

· 62 -

Для иллюстрации выбора из трех возможных вариантов при-

ведем программу функции, которая методом половинного деления

определяет, находится ли данное значение х в отсортированном

массиве V. Элементы массива V должны быть расположены в по-

рядке возрастания. Функция возвращает номер позиции (число

между 0 и N-1), в которой значение х находится в V, и -1,

если х не содержится в V.

BINARY(X, V, N) /* FIND X IN V[0]...V[N-1] */

INT X, V[], N;

{

INT LOW, HIGH, MID;

LOW = 0;

HIGH = N - 1;

WHILE (LOW <= HIGH) {

MID = (LOW + HIGH) / 2;

IF (X < V[MID])

HIGH = MID - 1;

ELSE IF (X > V[MID])

LOW = MID + 1;

ELSE /* FOUND MATCH */

RETURN(MID);

}

RETURN(-1);

}

Основной частью каждого шага алгоритма является провер-

ка, будет ли х меньше, больше или равен среднему элементу

V[MID]; использование конструкции ELSE - IF здесь вполне ес-

тественно.

3.4. Переключатель

Оператор SWITCH дает специальный способ выбора одного из

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

значения данного выражения с одной из заданных констант и

соответствующем ветвлении. В главе 1 мы привели программу

подсчета числа вхождений каждой цифры, символов пустых про-

межутков и всех остальных символов, использующую последова-

тельность IF...ELSE IF...ELSE. Вот та же самая программа с

переключателем.

· 63 -

MAIN() /* COUNT DIGITS,WHITE SPACE, OTHERS */

{

INT C, I, NWHITE, NOTHER, NDIGIT[10];

NWHITE = NOTHER = 0;

FOR (I = 0; I < 10; I++)

NDIGIT[I] = 0;

WHILE ((C = GETCHAR()) != EOF)

SWITCH © {

CASE '0':

CASE '1':

CASE '2':

CASE '3':

CASE '4':

CASE '5':

CASE '6':

CASE '7':

CASE '8':

CASE '9':

NDIGIT[C-'0']++;

BREAK;

CASE ' ':

CASE '\N':

CASE '\T':

NWHITE++;

BREAK;

DEFAULT :

NOTHER++;

BREAK;

}

PRINTF(“DIGITS =”);

FOR (I = 0; I < 10; I++)

PRINTF(“ %D”, NDIGIT[I]);

PRINTF(“\NWHITE SPACE = %D, OTHER = %D\N”,

NWHITE, NOTHER);

Переключатель вычисляет целое выражение в круглых скоб-

ках (в данной программе - значение символа с) и сравнивает

его значение со всеми случаями (CASE). Каждый случай должен

быть помечен либо целым, либо символьной константой, либо

константным выражением. Если значение константного выраже-

ния, стоящего после вариантного префикса CASE, совпадает со

значением целого выражения, то выполнение начинается с этого

случая. Если ни один из случаев не подходит, то выполняется

оператор после префикса DEFAULT. Префикс DEFAULT является

необязательным ,если его нет, и ни один из случаев не подхо-

дит, то вообще никакие действия не выполняются. Случаи и вы-

бор по умолчанию могут располагаться в любом порядке. Все

случаи должны быть различными.

· 64 -

Оператор BREAK приводит к немедленному выходу из перек-

лючателя. Поскольку случаи служат только в качестве меток,

то если вы не предпримите явных действий после выполнения

операторов, соответствующих одному случаю, вы провалитесь на

следующий случай. Операторы BREAK и RETURN являются самым

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

позже в этой главе, оператор BREAк можно использовать и для

немедленного выхода из операторов цикла WHILE, FOR и DO.

Проваливание сквозь случаи имеет как свои достоинства,

так и недостатки. К положительным качествам можно отнести

то, что оно позволяет связать несколько случаев с одним дей-

ствием, как было с пробелом, табуляцией и новой строкой в

нашем примере. Но в то же время оно обычно приводит к необ-

ходимости заканчивать каждый случай оператором BREAK, чтобы

избежать перехода к следующему случаю. Проваливание с одного

случая на другой обычно бывает неустойчивым, так как оно

склонно к расщеплению при модификации программы. За исключе-

нием, когда одному вычислению соответствуют несколько меток,

проваливание следует использовать умеренно.

Заведите привычку ставить оператор BREAK после последне-

го случая (в данном примере после DEFAULT), даже если это не

является логически необходимым. В один прекрасный день, ког-

да вы добавите в конец еще один случай, эта маленькая мера

предосторожности избавит вас от неприятностей.

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

Напишите программу для функции EXPAND(S, T), которая ко-

пирует строку S в т, заменяя при этом символы табуляции и

новой строки на видимые условные последовательности, как \N

и \т. используйте переключатель.

3.5. Циклы - WHILE и FOR

Мы уже сталкивались с операторами цикла WHILE и FOR. В

конструкции

WHILE (выражение)

оператор

вычисляется выражение. Если его значение отлично от нуля, то

выполняется оператор и выражение вычисляется снова. Этот

цикл продолжается до тех пор, пока значение выражения не

станет нулем, после чего выполнение программы продолжается с

места после оператора.

Оператор

FOR (выражение 1; выражение 2; выражение 3)

оператор

·
65 -

эквивалентен последовательности

выражение 1;

WHILE (выражение 2) {

оператор

выражение 3;

}

Грамматически все три компонента в FOR являются выражениями.

наиболее распространенным является случай, когда выражение 1

и выражение 3 являются присваиваниями или обращениями к фун-

кциям, а выражение 2 - условным выражением. любая из трех

частей может быть опущена, хотя точки с запятой при этом

должны оставаться. Если отсутствует выражение 1 или выраже-

ние 3, то оно просто выпадает из расширения. Если же отсутс-

твует проверка, выражение 2, то считается, как будто оно

всегда истинно, так что

FOR (;;) {

...

}

является бесконечным циклом, о котором предполагается, что

он будет прерван другими средствами (такими как BREAK или

RETURN).

Использовать ли WHILE или FOR - это, в основном дело

вкуса. Например в

WHILE ((C = GETCHAR())

== ' ' \!\! C == '\N' \!\! C == '\T')

; /* SKIP WHITE SPACE CHARACTERS */

нет ни инициализации, ни реинициализации, так что цикл WHILе

выглядит самым естественным.

Цикл FOR, очевидно, предпочтительнее там, где имеется

простая инициализация и реинициализация, поскольку при этом

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

вместе в начале цикла. Это наиболее очевидно в конструкции

FOR (I = 0; I < N; I++)

которая является идиомой языка “C” для обработки первых N

элементов массива, аналогичной оператору цикла DO в фортране

и PL/1. Аналогия, однако, не полная, так как границы цикла

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

сохраняет свое значение после выхода из цикла, какова бы ни

была причина этого выхода. Поскольку компонентами FOR могут

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

арифметическими прогрессиями. Тем не менее является плохим

стилем включать в FOR вычисления, которые не относятся к уп-

равлению циклом, лучше поместить их в управляемые циклом

операторы.

· 66 -

В качестве большего по размеру примера приведем другой

вариант функции ATOI, преобразующей строку в ее численный

эквивалент. Этот вариант является более общим; он допускает

присутствие в начале символов пустых промежутков и знака +

или -. (В главе 4 приведена функция ATOF, которая выполняет

то же самое преобразование для чисел с плавающей точкой).

Общая схема программы отражает форму поступающих данных:

· пропустить пустой промежуток, если он имеется

· извлечь знак, если он имеется

· извлечь целую часть и преобразовать ее

Каждый шаг выполняет свою часть работы и оставляет все в

подготовленном состоянии для следующей части. Весь процесс

заканчивается на первом символе, который не может быть

частью числа.

ATOI(S) /* CONVERT S TO INTEGER */

CHAR S[];

{

INT I, N, SIGN;

FOR(I=0;S[I]==' ' \!\!

S[I]=='\N' \!\! S[I]=='\T';I++)

; /* SKIP WHITE SPACE */

SIGN = 1;

IF(S[I] == '+' \!\! S[I] == '-') /* SIGN */

SIGN = (S[I++]=='+') ? 1 : - 1;

FOR( N = 0; S[I] >= '0' && S[I] <= '9'; I++)

N = 10 * N + S[I] - '0';

RETURN(SIGN * N);

}

Преимущества централизации управления циклом становятся

еще более очевидными, когда имеется несколько вложенных цик-

лов. Следующая функция сортирует массив целых чисел по мето-

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

том, что сначала сравниваются удаленные элементы, а не смеж-

ные, как в обычном методе сортировки. Это приводит к быстро-

му устранению большой части неупорядоченности и сокращает

последующую работу. Интервал между элементами постепенно

сокращается до единицы, когда сортировка фактически превра-

щается в метод перестановки соседних элементов.

· 67 -

SHELL(V, N) /* SORT V[0]...V[N-1]

INTO INCREASING ORDER */

INT V[], N;

{

INT GAP, I, J, TEMP;

FOR (GAP = N/2; GAP > 0; GAP /= 2)

FOR (I = GAP; I < N; I++)

FOR (J=I-GAP; J>=0 && V[J]>V[J+GAP]; J-=GAP) {

TEMP = V[J];

V[J] = V[J+GAP];

V[J+GAP] = TEMP;

}

}

Здесь имеются три вложенных цикла. Самый внешний цикл управ-

ляет интервалом между сравниваемыми элементами, уменьшая его

от N/2 вдвое при каждом проходе, пока он не станет равным

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

ленных на величину интервала; самый внутренний цикл перес-

тавляет любую неупорядоченную пару. Так как интервал в конце

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

дочиваются правильно. Отметим, что в силу общности конструк-

ции FOR внешний цикл укладывается в ту же самую форму, что и

остальные, хотя он и не является арифметической прогрессией.

Последней операцией языка “C” является запятая “,”, ко-

торая чаще всего используется в операторе FOR. Два выраже-

ния, разделенные запятой, вычисляются слева направо, причем

типом и значением результата являются тип и значение правого

операнда. Таким образом, в различные части оператора FOR

можно включить несколько выражений, например, для параллель-

ного изменения двух индексов. Это иллюстрируется функцией

REVERSE(S), которая располагает строку S в обратном порядке

на том же месте.

REVERSE(S) /* REVERSE STRING S IN PLACE */

CHAR S[];

{

INT C, I, J;

FOR(I = 0, J = STRLEN(S) - 1; I < J; I++, J--) {

C = S[I];

S[I] = S[J];

S[J] = C;

}

}

Запятые, которые разделяют аргументы функций, переменные в

описаниях и т.д., не имеют отношения к операции запятая и не

обеспечивают вычислений слева направо.

· 68 -

Упражнение 3-2.

Составьте программу для функции EXPAND(S1,S2), которая

расширяет сокращенные обозначения вида а-Z из строки S1 в

эквивалентный полный список авс...XYZ в S2. Допускаются сок-

ращения для строчных и прописных букв и цифр. Будьте готовы

иметь дело со случаями типа а-в-с, а-Z0-9 и -а-Z. (Полезное

соглашение состоит в том, что символ -, стоящий в начале или

конце, воспринимается буквально).

3.6. Цикл DO - WHILE

Как уже отмечалось в главе 1, циклы WHILE и FOR обладают

тем приятным свойством, что в них проверка окончания осущес-

твляется в начале, а не в конце цикла. Третий оператор цикла

языка “C”, DO-WHILE, проверяет условие окончания в конце,

после каждого прохода через тело цикла; тело цикла всегда

выполняется по крайней мере один раз. Синтаксис этого опера-

тора имеет вид:

DO

оператор

WHILE (выражение)

Сначала выполняется оператор, затем вычисляется выражение.

Если оно истинно, то оператор выполняется снова и т.д. Если

выражение становится ложным, цикл заканчивается.

Как и можно было ожидать, цикл DO-WHILE используется

значительно реже, чем WHILE и FOR, составляя примерно пять

процентов от всех циклов. Тем не менее, иногда он оказывает-

ся полезным, как, например, в следующей функции ITOA, кото-

рая преобразует число в символьную строку (обратная функции

ATOI). Эта задача оказывается несколько более сложной, чем

может показаться сначала. Дело в том, что простые методы вы-

деления цифр генерируют их в неправильном порядке. Мы пред-

почли получить строку в обратном порядке, а затем обратить

ее.

· 69 -

ITOA(N,S) /*CONVERT N TO CHARACTERS IN S */

CHAR S[];

INT N;

{

INT I, SIGN;

IF ((SIGN = N) < 0) /* RECORD SIGN */

N = -N; /* MAKE N POSITIVE */

I = 0;

DO { /* GENERATE DIGITS IN REVERSE ORDER */

S[I++] = N % 10 + '0';/* GET NEXT DIGIT */

} WHILE ((N /=10) > 0); /* DELETE IT */

IF (SIGN < 0)

S[I++] = '-'

S[I] = '\0';

REVERSE(S);

}

Цикл DO-WHILE здесь необходим, или по крайней мере удобен,

поскольку, каково бы ни было значение N, массив S должен со-

держать хотя бы один символ. Мы заключили в фигурные скобки

один оператор, составляющий тело DO-WHILе, хотя это и не

обязательно, для того, чтобы торопливый читатель не принял

часть WHILE за начало оператора цикла WHILE.

Упражнение 3-3.

--------------

При представлении чисел в двоичном дополнительном коде

наш вариант ITOA не справляется с наибольшим отрицательным

числом, т.е. Со значением N рAвным -2 в степени м-1, где м -

размер слова. объясните почему. Измените программу так, что-

бы она правильно печатала это значение на любой машине.

Упражнение 3-4.

--------------

Напишите аналогичную функцию ITOB(N,S), которая преобра-

зует целое без знака N в его двоичное символьное представле-

ние в S. Запрограммируйте функцию ITOH, которая преобразует

целое в шестнадцатеричное представление.

Упражнение 3-5.

---------------

Напишите вариант Iтоа, который имеет три, а не два аргу-

мента. Третий аргумент - минимальная ширина поля; преобразо-

ванное число должно, если это необходимо, дополняться слева

пробелами, так чтобы оно имело достаточную ширину.

· 70 -

3.7. Оператор BREAK

Иногда бывает удобным иметь возможность управлять выхо-

дом из цикла иначе, чем проверкой условия в начале или в

конце. Оператор BRеак позволяет выйти из операторов FOR,

WHILE и DO до окончания цикла точно так же, как и из перек-

лючателя. Оператор BRеак приводит к немедленному выходу из

самого внутреннего охватывающего его цикла (или переключате-

ля).

Следующая программа удаляет хвостовые пробелы и табуля-

ции из конца каждой строки файла ввода. Она использует опе-

ратор BRеак для выхода из цикла, когда найден крайний правый

отличный от пробела и табуляции символ.

#DEFINE MAXLINE 1000

MAIN() /* REMOVE TRAILING BLANKS AND TABS */

{

INT N;

CHAR LINE[MAXLINE];

WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) {

WHILE (--N >= 0)

IF (LINE[N] != ' ' && LINE[N] != '\T'

&& LINE[N] != '\N')

BREAK;

LINE[N+1] = '\0';

PRINTF(“%S\N”,LINE);

}

}

Функция GETLINE возвращает длину строки. Внутренний цикл

начинается с последнего символа LINE (напомним, что—N

уменьшает N до использования его значения) и движется в об-

ратном направлении в поиске первого символа , который отли-

чен от пробела, табуляции или новой строки. Цикл прерывает-

ся, когда либо найден такой символ, либо N становится отри-

цательным (т.е., когда просмотрена вся строка). Советуем вам

убедиться, что такое поведение правильно и в том случае,

когда строка состоит только из символов пустых промежутков.

В качестве альтернативы к BRеак можно ввести проверку в

сам цикл:

WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) {

WHILE (--N >= 0

&& (LINE[N] == ' ' \!\! LINE[N] == '\T'

\!\! LINE[N] == '\N'))

;

...

}

· 71 -

Это уступает предыдущему варианту, так как проверка стано-

вится труднее для понимания. Проверок, которые требуют пе-

реплетения &&, \!\!, ! И круглых скобок, по возможности сле-

дует избегать.

3.8. Оператор CONTINUE

Оператор CONTINUE родственен оператору BRеак, но исполь-

зуется реже; он приводит к началу следующей итерации охваты-

вающего цикла (FOR, WHILE, DO ). В циклах WHILE и DO это оз-

начает непосредственный переход к выполнению проверочной

части; в цикле FOR управление передается на шаг реинициали-

зации. (Оператор CONTINUE применяется только в циклах, но не

в переключателях. Оператор CONTINUE внутри переключателя

внутри цикла вызывает выполнение следующей итерации цикла).

В качестве примера приведем фрагмент, который обрабаты-

вает только положительные элементы массива а; отрицательные

значения пропускаются.

FOR (I = 0; I < N; I++) {

IF (A[I] < 0) /* SKIP NEGATIVE ELEMENTS */

CONTINUE;

... /* DO POSITIVE ELEMENTS */

}

Оператор CONTINUE часто используется, когда последующая

часть цикла оказывается слишком сложной, так что рассмотре-

ние условия, обратного проверяемому, приводит к слишком глу-

бокому уровню вложенности программы.

Упражнение 3-6.

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

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

строк выводится только одна. (Это простой вариант утилиты

UNIQ систем UNIX).

3.9. Оператор GOTO и метки

В языке “C” предусмотрен и оператор GOTO, которым беско-

нечно злоупотребляют, и метки для ветвления. С формальной

точки зрения оператор GOTO никогда не является необходимым,

и на практике почти всегда можно обойтись без него. Мы не

использовали GOTO в этой книге.

Тем не менее, мы укажем несколько ситуаций, где оператор

GOTO может найти свое место. Наиболее характерным является

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

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

из двух циклов. Здесь нельзя непосредственно использовать

оператор BRеак, так как он прерывает только самый внутренний

цикл. Поэтому:

· 72 -

FOR ( ... )

FOR ( ... ) {

...

IF (DISASTER)

GOTO ERROR;

}

...

ERROR:

CLEAN UP THE MESS

Если программа обработки ошибок нетривиальна и ошибки могут

возникать в нескольких местах, то такая организация оказыва-

ется удобной. Метка имеет такую же форму, что и имя перемен-

ной, и за ней всегда следует двоеточие. Метка может быть

приписана к любому оператору той же функции, в которой нахо-

дится оператор GOTO.

В качестве другого примера рассмотрим задачу нахождения

первого отрицательного элемента в двумерном массиве. (Много-

мерные массивы рассматриваются в главе 5). Вот одна из воз-

можностей:

FOR (I = 0; I < N; I++)

FOR (J = 0; J < M; J++)

IF (V[I][J] < 0)

GOTO FOUND;

/* DIDN'T FIND */

...

FOUND:

/* FOUND ONE AT POSITION I, J */

...

Программа, использующая оператор GOTO, всегда может быть

написана без него, хотя, возможно, за счет повторения неко-

торых проверок и введения дополнительных переменных. Напри-

мер, программа поиска в массиве примет вид:

FOUND = 0;

FOR (I = 0; I < N && !FOUND; I++)

FOR (J = 0; J < M && !FOUND; J++)

FOUND = V[I][J] < 0;

IF (FOUND)

/* IT WAS AT I-1, J-1 */

...

ELSE

/* NOT FOUND */

...

Хотя мы не являемся в этом вопросе догматиками, нам все

же кажется, что если и нужно использовать оператор GOTO, то

весьма умеренно.

· 73 -

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

Функции разбивают большие вычислительные задачи на ма-

ленькие подзадачи и позволяют использовать в работе то, что

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

та. Соответствующие функции часто могут скрывать в себе де-

тали проводимых в разных частях программы операций, знать

которые нет необходимости, проясняя тем самым всю программу,

как целое, и облегчая мучения при внесении изменений.

Язык “C” разрабатывался со стремлением сделать функции

эффективными и удобными для использования; “C”-программы

обычно состоят из большого числа маленьких функций, а не из

нескольких больших. Программа может размещаться в одном или

нескольких исходных файлах любым удобным образом; исходные

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

наряду со скомпилированными ранее функциями из библиотек. Мы

здесь не будем вдаваться в детали этого процесса, поскольку

они зависят от используемой системы.

Большинство программистов хорошо знакомы с “библиотечны-

ми” функциями для ввода и вывода /GETCHAR , PUTCHAR/ и для

численных расчетов /SIN, COS, SQRT/. В этой главе мы сообщим

больше о написании новых функций.

4.1. Основные сведения.

Для начала давайте разработаем и составим программу пе-

чати каждой строки ввода, которая содержит определенную ком-

бинацию символов. /Это - специальный случай утилиты GREP

системы “UNIX”/. Например, при поиске комбинации “THE” в на-

боре строк

NOW IS THE TIME

FOR ALL GOOD

MEN TO COME TO THE AID

OF THEIR PARTY

в качестве выхода получим

NOW IS THE TIME

MEN TO COME TO THE AID

OF THEIR PARTY

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

части:

WHILE (имеется еще строка)

IF (строка содержит нужную комбинацию)

вывод этой строки


· 74 -

Конечно, возможно запрограммировать все действия в виде

одной основной процедуры, но лучше использовать естественную

структуру задачи и представить каждую часть в виде отдельной

функции. С тремя маленькими кусками легче иметь дело, чем с

одним большим, потому что отдельные не относящиеся к сущест-

ву дела детали можно включить в функции и уменьшить возмож-

ность нежелательных взаимодействий. Кроме того, эти куски

могут оказаться полезными сами по себе.

“Пока имеется еще строка” - это GETLINE, функция, кото-

рую мы запрограммировали в главе 1, а “вывод этой строки” -

это функция PRINTF, которую уже кто-то подготовил для нас.

Это значит, что нам осталось только написать процедуру для

определения, содержит ли строка данную комбинацию символов

или нет. Мы можем решить эту проблему, позаимствовав разра-

ботку из PL/1: функция INDEX(S,т) возвращает позицию, или

индекс, строки S, где начинается строка T, и -1, если S не

содержит т . В качестве начальной позиции мы используем 0, а

не 1, потому что в языке “C” массивы начинаются с позиции

нуль. Когда нам в дальнейшем понадобится проверять на совпа-

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

ко функцию INDEX; остальная часть программы останется той же

самой.

После того, как мы потратили столько усилий на разработ-

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

ний. ниже приводится целиком вся программа, так что вы може-

те видеть, как соединяются вместе отдельные части. Комбина-

ция символов, по которой производится поиск, выступает пока

в качестве символьной строки в аргументе функции INDEX, что

не является самым общим механизмом. Мы скоро вернемся к об-

суждению вопроса об инициализации символьных массивов и в

главе 5 покажем, как сделать комбинацию символов параметром,

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

Программа также содержит новый вариант функции GETLINE; вам

может оказаться полезным сравнить его с вариантом из главы

1.

#DEFINE MAXLINE 1000

MAIN() /* FIND ALL LINES MATCHING A PATTERN */

{

CHAR LINE[MAXLINE];

WHILE (GETLINE(LINE, MAXLINE) > 0)

IF (INDEX(LINE, “THE”) >= 0)

PRINTF(“%S”, LINE);

}

· 75 -

GETLINE(S, LIM) /* GET LINE INTO S, RETURN LENGTH *

CHAR S[];

INT LIM;

{

INT C, I;

I = 0;

WHILE(--LIM>0 && (C=GETCHAR()) != EOF && C != '\N')

S[I++] = C;

IF (C == '\N')

S[I++] = C;

S[I] = '\0';

RETURN(I);

}

INDEX(S,T) /* RETURN INDEX OF T IN S,-1 IF NONE */

CHAR S[], T[];

{

INT I, J, K;

FOR (I = 0; S[I] != '\0'; I++) {

FOR(J=I, K=0; T[K] !='\0' && S[J] == T[K]; J++; K++)

;

IF (T[K] == '\0')

RETURN(I);

}

RETURN(-1);

}

Каждая функция имеет вид имя (список аргументов, если они

имеются) описания аргументов, если они имеются

{

описания и операторы , если они имеются

}

Как и указывается, некоторые части могут отсутство-

вать; минимальной функцией является

DUMMY () { }

которая не совершает никаких действий.

/Такая ничего не делающая функция иногда оказывается

удобной для сохранения места для дальнейшего развития прог-

раммы/. если функция возвращает что-либо отличное от целого

значения, то перед ее именем может стоять указатель типа;

этот вопрос обсуждается в следующем разделе.

· 76 -

Программой является просто набор определений отдельных

функций. Связь между функциями осуществляется через аргумен-

ты и возвращаемые функциями значения /в этом случае/; ее

можно также осуществлять через внешние переменные. Функции

могут располагаться в исходном файле в любом порядке, а сама

исходная программа может размещаться на нескольких файлах,

но так, чтобы ни одна функция не расщеплялась.

Оператор RETURN служит механизмом для возвращения зна-

чения из вызванной функции в функцию, которая к ней обрати-

лась. За RETURN может следовать любое выражение:

RETURN (выражение)

Вызывающая функция может игнорировать возвращаемое

значение, если она этого пожелает. Более того, после RETURN

может не быть вообще никакого выражения; в этом случае в вы-

зывающую программу не передается никакого значения. Управле-

ние также возвращется в вызывающую программу без передачи

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

“проваливаемся” на конец функции, достигая закрывающейся

правой фигурной скобки. EСли функция возвращает значение из

одного места и не возвращает никакого значения из другого

места, это не является незаконным, но может быть признаком

каких-то неприятностей. В любом случае “значением” функции,

которая не возвращает значения, несомненно будет мусор. От-

ладочная программа LINT проверяет такие ошибки.

Механика компиляции и загрузки “C”-программ, располо-

женных в нескольких исходных файлах, меняется от системы к

системе. В системе “UNIX”, например, эту работу выполняет

команда 'CC', упомянутая в главе 1. Предположим, что три

функции находятся в трех различных файлах с именами MAIN.с,

GETLINE.C и INDEX.с . Тогда команда

CC MAIN.C GETLINE.C INDEX.C

компилирует эти три файла, помещает полученный настраиваемый объектный код в файлы MAIN.O, GETLINE.O и INDEX.O и загружа-ет их всех в выполняемый файл, называемый A.OUT .

Если имеется какая-то ошибка, скажем в MAIN.C, то этот

файл можно перекомпилировать отдельно и загрузить вместе с

предыдущими объектными файлами по команде

CC MAIN.C GETLIN.O INDEX.O

Команда 'CC' использует соглашение о наименовании с “.с” и “.о” для того, чтобы отличить исходные файлы от объектных.

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

Составьте программу для функции RINDEX(S,T), которая

возвращает позицию самого правого вхождения т в S и -1, если

S не содержит T.

·
77 -

4.2. Функции, возвращающие нецелые значения.

До сих пор ни одна из наших программ не содержала како-

го-либо описания типа функции. Дело в том, что по умолчанию

функция неявно описывается своим появлением в выражении или

операторе, как, например, в

WHILE (GETLINE(LINE, MAXLINE) > 0)

Если некоторое имя, которое не было описано ранее, появ-

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

оно по контексту считается именем некоторой функции. Кроме

того, по умолчанию предполагается, что эта функция возвраща-

ет значение типа INT. Так как в выражениях CHAR преобразует-

ся в INT, то нет необходимости описывать функции, возвращаю-

щие CHAR. Эти предположения покрывают большинство случаев,

включая все приведенные до сих пор примеры.

Но что происходит, если функция должна возвратить значе-

ние какого-то другого типа ? Многие численные функции, такие

как SQRT, SIN и COS возвращают DOUBLE; другие специальные

функции возвращают значения других типов. Чтобы показать,

как поступать в этом случае, давайте напишем и используем

функцию ATоF(S), которая преобразует строку S в эквивалент-

ное ей плавающее число двойной точности. Функция ATоF явля-

ется расширением атоI, варианты которой мы написали в главах

2 и 3; она обрабатывает необязательно знак и десятичную точ-

ку, а также целую и дробную часть, каждая из которых может

как присутствовать, так и отсутствовать./эта процедура пре-

образования ввода не очень высокого качества; иначе она бы

заняла больше места, чем нам хотелось бы/.

Во-первых, сама ATоF должна описывать тип возвращаемого

ею значения, поскольку он отличен от INT. Так как в выраже-

ниях тип FLOAT преобразуется в DOUBLE, то нет никакого смыс-

ла в том, чтобы ATOF возвращала FLOAT; мы можем с равным ус-

пехом воспользоваться дополнительной точностью, так что мы

полагаем, что возвращаемое значение типа DOUBLE. Имя типа

должно стоять перед именем функции, как показывается ниже:

DOUBLE ATOF(S) /* CONVERT STRING S TO DOUBLE */

CHAR S[];

{

DOUBLE VAL, POWER;

INT I, SIGN;

·
78 -

FOR(I=0; S[I]==' ' \!\! S[I]=='\N' \!\! S[I]=='\T'; I++)

; /* SKIP WHITE SPACE */

SIGN = 1;

IF (S[I] == '+' \!\! S[I] == '-') /* SIGN */

SIGN = (S[I++] == '+') ? 1 : -1;

FOR (VAL = 0; S[I] >= '0' && S[I] <= '9'; I++)

VAL = 10 * VAL + S[I] - '0';

IF (S[I] == '.')

I++;

FOR (POWER = 1; S[I] >= '0' && S[I] <= '9'; I++) {

VAL = 10 * VAL + S[I] - '0';

POWER *= 10;

}

RETURN(SIGN * VAL / POWER);

}

Вторым, но столь же важным, является то, что вызывающая

функция должна объявить о том, что ATOF возвращает значение,

отличное от INT типа. Такое объявление демонстрируется на

примере следующего примитивного настольного калькулятора

/едва пригодного для подведения баланса в чековой книжке/,

который считывает по одному числу на строку, причем это чис-

ло может иметь знак, и складывает все числа, печатая сумму

после каждого ввода.

#DEFINE MAXLINE 100

MAIN() /* RUDIMENTARY DESK CALKULATOR */

{

DOUBLE SUM, ATOF();

CHAR LINE[MAXLINE];

SUM = 0;

WHILE (GETLINE(LINE, MAXLINE) > 0)

PRINTF(“\T%.2F\N”,SUM+=ATOF(LINE));

Оисание

DOUBLE SUM, ATOF();

говорит, что SUM является переменной типа DOUBLE , и что

ATOF является функцией, возвращающей значение типа DOUBLE .

Эта мнемоника означает, что значениями как SUM, так и

ATOF(...) являются плавающие числа двойной точности.

· 79 -

Если функция ATOF не будет описана явно в обоих местах,

то в “C” предполагается, что она возвращает целое значение,

и вы получите бессмысленный ответ. Если сама ATOF и обраще-

ние к ней в MAIN имеют несовместимые типы и находятся в од-

ном и том же файле, то это будет обнаружено компилятором. Но

если ATOF была скомпилирована отдельно /что более вероятно/,

то это несоответствие не будет зафиксировано, так что ATOF

будет возвращать значения типа DOUBLE, с которым MAIN будет

обращаться, как с INT , что приведет к бессмысленным резуль-

татам. /Программа LINT вылавливает эту ошибку/.

Имея ATOF, мы, в принципе, могли бы с ее помощью напи-

сать ATOI (преобразование строки в INT):

ATOI(S) /* CONVERT STRING S TO INTEGER */

CHAR S[];

{

DOUBLE ATOF();

RETURN(ATOF(S));

}

Обратите внимание на структуру описаний и оператор RETURN.

Значение выражения в

RETURN (выражение)

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

возвращения. Поэтому при появлении в операторе RETURN значе-

ние функции атоF, имеющее тип DOUBLE, автоматически преобра-

зуется в INT, поскольку функция ATOI возвращает INT. (Как

обсуждалось в главе 2, преобразование значения с плавающей

точкой к типу INT осуществляется посредством отбрасывания

дробной части).

Упражнение 4-2.

Расширьте ATOF таким образом, чтобы она могла работать с

числами вида

123.45е-6

где за числом с плавающей точкой может следовать 'E' и пока-

затель экспоненты, возможно со знаком.

4.3. Еще об аргументах функций.

В главе 1 мы уже обсуждали тот факт , что аргументы фун-

кций передаются по значению, т.е. вызванная функция получает

свою временную копию каждого аргумента, а не его адрес. это

означает, что вызванная функция не может воздействовать на

исходный аргумент в вызывающей функции. Внутри функции каж-

дый аргумент по существу является локальной переменной, ко-

торая инициализируется тем значением, с которым к этой функ-

ции обратились.

· 80 -

Если в качестве аргумента функции выступает имя массива,

то передается адрес начала этого массива; сами элементы не

копируются. Функция может изменять элементы массива, исполь-

зуя индексацию и адрес начала. Таким образом, массив переда-

ется по ссылке. В главе 5 мы обсудим, как использование ука-

зателей позволяет функциям воздействовать на отличные от

массивов переменные в вызывающих функциях.

Между прочим, несуществует полностью удовлетворительного

способа написания переносимой функции с переменным числом

аргументов. Дело в том, что нет переносимого способа, с по-

мощью которого вызванная функция могла бы определить, сколь-

ко аргументов было фактически передано ей в данном обраще-

нии. Таким образом, вы, например, не можете написать дейст-

вительно переносимую функцию, которая будет вычислять макси-

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

функции MAX в фортране и PL/1.

Обычно со случаем переменного числа аргументов безопасно

иметь дело, если вызванная функция не использует аргументов,

которые ей на самом деле не были переданы, и если типы сог-

ласуются. Самая распространенная в языке “C” функция с пере-

менным числом - PRINTF . Она получает из первого аргумента

информацию, позволяющую определить количество остальных ар-

гументов и их типы. Функция PRINTF работает совершенно неп-

равильно, если вызывающая функция передает ей недостаточное

количество аргументов, или если их типы не согласуются с ти-

пами, указанными в первом аргументе. Эта функция не является

переносимой и должна модифицироваться при использовании в

различных условиях.

Если же типы аргументов известны, то конец списка аргу-

ментов можно отметить, используя какое-то соглашение; напри-

мер, считая, что некоторое специальное значение аргумента

(часто нуль) является признаком конца аргументов.

4.4. Внешние переменные.

Программа на языке “C” состоит из набора внешних объек-

тов, которые являются либо переменными, либо функциями. Тер-

мин “внешний” используется главным образом в противопостав-

ление термину “внутренний”, которым описываются аргументы и

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

Внешние переменные определены вне какой-либо функции и, та-

ким образом, потенциально доступны для многих функций. Сами

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

“C” не разрешают определять одни функции внутри других. По

умолчанию внешние переменные являются также и “глобальными”,

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

то же имя (даже из функций, скомпилированных независимо),

будут ссылками на одно и то же. В этом смысле внешние пере-

менные аналогичны переменным COмMON в фортране и EXTERNAL в

PL/1. Позднее мы покажем, как определить внешние переменные

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

но, а только в пределах одного исходного файла.

· 81 -

В силу своей глобальной доступности внешние переменные

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

значений, возможность для обмена данными между функциями.

Если имя внешней переменной каким-либо образом описано, то

любая функция имеет доступ к этой переменной, ссылаясь к ней

по этому имени.

В случаях, когда связь между функциями осуществляется с

помощью большого числа переменных, внешние переменные оказы-

ваются более удобными и эффективными, чем использование

длинных списков аргументов. Как, однако, отмечалось в главе

1, это соображение следует использовать с определенной осто-

рожностью, так как оно может плохо отразиться на структуре

программ и приводить к программам с большим числом связей по

данным между функциями.

Вторая причина использования внешних переменных связана

с инициализацией. В частности, внешние массивы могут быть

инициализированы а автоматические нет. Мы рассмотрим вопрос

об инициализации в конце этой главы.

Третья причина использования внешних переменных обуслов-

лена их областью действия и временем существования. Автома-

тические переменные являются внутренними по отношению к фун-

кциям; они возникают при входе в функцию и исчезают при вы-

ходе из нее. Внешние переменные, напротив, существуют посто-

янно. Они не появляютя и не исчезают, так что могут сохра-

нять свои значения в период от одного обращения к функции до

другого. В силу этого, если две функции используют некоторые

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

то часто наиболее удобным оказывается хранить эти общие дан-

ные в виде внешних переменных, а не передавать их в функцию

и обратно с помощью аргументов.

Давайте продолжим обсуждение этого вопроса на большом

примере. Задача будет состоять в написании другой программы

для калькулятора, лучшей,чем предыдущая. Здесь допускаются

операции +,-,*,/ и знак = (для выдачи ответа).вместо инфикс-

ного представления калькулятор будет использовать обратную

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

обратной польской нотации знак следует за операндами; инфик-

сное выражение типа

(1-2)*(4+5)=

записывается в виде

12-45+*=

круглые скобки при этом не нужны

· 82 -

Реализация оказывается весьма простой.каждый операнд по-

мещается в стек; когда поступает знак операции,нужное число

операндов (два для бинарных операций) вынимается,к ним при-

меняется операция и результат направляется обратно в

стек.так в приведенном выше примере 1 и 2 помещаются в стек

и затем заменяются их разностью, -1.после этого 4 и 5 вво-

дятся в стек и затем заменяются своей суммой,9.далее числа

· 1 и 9 заменяются в стеке на их произведение,равное -9.опе-рация = печатает верхний элемент стека, не удаляя его (так что промежуточные вычисления могут быть проверены).

Сами операции помещения чисел в стек и их извлечения

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

обнаружения ошибок и восстановления,они оказываются доста-

точно длинными. Поэтому лучше оформить их в виде отдельных