Язык С

         

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


Аддитивные операции + и - группируются слева направо. выполняются обычные арифметические преобразования. Для каж- дой операции имеются некоторые дополнительные возможности, связанные с типами операндов.

Аддитивное-выражение: выражение + выражение выражение - выражение

Результатом операции + является сумма операндов. Можно скла- дывать указатель на объект в массиве и значение любого цело- численного типа. во всех случаях последнее преобразуется в адресное смещение посредством умножения его на длину объек- та, на который указывает этот указатель. Результатом являет- ся указатель того же самого типа, что и исходный указатель, который указывает на другой объект в том же массиве, смещен- ный соответствующим образом относительно первоначального объекта. Таким образом, если P является указателем объекта в массиве, то выражение P+1 является указателем на следующий объект в этом массиве. Никакие другие комбинации типов для указателей не разре- шаются. Операция + ассоциативна, и выражение с несколькими сло- жениями на том же самом уровне могут быть переупорядочены компилятором. Результатом операции - является разность операндов. Вы- полняются обычные арифметические преобразования. Кроме того, из указателя может быть вычтено значение любого целочислен- ного типа, причем, проводятся те же самые преобразования, что и при операции сложения. Если вычитаются два указателя на объекты одинакового ти- па, то результат преобразуется (делением на длину объекта) к типу INT, представляя собой число объектов, разделяющих ука- зываемые объекты. Если эти указатели не на объекты из одного и того же массива, то такое преобразование, вообще говоря, даст неожиданные результаты, потому что даже указатели на объекты одинакового типа не обязаны отличаться на величину, кратную длине объекта.



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


Если P является указателем, то каков бы ни был сорт объекта, на который он указывает, операция P++ увеличивает P так, что он указывает на следующий элемент набора этих объектов, а операция P +=I увеличивает P так, чтобы он ука- зывал на элемент, отстоящий на I элементов от текущего эле- мента.эти и аналогичные конструкции представляют собой самые простые и самые распространенные формы арифметики указателей или адресной арифметики. Язык "C" последователен и постоянен в своем подходе к адресной арифметике; объединение в одно целое указателей, массивов и адресной арифметики является одной из наиболее сильных сторон языка. Давайте проиллюстрируем некоторые из соответствующих возможностей языка на примере элементарной (но полезной, несмотря на свою простоту) программы распреде- ления памяти. Имеются две функции: функция ALLOC(N) возвра- щает в качестве своего значения указатель P, который указы- вает на первую из N последовательных символьных позиций, ко- торые могут быть использованы вызывающей функцию ALLOC прог- раммой для хранения символов; функция FREE(P) освобождает приобретенную таким образом память, так что ее в дальнейшем можно снова использовать. программа является "элементарной", потому что обращения к FREE должны производиться в порядке, обратном тому, в котором производились обращения к ALLOC. Таким образом, управляемая функциями ALLOC и FREE память яв- ляется стеком или списком, в котором последний вводимый эле- мент извлекается первым. Стандартная библиотека языка "C" содержит аналогичные функции, не имеющие таких ограничений, и, кроме того, в главе 8 мы приведем улучшенные варианты. Между тем, однако, для многих приложений нужна только триви- альная функция ALLOC для распределения небольших участков памяти неизвестных заранее размеров в непредсказуемые момен- ты времени. Простейшая реализация состоит в том, чтобы функция раз- давала отрезки большого символьного массива, которому мы присвоили имя ALLOCBUF. Этот массив является собственностью функций ALLOC и FREE. Так как они работают с указателями, а не с индексами массива, никакой другой функции не нужно знать имя этого массива. Он может быть описан как внешний статический, т.е. Он будет локальным по отношению к исходно- му файлу, содержащему ALLOC и FREE, и невидимым за его пре- делами. При практической реализации этот массив может даже не иметь имени; вместо этого он может быть получен в резуль- тате запроса к операционной системе на указатель некоторого неименованного блока памяти. Другой необходимой информацией является то, какая часть массива ALLOCBUF уже использована. Мы пользуемся указателем первого свободного элемента, названным ALLOCP. Когда к функ- ции ALLOC обращаются за выделением N символов, то она прове- ряет, достаточно ли осталось для этого места в ALLOCBUF. Ес- ли достаточно, то ALLOC возвращает текущее значение ALLOCP (т.е. Начало свободного блока), затем увеличивает его на N, с тем чтобы он указывал на следующую свободную область. Фун- кция FREE(P) просто полагает ALLOCP равным P при условии, что P указывает на позицию внутри ALLOCBUF.


DEFINE NULL 0 /* POINTER VALUE FOR ERROR REPORT */ DEFINE ALLOCSIZE 1000 /* SIZE OF AVAILABLE SPACE */

TATIC CHAR ALLOCBUF[ALLOCSIZE];/* STORAGE FOR ALLOC */ TATIC CHAR *ALLOCP = ALLOCBUF; /* NEXT FREE POSITION */

HAR *ALLOC(N) /* RETURN POINTER TO N CHARACTERS */ INT N; ( IF (ALLOCP + N <= ALLOCBUF + ALLOCSIZE) { ALLOCP += N; RETURN(ALLOCP - N); /* OLD P */ } ELSE /* NOT ENOUGH ROOM */ RETURN(NULL); )

REE(P) /* FREE STORAGE POINTED BY P */ HAR *P; ( IF (P >= ALLOCBUF && P < ALLOCBUF + ALLOCSIZE) ALLOCP = P; )

Дадим некоторые пояснения. Вообще говоря, указатель мо- жет быть инициализирован точно так же, как и любая другая переменная, хотя обычно единственными осмысленными значения- ми являются NULL (это обсуждается ниже) или выражение, вклю- чающее адреса ранее определенных данных соответствующего ти- па. Описание

STATIC CHAR *ALLOCP = ALLOCBUF;

определяет ALLOCP как указатель на символы и инициализирует его так, чтобы он указывал на ALLOCBUF, т.е. На первую сво- бодную позицию при начале работы программы. Так как имя мас- сива является адресом его нулевого элемента, то это можно было бы записать в виде

STATIC CHAR *ALLOCP = &ALLOCBUF[0];

используйте ту запись, которая вам кажется более естествен- ной. С помощью проверки

IF (ALLOCP + N <= ALLOCBUF + ALLOCSIZE)

выясняется, осталось ли достаточно места, чтобы удовлетво- рить запрос на N символов. Если достаточно, то новое значе- ние ALLOCP не будет указывать дальше, чем на последнюю пози- цию ALLOCBUF. Если запрос может быть удовлетворен, то ALLOC возвращает обычный указатель (обратите внимание на описание самой функции). Если же нет, то ALLOC должна вернуть некото- рый признак, говорящий о том, что больше места не осталось. В языке "C" гарантируется, что ни один правильный указатель данных не может иметь значение нуль, так что возвращение ну- ля может служить в качестве сигнала о ненормальном событии, в данном случае об отсутствии места. Мы, однако, вместо нуля пишем NULL, с тем чтобы более ясно показать, что это специ- альное значение указателя. Вообще говоря, целые не могут ос- мысленно присваиваться указателям, а нуль - это особый слу- чай. Проверки вида





IF (ALLOCP + N <= ALLOCBUF + ALOOCSIZE) и IF (P >= ALLOCBUF && P < ALLOCBUF + ALLOCSIZE)

демонстрируют несколько важных аспектов арифметики указате- лей. Во-первых , при определенных условиях указатели можно сравнивать. Если P и Q указывают на элементы одного и того же массива, то такие отношения, как <, >= и т.д., работают надлежащим образом. Например,

P < Q

истинно, если P указывает на более ранний элемент массива, чем Q. Отношения == и != тоже работают. Любой указатель мож- но осмысленным образом сравнить на равенство или неравенство с NULL. Но ни за что нельзя ручаться, если вы используете сравнения при работе с указателями, указывающими на разные массивы. Если вам повезет, то на всех машинах вы получите очевидную бессмыслицу. Если же нет, то ваша программа будет правильно работать на одной машине и давать непостижимые ре- зультаты на другой. Во-вторых, как мы уже видели, указатель и целое можно складывать и вычитать. Конструкция

P + N

подразумевает N-ый объект за тем, на который P указывает в настоящий момент. Это справедливо независимо от того, на ка- кой вид объектов P должен указывать; компилятор сам масшта- бирует N в соответствии с определяемым из описания P разме- ром объектов, указываемых с помощью P. например, на PDP-11 масштабирующий множитель равен 1 для CHAR, 2 для INT и SHORT, 4 для LONG и FLOAT и 8 для DOUBLE. Вычитание указателей тоже возможно: если P и Q указывают на элементы одного и того же массива, то P-Q - количество элементов между P и Q. Этот факт можно использовать для на- писания еще одного варианта функции

STRLEN: STRLEN(S) /* RETURN LENGTH OF STRING S */ CHAR *S; { CHAR *P = S;

WHILE (*P != '\0') P++; RETURN(P-S); } При описании указатель P в этой функции инициализирован посредством строки S, в результате чего он указывает на пер- вый символ строки. В цикле WHILE по очереди проверяется каж- дый символ до тех пор, пока не появится символ конца строки \0. Так как значение \0 равно нулю, а WHILE только выясняет, имеет ли выражение в нем значение 0, то в данном случае яв- ную проверку можно опустить. Такие циклы часто записывают в виде



WHILE (*P) P++;

Так как P указывает на символы, то оператор P++ передви- гает P каждый раз так, чтобы он указывал на следующий сим- вол. В результате P-S дает число просмотренных символов,

т.е. Длину строки. Арифметика указателей последовательна: если бы мы имели дело с переменными типа FLOAT, которые за- нимают больше памяти, чем переменные типа CHAR, и если бы P был указателем на FLOAT, то оператор P++ передвинул бы P на следующее FLOAT. таким образом, мы могли бы написать другой вариант функции ALLOC, распределяющей память для FLOAT, вместо CHAR, просто заменив всюду в ALLOC и FREE описатель CHAR на FLOAT. Все действия с указателями автоматически учи- тывают размер объектов, на которые они указывают, так что больше ничего менять не надо. За исключением упомянутых выше операций (сложение и вы- читание указателя и целого, вычитание и сравнение двух ука- зателей), вся остальная арифметика указателей является неза- конной. Запрещено складывать два указателя, умножать, де- лить, сдвигать или маскировать их, а также прибавлять к ним переменные типа FLOAT или DOUBLE.




Анахронизмы


Так как язык "C" является развивающимся языком, в старых программах можно встретить некоторые устаревшие конструкции. Хотя большинство версий компилятора поддерживает такие анах- ронизмы, они в конце концов исчезнут, оставив за собой толь- ко проблемы переносимости. В ранних версиях "C" для проблем присваивания использо- валась форма =ON, а не ON=, приводя к двусмысленностям, ти- пичным примером которых является

X = -1

где X фактически уменьшается, поскольку операции = и - при- мыкают друг к другу, но что вполне могло рассматриваться и как присваивание -1 к X. Синтаксис инициализаторов изменился: раньше знак равенс- тва, с которого начинается инициализатор, отсутствовал, так что вместо

INT X = 1;

использовалось

INT X 1;

изменение было внесено из-за инициализации

INT F (1+2)

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



Аннотация


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

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



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


Один аспект в "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. Когда в качестве аргумента выступает имя массива, то фактическим значением, передаваемым функции, является адрес начала массива. /Здесь нет никакого копирования элементов массива/. С помощью индексации и адреса начала функция может найти и изменить любой элемент массива. Это - тема следующе- го раздела.



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


Бинарными арифметическими операциями являются +, -, *, / и операция деления по модулю %. Имеется унарная операция -, но не существует унарной операции +.

При делении целых дробная часть отбрасывается. Выражение

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. Это редко приводит к како- му-либо расхождению, но если необходимо обеспечить строго определенный порядок, то нужно использовать явные промежу- точные переменные. Действия, предпринимаемые при переполнении и антипере- полнении (т.е. При получении слишком маленького по абсолют- ной величине числа), зависят от используемой машины.



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


Подавляющее большинство операций вызывает преобразование и определяет типы результата аналогичным образом. Приводимая ниже схема в дальнейшем будет называться "обычными арифмети- ческими преобразованиями". Сначала любые операнды типа CHAR или SHORT преобразуются в INT, а любые операнды типа FLOAT преобразуются в DOUBLE. Затем, если какой-либо операнд имеет тип DOUBLE, то другой преобразуется к типу DOUBLE, и это будет типом результата. В противном случае, если какой-либо операнд имеет тип LONG, то другой операнд преобразуется к типу LONG, и это и будет типом результата. В противном случае, если какой-либо операнд имеет тип UNSIGNED, то другой операнд преобразуется к типу UNSIGNED, и это будет типом результата. В противном случае оба операнда будут иметь тип INT, и это будет типом результата.



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


Язык "C" не является языком с блочной структурой в смыс- ле PL/1 или алгола; в нем нельзя описывать одни функции внутри других. Переменные же, с другой стороны, могут определяться по методу блочного структурирования. Описания переменных (вклю- чая инициализацию) могут следовать за левой фигурной скоб- кой,открывающей любой оператор, а не только за той, с кото- рой начинается тело функции. Переменные, описанные таким об- разом, вытесняют любые переменные из внешних блоков, имеющие такие же имена, и остаются определенными до соответствующей правой фигурной скобки. Например в

IF (N > 0) { INT I; /* DECLARE A NEW I */ FOR (I = 0; I < N; I++) ... }

Областью действия переменной I является "истинная" ветвь IF; это I никак не связано ни с какими другими I в програм- ме. Блочная структура влияет и на область действия внешних переменных. Если даны описания

INT X;

F() { DOUBLE X; ... }

То появление X внутри функции F относится к внутренней пере- менной типа DOUBLE, а вне F - к внешней целой переменной. это же справедливо в отношении имен формальных параметров:

INT X; F(X) DOUBLE X; { ... }

Внутри функции F имя X относится к формальному параметру, а не к внешней переменной.



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


Всякий раз, когда целое без знака объединяется с простым целым, простое целое преобразуется в целое без знака и ре- зультат оказывается целым без знака. Значением является наи- меньшее целое без знака, соответствующее целому со знаком (по модулю 2**размер слова). В двоичном дополнительном пред- ставлении это преобразование является чисто умозрительным и не изменяет фактическую комбинацию битов. Когда целое без знака преобразуется к типу LONG, значе- ние результата совпадает со значением целого без знака. Та- ким образом, это преобразование сводится к добавлению нулей слева.



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


Целая константа, состоящая из последовательности цифр, считается восьмеричной, если она начинается с 0 (цифра нуль), и десятичной в противном случае. Цифры 8 и 9 имеют восьмеричные значения 10 и 11 соответственно. Последователь- ность цифр, которой предшествуют символы 0х (нуль, х-малень- кое) или 0х (нуль х-большое), рассматривается как шестнадца- тиричное целое. Шестнадцатиричные цифры включают буквы от а (маленькое) или а (большое) до F (маленькое) или F (большое) со значениями от 10 до 15. Десятичная константа, величина которой превышает наибольшее машинное целое со знаком, счи- тается длинной; восмеричная или шестнадцатиричная константа, которое превышает наибольшее машинное целое без знака, также считается длинной.



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


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

В языке "C" предусмотрено несколько основных типов объектов: объекты, написанные как символы (CHAR), достаточно вели- ки, чтобы хранить любой член из соответствующего данной реа- лизации внутреннего набора символов, и если действительный символ из этого набора символов хранится в символьной пере- менной, то ее значение эквивалентно целому коду этого симво- ла. В символьных переменных можно хранить и другие величины, но реализация будет машинно-зависимой. Можно использовать до трех размеров целых, описываемых как SHORT INT, INT и LONG INT. Длинные целые занимают не меньше памяти, чем короткие, но в конкретной реализации мо- жет оказаться, что либо короткие целые, либо длинные целые, либо те и другие будут эквивалентны простым целым. "Простые" целые имеют естественный размер, предусматриваемый архиитек- турой используемой машины; другие размеры вводятся для удво- летворения специальных потребностей. Целые без знака, описываемые как UNSIGNED, подчиняются законам арифметики по модулю 2**N, где N - число битов в их представлении. (На PDP-11 длинные величины без знака не пре- дусмотрены). Плавающие одинарной точности (FLOAT) и плавающие двойной точности (DOUBLE) в некоторых реализациях могут быть синони- мами. Поскольку объекты упомянутых выше типов могут быть ра- зумно интерпретированы как числа, эти типы будут называться арифметическими. типы CHAR и INT всех размеров совместно бу- дут называться целочисленными. Типы FLOAT и DOUBLE совместно будут называться плавающими типами. Кроме основных арифметических типов существует концепту- ально бесконечный класс производных типов, которые образуют- ся из основных типов следующим образом: массивы объектов большинства типов; функции, которые возвращают объекты заданного типа; указатели на объекты данного типа; структуры, содержащие последовательность объектов различных типов; объединения, способные содержать один из нескольких объектов различных типов. Вообще говоря, эти методы построения объектов могут при- меняться рекурсивно.



Цикл DO - WHILE


Как уже отмечалось в главе 1, циклы WHILE и FOR обладают тем приятным свойством, что в них проверка окончания осущес- твляется в начале, а не в конце цикла. Третий оператор цикла языка "C", DO-WHILE, проверяет условие окончания в конце, после каждого прохода через тело цикла; тело цикла всегда выполняется по крайней мере один раз. Синтаксис этого опера- тора имеет вид:

DO оператор WHILE (выражение)

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

Как и можно было ожидать, цикл DO-WHILE используется значительно реже, чем WHILE и FOR, составляя примерно пять процентов от всех циклов. Тем не менее, иногда он оказывает- ся полезным, как, например, в следующей функции ITOA, кото- рая преобразует число в символьную строку (обратная функции ATOI). Эта задача оказывается несколько более сложной, чем может показаться сначала. Дело в том, что простые методы вы- деления цифр генерируют их в неправильном порядке. Мы пред- почли получить строку в обратном порядке, а затем обратить ее. 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тоа, который имеет три, а не два аргу- мента. Третий аргумент - минимальная ширина поля; преобразо- ванное число должно, если это необходимо, дополняться слева пробелами, так чтобы оно имело достаточную ширину.



Циклы - WHILE и FOR


Мы уже сталкивались с операторами цикла WHILE и FOR. В конструкции

WHILE (выражение) оператор

вычисляется выражение. Если его значение отлично от нуля, то выполняется оператор и выражение вычисляется снова. Этот цикл продолжается до тех пор, пока значение выражения не станет нулем, после чего выполнение программы продолжается с места после оператора.

Оператор

FOR (выражение 1; выражение 2; выражение 3) оператор

эквивалентен последовательности

выражение 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 вычисления, которые не относятся к уп- равлению циклом, лучше поместить их в управляемые циклом операторы.


В качестве большего по размеру примера приведем другой вариант функции 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); }

Преимущества централизации управления циклом становятся еще более очевидными, когда имеется несколько вложенных цик- лов. Следующая функция сортирует массив целых чисел по мето- ду шелла. основная идея сортировки по шеллу заключается в том, что сначала сравниваются удаленные элементы, а не смеж- ные, как в обычном методе сортировки. Это приводит к быстро- му устранению большой части неупорядоченности и сокращает последующую работу. Интервал между элементами постепенно сокращается до единицы, когда сортировка фактически превра- щается в метод перестановки соседних элементов. 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; } }

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

Упражнение 3-2

--------------- Составьте программу для функции EXPAND(S1,S2), которая расширяет сокращенные обозначения вида а-Z из строки S1 в эквивалентный полный список авс...XYZ в S2. Допускаются сок- ращения для строчных и прописных букв и цифр. Будьте готовы иметь дело со случаями типа а-в-с, а-Z0-9 и -а-Z. (Полезное соглашение состоит в том, что символ -, стоящий в начале или конце, воспринимается буквально).




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


В операционной системе UNIX весь ввод и вывод осуществ- ляется посредством чтения файлов или их записи, потому что все периферийные устройства, включая даже терминал пользова- теля, являются файлами определенной файловой системы. Это означает, что один однородный интерфейс управляет всеми свя- зями между программой и периферийными устройствами. В наиболее общем случае перед чтением из файла или за- писью в файл необходимо сообщить системе о вашем намерении; этот процесс называется "открытием" файла. Система выясня- ет,имеете ли вы право поступать таким образом (существует ли этот файл? имеется ли у вас разрешение на обращение к не- му?), и если все в порядке, возвращает в программу небольшое положительное целое число, называемое дескриптором файла. всякий раз, когда этот файл используется для ввода или выво- да, для идентификации файла употребляется дескриптор файла, а не его имя. (Здесь существует примерная аналогия с исполь- зованием READ (5,...) и WRITE (6,...) в фортране). Вся ин- формация об открытом файле содержится в системе; программа пользователя обращается к файлу только через дескриптор фай- ла. Для удобства выполнения обычных операций ввода и вывода с помощью терминала пользователя существуют специальные сог- лашения. Когда интерпретатор команд ("SHELL") прогоняет программу, он открывает три файла, называемые стандартным вводом, стандартным выводом и стандартным выводом ошибок, которые имеют соответственно числа 0, 1 и 2 в качестве деск- рипторов этих файлов. В нормальном состоянии все они связаны с терминалом, так что если программа читает с дескриптором файла 0 и пишет с дескрипторами файлов 1 и 2, то она может осуществлять ввод и вывод с помощью терминала, не заботясь об открытии соответствующих файлов.

Пользователь программы может перенаправлять ввод и вывод на файлы, используя операции командного интерпретатора SHELL "<" и ">" :

PROG <INFILE>OUTFILE

В этом случае интерпретатор команд SHELL изменит присваива- ние по умолчанию дескрипторов файлов 0 и 1 с терминала на указанные файлы. Нормально дескриптор файла 2 остается свя- занным с терминалом, так что сообщения об ошибках могут пос- тупать туда. Подобные замечания справедливы и тогда, когда ввод и вывод связан с каналом. Следует отметить, что во всех случаях прикрепления файлов изменяются интерпретатором SHELL, а не программой. Сама программа, пока она использует файл 0 для ввода и файлы 1 и 2 для вывода, не знает ни отку- да приходит ее ввод, ни куда поступает ее выдача.



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


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

CAT X.C.Y.C

печатает содержимое файлов X.C и Y.C в стандартный вывод. Вопрос состоит в том, как организовать чтение из имено- ванных файлов, т.е., как связать внешние имена, которыми мыслит пользователь, с фактически читающими данные операто- рами.

Эти правила просты. Прежде чем можно считывать из неко- торого файла или записывать в него, этот файл должен быть открыт с помощью функции FOPEN из стандартной библиотеки. функция FOPEN берет внешнее имя (подобное X.C или Y.C), про- водит некоторые обслуживающие действия и переговоры с опера- ционной системой (детали которых не должны нас касаться) и возвращает внутреннее имя, которое должно использоваться при последующих чтениях из файла или записях в него. Это внутреннее имя, называемое "указателем файла", фак- тически является указателем структуры, которая содержит ин- формацию о файле, такую как место размещения буфера, текущая позиция символа в буфере, происходит ли чтение из файла или запись в него и тому подобное. Пользователи не обязаны знать эти детали, потому что среди определений для стандартного ввода-вывода, получаемых из файла STDIO.H, содержится опре- деление структуры с именем FILE. Единственное необходимое для указателя файла описание демонстрируется примером:

FILE *FOPEN(), *FP;

Здесь говорится, что FP является указателем на FILE и FOPEN возвращает указатель на FILE. Oбратите внимание, что FILE является именем типа, подобным INT, а не ярлыку струк- туры; это реализовано как TYPEDEF. (Подробности того, как все это работает на системе UNIX, приведены в главе 8). Фактическое обращение к функции FOPEN в программе имеет вид: FP=FOPEN(NAME,MODE);


Первым аргументом функции FOPEN является "имя" файла, кото- рое задается в виде символьной строки. Второй аргумент MODE ("режим") также является символьной строкой, которая указы- вает, как этот файл будет использоваться. Допустимыми режи- мами являются: чтение ("R"), запись ("W") и добавление ("A"). Если вы откроете файл, который еще не сущетвует, для за- писи или добавления, то такой файл будет создан (если это возможно). Открытие существующего файла на запись приводит к отбрасыванию его старого содержимого. Попытка чтения несу- ществующего файла является ощибкой. Ошибки могут быть обус- ловлены и другими причинами (например, попыткой чтения из файла, не имея на то разрешения). При наличии какой-либо ошибки функция возвращает нулевое значение указателя NULL (которое для удобства также определяется в файле STDIO.H). Другой необходимой вещью является способ чтения или за- писи, если файл уже открыт. Здесь имеется несколько возмож- ностей, из которых GETC и PUTC являются простейшими.функция GETC возвращает следующий символ из файла; ей необходим ука- затель файла, чтобы знать, из какого файла читать. Таким об- разом,

C=GETC(FP)

помещает в "C" следующий символ из файла, указанного посред- ством FP, и EOF, если достигнут конец файла. Функция PUTC, являющаяся обращением к функции GETC,

PUTC(C,FP)

помещает символ "C" в файл FP и возвращает "C". Подобно фун- кциям GETCHAR и PUTCHAR, GETC и PUTC могут быть макросами, а не функциями. При запуске программы автоматически открываются три фай- ла, которые снабжены определенными указателями файлов. Этими файлами являются стандартный ввод, стандартный вывод и стан- дартный вывод ошибок; соответствующие указатели файлов назы- ваются STDIN, STDOUT и STDERR. Обычно все эти указатели свя- заны с терминалом, но STDIN и STDOUT могут быть перенаправ- лены на файлы или в поток (PIPE), как описывалось в разделе 7.2. Функции GETCHAR и PUTCHAR могут быть определены в терми- налах GETC, PUTC, STDIN и STDOUT следующим образом: #DEFINE GETCHAR() GETC(STDIN) #DEFINE PUTCHAR(C) PUTC(C, STDOUT) При работе с файлами для форматного ввода и вывода можно ис- пользовать функции FSCANF и FPRINTF. Они идентичны функциям SCANF и PRINTF, за исключением того, что первым аргументом является указатель файла, определяющий тот файл, который бу- дет читаться или куда будет вестись запись; управляющая строка будет вторым аргументом.



Покончив с предварительными замечаниями, мы теперь в состоянии написать программу CAT для конкатенации файлов. Используемая здесь основная схема оказывается удобной во многих программах: если имеются аргументы в командной стро- ке, то они обрабатываются последовательно. Если такие аргу- менты отсутствуют, то обрабатывается стандартный ввод. Это позволяет использовать программу как самостоятельно, так и как часть большей задачи.

#INCLUDE <STDIO.H>

MAIN(ARGC, ARGV) /*CAT: CONCATENATE FILES*/ INT ARGC; CHAR *ARGV[]; \( FILE *FP, *FOPEN(); IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/ FILECOPY(STDIN); ELSE WHILE (--ARGC > 0) IF ((FP=FOPEN(*++ARGV,"R"))==NULL) \( PRINTF("CAT:CAN'T OPEN %\N",*ARGV); BREAK; \) ELSE \( FILECOPY(FP); FCLOSE(FP); \) \) FILECOPY(FP) /*COPY FILE FP TO STANDARD OUTPUT*/ FILE *FP; \( INT C; WHILE ((C=GETC(FP)) !=EOF) PUTC(C, STDOUT); \)

Указатели файлов STDIN и STDOUT заранее определены в библио- теке ввода-вывода как стандартный ввод и стандартный вывод; они могут быть использованы в любом месте, где можно исполь- зовать объект типа FILE*.они однако являются константами, а не переменными, так что не пытайтесь им что-либо присваи- вать. Функция FCLOSE является обратной по отношению к FOPEN; она разрывает связь между указателем файла и внешним именем, установленную функцией FOPEN, и высвобождает указатель файла для другого файла.большинство операционных систем имеют не- которые ограничения на число одновременно открытых файлов, которыми может распоряжаться программа. Поэтому, то как мы поступили в CAT, освободив не нужные нам более объекты, яв- ляется хорошей идеей. Имеется и другая причина для примене- ния функции FCLOSE к выходному файлу - она вызывает выдачу информации из буфера, в котором PUTC собирает вывод. (При нормальном завершении работы программы функция FCLOSE вызы- вается автоматически для каждого открытого файла).




ELSE - IF


Конструкция

IF (выражение) оператор ELSE IF (выражение) оператор ELSE IF (выражение) оператор ELSE оператор

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

ELSE оператор

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

Для иллюстрации выбора из трех возможных вариантов при- ведем программу функции, которая методом половинного деления определяет, находится ли данное значение х в отсортированном массиве 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 здесь вполне ес- тественно.



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


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

Если в качестве аргумента функции выступает имя массива, то передается адрес начала этого массива; сами элементы не копируются. Функция может изменять элементы массива, исполь- зуя индексацию и адрес начала. Таким образом, массив переда- ется по ссылке. В главе 5 мы обсудим, как использование ука- зателей позволяет функциям воздействовать на отличные от массивов переменные в вызывающих функциях. Между прочим, несуществует полностью удовлетворительного способа написания переносимой функции с переменным числом аргументов. Дело в том, что нет переносимого способа, с по- мощью которого вызванная функция могла бы определить, сколь- ко аргументов было фактически передано ей в данном обраще- нии. Таким образом, вы, например, не можете написать дейст- вительно переносимую функцию, которая будет вычислять макси- мум от произвольного числа аргументов, как делают встроенные функции MAX в фортране и PL/1. Обычно со случаем переменного числа аргументов безопасно иметь дело, если вызванная функция не использует аргументов, которые ей на самом деле не были переданы, и если типы сог- ласуются. Самая распространенная в языке "C" функция с пере- менным числом - PRINTF . Она получает из первого аргумента информацию, позволяющую определить количество остальных ар- гументов и их типы. Функция PRINTF работает совершенно неп- равильно, если вызывающая функция передает ей недостаточное количество аргументов, или если их типы не согласуются с ти- пами, указанными в первом аргументе. Эта функция не является переносимой и должна модифицироваться при использовании в различных условиях. Если же типы аргументов известны, то конец списка аргу- ментов можно отметить, используя какое-то соглашение; напри- мер, считая, что некоторое специальное значение аргумента (часто нуль) является признаком конца аргументов.



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


От функции SCANF и PRINTF происходят функции SSCANF и SPRINTF, которые осуществляют аналогичные преобразования, но оперируют со строкой, а не с файлом. Обращения к этим функ- циям имеют вид:

SPRINTF(STRING, CONTROL, ARG1, ARG2, ...) SSCANF(STRING, CONTROL, ARG1, ARG2, ...)

Как и раньше , функция SPRINTF преобразует свои аргументы ARG1, ARG2 и т.д. В соответствии с форматом, указанным в CONTROL, но помещает результаты в STRING, а не в стандартный вывод. KОнечно, строка STRING должна быть достаточно велика, чтобы принять результат. Например, если NAME - это символь- ный массив, а N - целое, то

SPRINTF(NAME, "TEMP%D", N);

создает в NAME строку вида TEMPNNN, где NNN - значение N. Функция SSCANF выполняет обратные преобразования - она просматривает строку STRING в соответствии с форматом в ар- гументе CONTROL и помещает результирующие значения в аргу- менты ARG1, ARG2 и т.д.эти аргументы должны быть указателя- ми. В результате обращения

SSCANF(NAME, "TEMP%D", &N);

переменная N получает значение строки цифр, следующих за TEMP в NAME.

Упражнение 7-2

-------------- Перепишите настольный калькулятор из главы 4, используя для ввода и преобразования чисел SCANF и/или SSCANF.



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


Осуществляющая ввод функция SCANF является аналогом PRINTF и позволяет проводить в обратном направлении многие из тех же самых преобразований. Функция

SCANF(CONTROL, ARG1, ARG2, ...)

читает символы из стандартного ввода, интерпретирует их в соответствии с форматом, указанном в аргументе CONTROL, и помещает результаты в остальные аргументы. Управляющий аргу- мент описывается ниже; другие аргументы, каждый из которых должен быть указателем, определяют, куда следует поместить соответствующим образом преобразованный ввод. Управляющая строка обычно содержит спецификации преобра- зования, которые используются для непосредственной интерпре- тации входных последовательностей. Управляющая строка может содержать: - пробелы, табуляции или символы новой строки ("символы пус- тых промежутков"), которые игнорируются.

- Обычные символы (не %), которые предполагаются совпадающи- ми со следующими отличными от символов пустых промежутков символами входного потока. - Спецификации преобразования, состоящие из символа %, нео- бязательного символа подавления присваивания *, необяза- тельного числа, задающего максимальную ширину поля и сим- вола преобразования. Спецификация преобразования управляет преобразованием следующего поля ввода. нормально результат помещается в пе- ременную, которая указывается соответствующим аргументом. Если, однако , с помощью символа * указано подавление прис- ваивания, то это поле ввода просто пропускается и никакого присваивания не производится. Поле ввода определяется как строка символов, которые отличны от символов простых проме- жутков; оно продолжается либо до следующего символа пустого промежутка, либо пока не будет исчерпана ширина поля, если она указана. Отсюда следует, что при поиске нужного ей вво- да, функция SCANF будет пересекать границы строк, поскольку символ новой строки входит в число пустых промежутков. Символ преобразования определяет интерпретацию поля вво- да; согласно требованиям основанной на вызове по значению семантики языка "с" соответствующий аргумент должен быть указателем. Допускаются следующие символы преобразования: D - на вводе ожидается десятичное целое; соответствующий ар- гумент должен быть указателем на целое. O - На вводе ожидается восьмеричное целое (с лидирующим ну- лем или без него); соответствующий аргумент должен быть указателем на целое. X - На вводе ожидается шестнадцатеричное целое (с лидирующи- ми 0X или без них); соответствующий аргумент должен быть указателем на целое. H - На вводе ожидается целое типа SHORT; соответсвующий ар- гумент должен быть указателем на целое типа SHORT. C - Ожидается отдельный символ; соответствующий аргумент должен быть указателем на символы; следующий вводимый символ помещается в указанное место. Обычный пропуск сим- волов пустых промежутков в этом случае подавляется; для чтения следующего символа, который не является символом пустого промежутка, пользуйтесь спецификацией преобразо- вания %1S. S - Ожидается символьная строка; соответствующий аргумент должен быть указателем символов, который указывает на массив символов, который достаточно велик для принятия строки и добавляемого в конце символа \0. F - Ожидается число с плавающей точкой; соответствующий ар- гумент должен быть указателем на переменную типа FLOAT. Е - символ преобразования E является синонимом для F. Формат ввода переменной типа FLOAT включает необязательный знак, строку цифр, возможно содержащую десятичную точку и нео- бязательное поле экспоненты, состоящее из буквы E, за ко- торой следует целое, возможно имеющее знак.


Перед символами преобразования D, O и X может стоять L, которая означает , что в списке аргументов должен находиться указатель на переменную типа LONG, а не типа INT. Аналогич- но, буква L может стоять перед символами преобразования E или F, говоря о том, что в списке аргументов должен нахо- диться указатель на переменную типа DOUBLE, а не типа FLOAT. Например, обращение INT I; FLOAT X; CHAR NAME[50]; SCANF("&D %F %S", &I, &X, NAME);

со строкой на вводе

25 54.32E-1 THOMPSON

приводит к присваиванию I значения 25,X - значения 5.432 и NAME - строки "THOMPSON", надлежащим образом законченной символом \ 0. эти три поля ввода можно разделить столькими пробелами, табуляциями и символами новых строк, сколько вы пожелаете. Обращение

INT I; FLOAT X; CHAR NAME[50]; SCANF("%2D %F %*D %2S", &I, &X, NAME);

с вводом

56789 0123 45A72

присвоит I значение 56, X - 789.0, пропустит 0123 и поместит в NAME строку "45". при следующем обращении к любой процеду- ре ввода рассмотрение начнется с буквы A. В этих двух приме- рах NAME является указателем и, следовательно, перед ним не нужно помещать знак &. В качестве другого примера перепишем теперь элементарный калькулятор из главы 4, используя для преобразования ввода функцию SCANF:

#INCLUDE <STDIO.H>

MAIN() /* RUDIMENTARY DESK CALCULATOR */ \( DOUBLE SUM, V; SUM =0; WHILE (SCANF("%LF", &V) !=EOF) PRINTF("\T%.2F\N", SUM += V); \)

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

использовано для определения количества найденных элементов ввода. при выходе на конец файла возвращается EOF; подчерк- нем, что это значение отлично от 0, что следующий вводимый символ не удовлетворяет первой спецификации в управляющей строке. При следующем обращении к SCANF поиск возобновляется непосредственно за последним введенным символом. Заключительное предостережение: аргументы функции SCANF должны быть указателями. Несомненно наиболее распространен- ная ошибка состоит в написании

SCANF("%D", N);

вместо

SCANF("%D", &N);




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


Две функции: PRINTF для вывода и SCANF для ввода (следу- ющий раздел) позволяют преобразовывать численные величины в символьное представлEние и обратно. Они также позволяют ге- нерировать и интерпретировать форматные строки. Мы уже всюду в предыдущих главах неформально использовали функцию PRINTF; здесь приводится более полное и точное описание. Функция

PRINTF(CONTROL, ARG1, ARG2, ...)

преобразует, определяет формат и печатает свои аргументы в стандартный вывод под управлением строки CONTROL. Управляю- щая строка содержит два типа объектов: обычные символы, ко- торые просто копируются в выходной поток, и спецификации преобразований, каждая из которых вызывает преобразование и печать очередного аргумента PRINTF. Каждая спецификация преобразования начинается с символа % и заканчивается символом преобразования. Между % и симво- лом преобразования могут находиться: - знак минус, который указывает о выравнивании преобразован- ного аргумента по левому краю его поля. - Строка цифр, задающая минимальную ширину поля. Преобразо- ванное число будет напечатано в поле по крайней мере этой ширины, а если необходимо, то и в более широком. Если пре- образованный аргумент имеет меньше символов, чем указанная ширина поля, то он будет дополнен слева (или справа, если было указано выравнивание по левому краю)заполняющими сим- волами до этой ширины. Заполняющим символом обычно являет- ся пробел, а если ширина поля указывается с лидирующим ну- лем, то этим символом будет нуль (лидирующий нуль в данном случае не означает восьмеричной ширины поля). - Точка, которая отделяет ширину поля от следующей строки цифр. - Строка цифр (точность), которая указывает максимальное число символов строки, которые должны быть напечатаны, или число печатаемых справа от десятичной точки цифр для пере- менных типа FLOAT или DOUBLE. - Модификатор длины L, который указывает, что соответствую- щий элемент данных имеет тип LONG, а не INT. Ниже приводятся символы преобразования и их смысл:

D - аргумент преобразуется к десятичному виду. O - Аргумент преобразуется в беззнаковую восьмеричную форму (без лидирующего нуля). X - Аргумент преобразуется в беззнаковую шестнадцатеричную форму (без лидирующих 0X). U - Аргумент преобразуется в беззнаковую десятичную форму. C - Аргумент рассматривается как отдельный символ. S - Аргумент является строкой: символы строки печатаются до тех пор, пока не будет достигнут нулевой символ или не бу- дет напечатано количество символов, указанное в специфика- ции точности. E - Аргумент, рассматриваемый как переменная типа FLOAT или DOUBLE, преобразуется в десятичную форму в виде [-]M.NNNNNNE[+-]XX, где длина строки из N определяется указанной точностью. Точность по умолчанию равна 6. F - Аргумент, рассматриваемый как переменная типа FLOAT или DOUBLE, преобразуется в десятичную форму в виде [-]MMM.NNNNN, где длина строки из N определяется указанной точностью. Точность по умолчанию равна 6. отметим, что эта точность не определяет количество печатаемых в формате F значащих цифр.


G - Используется или формат %E или %F, какой короче; незна- чащие нули не печатаются. Если идущий за % символ не является символом преобразования, то печатается сам этот символ; следовательно,символ % можно напечатать, указав %%. Большинство из форматных преобразований очевидно и было проиллюстрировано в предыдущих главах. Единственным исключе- нием является то, как точность взаимодействует со строками. Следующая таблица демонстрирует влияние задания различных спецификаций на печать "HELLO, WORLD" (12 символов). Мы по- местили двоеточия вокруг каждого поля для того, чтобы вы могли видеть его протяженность.

:%10S: :HELLO, WORLD: :%10-S: :HELLO, WORLD: :%20S: : HELLO, WORLD: :%-20S: :HELLO, WORLD : :%20.10S: : HELLO, WOR: :%-20.10S: :HELLO, WOR : :%.10S: :HELLO, WOR:

Предостережение: PRINTF использует свой первый аргумент для определения числа последующих аргументов и их типов. Ес- ли количество аргументов окажется недостаточным или они бу- дут иметь несоответственные типы, то возникнет путаница и вы получите бессмысленные результаты.

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

--------------

Напишите программу, которая будет печатать разумным об- разом произвольный ввод. Как минимум она должна печатать неграфические символы в восьмеричном или шестнадцатеричном виде (в соответствии с принятыми у вас обычаями) и склады- вать длинные строки.




Функции


В языке "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); } Все функции имеют одинаковый вид: имя (список аргументов, если они имеются) описание аргументов, если они имеются { описания операторы }

Эти функции могут быть записаны в любом порядке и нахо- диться в одном или двух исходных файлах. Конечно, если ис- ходная программа размещается в двух файлах, вам придется дать больше указаний при компиляции и загрузке, чем если бы она находилась в одном, но это дело операционной системы, а не атрибут языка. В данный момент, для того чтобы все полу- ченные сведения о прогоне "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'- не буква, и значение соответствующей строчной уквы, если 'C'-буква.




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


Функции разбивают большие вычислительные задачи на ма- ленькие подзадачи и позволяют использовать в работе то, что уже сделано другими, а не начинать каждый раз с пустого мес- та. Соответствующие функции часто могут скрывать в себе де- тали проводимых в разных частях программы операций, знать которые нет необходимости, проясняя тем самым всю программу, как целое, и облегчая мучения при внесении изменений. Язык "C" разрабатывался со стремлением сделать функции эффективными и удобными для использования; "C"-программы обычно состоят из большого числа маленьких функций, а не из нескольких больших. Программа может размещаться в одном или нескольких исходных файлах любым удобным образом; исходные файлы могут компилироваться отдельно и загружаться вместе наряду со скомпилированными ранее функциями из библиотек. Мы здесь не будем вдаваться в детали этого процесса, поскольку они зависят от используемой системы. Большинство программистов хорошо знакомы с "библиотечны- ми" функциями для ввода и вывода /GETCHAR , PUTCHAR/ и для численных расчетов /SIN, COS, SQRT/. В этой главе мы сообщим больше о написании новых функций.



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


До сих пор ни одна из наших программ не содержала како- го-либо описания типа функции. Дело в том, что по умолчанию функция неявно описывается своим появлением в выражении или операторе, как, например, в

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;

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(...) являются плавающие числа двойной точности.

Если функция 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' и пока- затель экспоненты, возможно со знаком.

Функция UNGETC


Стандартная библиотека содержит довольно ограниченную версию функции UNGETCH, написанной нами в главе 4; она назы- вается UNGETC. В результате обращения

UNGETC(C,FP)

символ "C" возвращается в файл FP. Позволяется возвращать в каждый файл только один символ. Функция UNGETC может быть использована в любой из функций ввода и с макросами типа SCANF, GETC или GETCHAR.



Hачинаем


Единственный способ освоить новый язык программирования - писать на нем программы. Первая програм- ма, которая должна быть написана, - одна для всех языков: напечатать слова : HELLO, WORLD. Это - самый существенный барьер; чтобы преодолеть его, вы должны суметь завести где-то текст программы, успешно его скомпилировать, загрузить, прогнать и найти, где оказалась ваша выдача. Если вы научились справляться с этими техничес- кими деталями, все остальное сравнительно просто.

Программа печати "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 обычно обращается к другим функциям, часть из которых находится в той же самой программе, а часть - в библиотеках, содержащих ранее написанные функции.


Одним способом обмена данными между функциями является передача посредством аргументов. Круглые скобки, следующие за именем функции, заключают в себе список аргументов; здесь ма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"); } Подчеркнем, что \N представляет только один символ. Ус- ловные 'последовательности', подобные \N , дают общий и до- пускающий расширение механизм для представления трудных для печати или невидимых символов. Среди прочих символов в языке "C" предусмотрены следующие: \т - для табуляции, \B - для возврата на одну позицию, \" - для двойной кавычки и \\ для самой обратной косой черты.

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

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




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


Следующая ниже таблица суммирует некоторые свойства ап- паратного оборудования, которые меняются от машины к машине. Хотя они и влияют на переносимость программ, на практике они представляют маленькую проблему, чем это может казаться за- ранее. Таблица 1 ------------------------------------------------------- DEC PDP-11 HONEYWELL IBM 370 INTERDATA 8/32 ASCII ASCII EBCDIC ASCII CHAR 8 BITS 9 BITS 8 BITS 8 BITS INT 16 36 32 32 SHORT 16 36 16 16 LONG 32 36 32 32 FLOAT 32 36 32 32 DOUBLE 64 72 64 64 RANGE -38/+38 -38/+38 -76/+76 -76/+76 --------------------------------------------------------



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


Идентификатор - это последовательность букв и цифр; пер- вый символ должен быть буквой. Подчеркивание _ считается буквой. Буквы нижнего и верхнего регистров различаются. зна- чащими являются не более, чем первые восемь символов, хотя можно использовать и больше. На внешние идентификаторы, ко- торые используются различными ассемблерами и загрузчиками, накладыватся более жесткие ограничения:

DEC PDP-11 7 символов, 2 регистра HONEYWELL 6000 6 символов, 1 регистр IBM 360/370 7 символов, 1 регистр INTERDATA 8/32 8 символов, 2 регистра



IF - ELSE


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

IF (выражение) оператор-1 ELSE оператор-2,

Где часть ELSE является необязательной. Сначала вычисля- ется выражение; если оно "истинно" /т.е. значение выражения отлично от нуля/, то выполняется оператор-1. Если оно ложно /значение выражения равно нулю/, и если есть часть с ELSE, то вместо оператора-1 выполняется оператор-2.

Так как 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");

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

IF (A > B) Z = A; ELSE Z = B;

после Z=A стоит точка с запятой. Дело в том, что согласно грамматическим правилам за IF должен следовать оператор, а выражение типа Z=A, являющееся оператором, всегда заканчива- ется точкой с запятой.



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


Хотя мы этого сразу прямо не сказали, существуют некото- рые ограничения на имена переменных и символических конс- тант. Имена составляются из букв и цифр; первый символ дол- жен быть буквой. Подчеркивание "_" тоже считается буквой; это полезно для удобочитаемости длинных имен переменных. Прописные и строчные буквы различаются; традиционная практи- ка в "с" - использовать строчные буквы для имен переменных, а прописные - для символических констант. Играют роль только первые восемь символов внутреннего имени, хотя использовать можно и больше. Для внешних имен, таких как имена функций и внешних переменных, это число мо- жет оказаться меньше восьми, так как внешние имена использу- ются различными ассемблерами и загрузчиками. Детали приво- дятся в приложении а. Кроме того, такие ключевые слова как IF, ELSE, INT, FLOAT и т.д., зарезервированы: вы не можете использовать их в качестве имен переменных. (Они пишутся строчными буквами). Конечно, разумно выбирать имена переменных таким обра- зом, чтобы они означали нечто, относящееся к назначению пе- ременных, и чтобы было менее вероятно спутать их при написа- нии.



Имена типов


В двух случаях (для явного указания типа преобразования в конструкции перевода и для аргументов операции SIZEOF) же- лательно иметь возможность задавать имя типа данных. Это осуществляется с помощью "имени типа", которое по существу является описанием объекта такого типа , в котором опущено имя самого объекта.

Имя типа: спецификатор-типа абстрактный-описатель абстрактный-описатель: пусто (абстрактный-описатель) * абстрактный описатель абстрактный-описатель () абстрактный-описатель [константное выражение необ]

Во избежании двусмысленности в конструкции

(абстрактный описатель)

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

INT INT * INT *[3] INT (*)[3] INT *() INT (*)()

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



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


Мы до сих пор уже много раз упоминали инициализацию, но всегда мимоходом , среди других вопросов. Теперь, после того как мы обсудили различные классы памяти, мы в этом разделе просуммируем некоторые правила, относящиеся к инициализации. Если явная инициализация отсутствует, то внешним и ста- тическим переменным присваивается значение нуль; автомати- ческие и регистровые переменные имеют в этом случае неопре- деленные значения (мусор).

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

INT X = 1; CHAR SQUOTE = '\''; LONG DAY = 60 * 24; /* MINUTES IN A DAY */

Для внешних и статических переменных инициализация выполня- ется только один раз, на этапе компиляции. Автоматические и регистровые переменные инициализируются каждый раз при входе в функцию или блок. В случае автоматических и регистровых переменных инициализа- тор не обязан быть константой: на самом деле он может быть любым значимым выражением, которое может включать определен- ные ранее величины и даже обращения к функциям. Например, инициализация в программе бинарного поиска из главы 3 могла бы быть записана в виде

BINARY(X, V, N) INT X, V[], N; { INT LOW = 0; INT HIGH = N - 1; INT MID; ... }

вместо

BINARY(X, V, N) INT X, V[], N; { INT LOW, HIGH, MID;

LOW = 0; HIGH = N - 1; ... }

По своему результату, инициализации автоматических перемен- ных являются сокращенной записью операторов присваивания. Какую форму предпочесть - в основном дело вкуса. мы обычно используем явные присваивания, потому что инициализация в описаниях менее заметна. Автоматические массивы не могут быть инициализированы. Внеш- ние и статические массивы можно инициализировать, помещая вслед за описанием заключенный в фигурные скобки список на- чальных значений, разделенных запятыми. Например программа подсчета символов из главы 1, которая начиналась с 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; ... )

Ожет быть переписана в виде

INT NWHITE = 0; INT NOTHER = 0; INT NDIGIT[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */ ( INT C, I; ... )

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

CHAR PATTERN[] = "THE";

Это сокращение более длинной, но эквивалентной записи:

CHAR PATTERN[] = { 'T', 'H', 'E', '\0' };

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




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


Рассмотрим задачу написания функции MONTH_NAME(N), кото- рая возвращает указатель на символьную строку, содержащую имя N-го месяца. Это идеальная задача для применения внут- реннего статического массива. Функция MONTH_NAME содержит локальный массив символьных строк и при обращении к ней воз- вращает указатель нужной строки. Тема настоящего раздела - как инициализировать этот массив имен.

CHAR *MONTH_NAME(N) /* RETURN NAME OF N-TH MONTH */ INT N; \( STATIC CHAR *NAME[] = \( "ILLEGAL MONTH", "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUN", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" \); RETURN ((N < 1 \!\! N > 12) ? NAME[0] : NAME[N]); \)

Описание массива указателей на символы NAME точно такое же, как аналогичное описание LINEPTR в примере с сортировкой. Инициализатором является просто список символьных строк; каждая строка присваивается соответствующей позиции в масси- ве. Более точно, символы I-ой строки помещаются в какое-то иное место, а ее указатель хранится в NAME[I]. Поскольку размер массива NAME не указан, компилятор сам подсчитывает количество инициализаторов и соответственно устанавливает правильное число.



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


Материал этой главы относится к интерфейсу между с-прог- раммами и операционной системой UNIX. Так как большинство пользователей языка "C" работают на системе UNIX, эта глава окажется полезной для большинства читателей. даже если вы используете с-компилятор на другой машине, изучение приводи- мых здесь примеров должно помочь вам глубже проникнуть в ме- тоды программирования на языке "C". Эта глава делится на три основные части: ввод/вывод, система файлов и распределение памяти. Первые две части предполагают небольшое знакомство с внешними характеристика- ми системы UNIX. В главе 7 мы имели дело с системным интерфейсом, который одинаков для всего многообразия операционных систем. На каж- дой конкретной системе функции стандартной библиотеки должны быть написаны в терминах ввода-вывода, доступных на данной машине. В следующих нескольких разделах мы опишем основную систему связанных с вводом и выводом точек входа операцион- ной системы UNIX и проиллюстрируем, как с их помощью могут быть реализованы различные части стандартной библиотеки.



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


Десятичная, восмеричная или шестнадцатиричная константа, за которой непосредственно следует L (эль-маленькое) или L (эль-большое), является длинной константой. Как обсуждается ниже, на некоторых машинах целые и длинные значения могут рассматриваться как идентичные.



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


Разрешаются определенные преобразования, с использовани- ем указателей , но они имеют некоторые зависящие от конкрет- ной реализации аспекты. Все эти преобразования задаются с помощью операции явного преобразования типа; см. П. 15.2 и 16.7. Указатель может быть преобразован в любой из целочислен- ных типов, достаточно большой для его хранения. Требуется ли при этом INT или LONG, зависит от конкретной машины. Преоб- разующая функция также является машинно-зависимой, но она будет вполне естественной для тех, кто знает структуру адре- сации в машине. Детали для некоторых конкретных машин приво- дятся ниже. Объект целочисленного типа может быть явным образом пре- образован в указатель. такое преобразование всегда переводит преобразованное из указателя целое в тот же самый указатель, но в других случаях оно будет машинно-зависимым.

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

EXTERN CHAR *ALLOC(); DOUBLE *DP; DP=(DOUBLE*) ALLOC(SIZEOF(DOUBLE)); *DP=22.0/7.0;

Функция ALLOC должна обеспечивать (машинно-зависимым спосо- бом), что возвращаемое ею значение будет подходящим для пре- образования в указатель на DOUBLE; в таком случае использо- вание этой функции будет переносимым. Представление указателя на PDP-11 соответствует 16-бито- вому целому и измеряется в байтах. Объекты типа CHAR не име- ют никаких ограничений на выравнивание; все остальные объек- ты должны иметь четные адреса. На HONEYWELL 6000 указатель соответствует 36-битовому целому; слову соответствует 18 левых битов и два непосредст- венно примыкающих к ним справа бита, которые выделяют символ в слове. Таким образом, указатели на символы измеряются в единицах 2 в степени 16 байтов; все остальное измеряется в единицах 2 в степени 18 машинных слов. Величины типа DOUBLE и содержащие их агрегаты должны выравниваться по четным ад- ресам слов (0 по модулю 2 в степени 19). Эвм IBM 370 и INTERDATA 8/32 сходны между собой. На обеих машинах адреса измеряются в байтах; элементарные объекты должны быть выров- нены по границе, равной их длине, так что указатели на SHORT должны быть кратны двум, на INT и FLOAT - четырем и на DOUBLE - восьми. Агрегаты выравниваются по самой строгой границе, требуемой каким-либо из их элементов.



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


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

INT EXTERN ELSE CHAR REGISTER FOR FLOAT TYPEDEF DO DOUBLE STATIC WHILE STRUCT GOTO SWITCH UNION RETURN CASE LONG SIZEOF DEFAULT SHORT BREAK ENTRY UNSIGNED CONTINUE *AUTO IF

Ключевое слово ENTRY в настоящее время не используется ка- ким-либо компилятором; оно зарезервировано для использования в будущем. В некоторых реализациях резервируется также слова FORTRAN и ASM



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


Системные средства, на которые опирается реализация язы- ка "с", позволяют передавать командную строку аргументов или параметров начинающей выполняться программе. Когда функция MAIN вызывается к исполнению, она вызывается с двумя аргу- ментами. Первый аргумент (условно называемый ARGC) указывает число аргументов в командной строке, с которыми происходит обращение к программе; второй аргумент (ARGV) является ука- зателем на массив символьных строк, содержащих эти аргумен- ты, по одному в строке. Работа с такими строками - это обыч- ное использование многоуровневых указателей. Самую простую иллюстрацию этой возможности и необходимых при этом описаний дает программа ECHO, которая просто печа- тает в одну строку аргументы командной строки, разделяя их пробелами. Таким образом, если дана команда

ECHO HELLO, WORLD

то выходом будет

HELLO, WORLD

по соглашению ARGV[0] является именем, по которому вызывает- ся программа, так что ARGC по меньшей мере равен 1. В приве- денном выше примере ARGC равен 3, а ARGV[0], ARGV[1] и ARGV[2] равны соответственно "ECHO", "HELLO," и "WORLD". Первым фактическим агументом является ARGV[1], а последним - ARGV[ARGC-1]. Если ARGC равен 1, то за именем программы не следует никакой командной строки аргументов. Все это показа- но в ECHO:

MAIN(ARGC, ARGV) /* ECHO ARGUMENTS; 1ST VERSION */ INT ARGC; CHAR *ARGV[]; \( INT I;

FOR (I = 1; I < ARGC; I++) PRINTF("%S%C", ARGV[I], (I<ARGC-1) ? ' ' : '\N'); \)

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

MAIN(ARGC, ARGV) /* ECHO ARGUMENTS; 2ND VERSION */ INT ARGC; CHAR *ARGV[]; \( WHILE (--ARGC > 0) PRINTF("%S%C",*++ARGV, (ARGC > 1) ? ' ' : '\N'); \)

Так как ARGV является указателем на начало массива строк-ар- гументов, то, увеличив его на 1 (++ARGV), мы вынуждаем его указывать на подлинный аргумент ARGV[1], а не на ARGV[0]. Каждое последующее увеличение передвигает его на следующий аргумент; при этом *ARGV становится указателем на этот аргу- мент. одновременно величина ARGC уменьшается; когда она об- ратится в нуль, все аргументы будут уже напечатаны. Другой вариант:


MAIN(ARGC, ARGV) /* ECHO ARGUMENTS; 3RD VERSION */ INT ARGC; CHAR *ARGV[]; \( WHILE (--ARGC > 0) PRINTF((ARGC > 1) ? "%S" : "%S\N", *++ARGV); \)

Эта версия показывает, что аргумент формата функции PRINTF может быть выражением, точно так же, как и любой другой. Та- кое использование встречается не очень часто, но его все же стоит запомнить. Как второй пример, давайте внесем некоторые усовершенст- вования в программу отыскания заданной комбинации символов из главы 4. Если вы помните, мы поместили искомую комбинацию глубоко внутрь программы, что очевидно является совершенно неудовлетворительным. Следуя утилите GREP системы UNIX, да- вайте изменим программу так, чтобы эта комбинация указыва- лась в качестве первого аргумента строки.

#DEFINE MAXLINE 1000

MAIN(ARGC, ARGV) /* FIND PATTERN FROM FIRST ARGUMENT */ INT ARGC; CHAR *ARGV[]; \( CHAR LINE[MAXLINE];

IF (ARGC != 2) PRINTF ("USAGE: FIND PATTERN\N"); ELSE WHILE (GETLINE(LINE, MAXLINE) > 0) IF (INDEX(LINE, ARGV[1] >= 0) PRINTF("%S", LINE); \)

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

Общепринятым соглашением в "с"-программах является то, что аргумент, начинающийся со знака минус, вводит необяза- тельный признак или параметр. Если мы, для того, чтобы сооб- щить об инверсии, выберем -X, а для указания о нумерации нужных строк выберем -N("номер"), то команда

FIND -X -N THE

при входных данных

NOW IS THE TIME FOR ALL GOOD MEN TO COME TO THE AID OF THEIR PARTY.

Должна выдать

2:FOR ALL GOOD MEN

Нужно, чтобы необязательные аргументы могли располагать- ся в произвольном порядке, и чтобы остальная часть программы не зависела от количества фактически присутствующих аргумен- тов. в частности, вызов функции INDEX не должен содержать ссылку на ARGV[2], когда присутствует один необязательный аргумент, и на ARGV[1], когда его нет. Более того, для поль- зователей удобно, чтобы необязательные аргументы можно было объединить в виде:



FIND -NX THE

вот сама программа:

#DEFINE MAXLINE 1000

MAIN(ARGC, ARGV) /* FIND PATTERN FROM FIRST ARGUMENT */ INT ARGC; CHAR *ARGV[]; \( CHAR LINE[MAXLINE], *S; LONG LINENO = 0; INT EXCEPT = 0, NUMBER = 0; WHILE (--ARGC > 0 && (*++ARGV)[0] == '-') FOR (S = ARGV[0]+1; *S != '\0'; S++) SWITCH (*S) \( CASE 'X': EXCEPT = 1; BREAK;

CASE 'N': NUMBER = 1; BREAK; DEFAULT: PRINTF("FIND: ILLEGAL OPTION %C\N", *S); ARGC = 0; BREAK; \) IF (ARGC != 1) PRINTF("USAGE: FIND -X -N PATTERN\N"); ELSE WHILE (GETLINе(LINE, MAXLINE) > 0) \( LINENO++; IF ((INDEX(LINE, *ARGV) >= 0) != EXCEPT) \ IF (NUMBER) PRINTF("%LD: ", LINENO); PRINTF("%S", LINE); \) \) \)

Аргумент ARGV увеличивается перед каждым необязательным аргументом, в то время как аргумент ARGC уменьшается. если нет ошибок, то в конце цикла величина ARGC должна равняться 1, а *ARGV должно указывать на заданную комбинацию. Обратите внимание на то, что *++ARGV является указателем аргументной строки; (*++ARGV)[0] - ее первый символ. Круглые скобки здесь необходимы, потому что без них выражение бы приняло совершенно отличный (и неправильный) вид *++(ARGV[0]). Дру- гой правильной формой была бы **++ARGV.

Упражнение 5-7

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

ADD 2 3 4 + *

вычисляет 2*(3+4).

Упражнение 5-8

-------------- Модифицируйте программы ENTAB и DETAB (указанные в ка- честве упражнений в главе 1) так, чтобы они получали список табуляционных остановок в качестве аргументов. Если аргумен- ты отсутствуют, используйте стандартную установку табуляций.

Упражнение 5-9

-------------- Расширьте ENTAB и DETAB таким образом, чтобы они воспри- нимали сокращенную нотацию

ENTAB M +N

означающую табуляционные остановки через каждые N столбцов, начиная со столбца M. Выберите удобное (для пользователя) поведение функции по умолчанию.

Упражнение 5-10

--------------- Напишите программу для функции TAIL, печатающей послед- ние N строк из своего файла ввода. Пусть по умолчанию N рав- но 10, но это число может быть изменено с помощью необяза- тельного аргумента, так что

TAIL -N

печатает последние N строк. программа должна действовать ра- ционально, какими бы неразумными ни были бы ввод или значе- ние N. Составьте программу так, чтобы она оптимальным обра- зом использовала доступную память: строки должны храниться, как в функции SORT, а не в двумерном массиве фиксированного размера.




Комментарии


Комментарий открывается символами /* и заканчивается символами /*. Комментарии не вкладываются друг в друга.



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


Константное выражение - это выражение, состоящее из од- них констант. Такие выражения обрабатываются во время компи- ляции, а не при прогоне программы, и соответственно могут быть использованы в любом месте, где можно использовать кон- станту, как, например в #DEFINE MAXLINE 1000 CHAR LINE[MAXLINE+1];

или

SECONDS = 60 * 60 * HOURS;



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


В нескольких местах в языке "C" требуются выражения, ко- торые после вычисления становятся константами: после вариан- тного префикса CASE, в качестве границ массивов и в инициа- лизаторах. В первых двух случаях выражение может содержать только целые константы, символьные константы и выражения SIZEOF, возможно связанные либо бинарными операциями

+ - * / . % & \! Ч << >> == 1= <> <= >=

либо унарными операциями

- \^

либо тернарной операцией ?:

Круглые скобки могут использоваться для группировки, но не для обращения к функциям.

В случае инициализаторов допускается большая (ударение на букву о) свобода; кроме перечисленных выше константных выражений можно также применять унарную операцию & к внешним или статическим объектам и к внешним или статическим масси- вам, имеющим в качестве индексов константное выражение. Унарная операция & может быть также применена неявно, в ре- зультате появления неиндексированных массивов и функций. Ос- новное правило заключается в том, что после вычисления ини- циализатор должен становится либо константой, либо адресом ранее описанного внешнего или статического объекта плюс или минус константа.



Константы


Константы типа INT и FLOAT мы уже рассмотрели. Отметим еще только, что как обычная

123.456е-7,

так и "научная" запись

0.12е3

для FLOAT является законной. Каждая константа с плавающей точкой считается имеющей тип DOUBLE, так что обозначение "E" служит как для FLOAT, так и для DOUBLE. Длинные константы записываются в виде 123L. Обычная це- лая константа, которая слишком длинна для типа INT, рассмат- ривается как LONG.

Существует система обозначений для восьмеричных и шест- надцатеричных констант: лидирующий 0(нуль) в константе типа INT указывает на восьмеричную константу, а стоящие впереди 0X соответствуют шестнадцатеричной константе. Например, де- сятичное число 31 можно записать как 037 в восьмеричной фор- ме и как 0X1F в шестнадцатеричной. Шестнадцатеричные и вось- меричные константы могут также заканчиваться буквой L, что делает их относящимися к типу LONG.


Имеется несколько видов констант, которые перечислены ниже. В пункте 10.6 резюмируются характеристики аппаратных сред- ств, которые влияют на размеры.



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


Имея в своем распоряжении только функции GETCHAR и PUTCHAR вы можете, не зная ничего более об операциях вво- да-вывода, написать удивительное количество полезных прог- рамм. Простейшим примером может служить программа посимволь- ного копирования вводного файла в выводной. Общая схема име- ет вид: ввести символ WHILE (символ не является признаком конца файла) вывести только что прочитанный символ ввести новый символ

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

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

C = GETCHAR(); WHILE (C != EOF) { PUTCHAR (C); C = GETCHAR(); } }

оператор отношения != означает "не равно". Основная проблема заключается в том, чтобы зафиксиро- вать конец файла ввода. Обычно, когда функция 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); }

Программа извлекает символ , присваивает его переменной 'C' и затем проверяет, не является ли этот символ признаком конца файла. Если нет - выполняется тело оператора WHILE, выводящее этот символ. Затем цикл WHILE повторяется. когда, наконец, будет достигнут конец файла ввода, оператор WHILE завершается, а вместе с ним заканчивается выполнение и функ- ции MAIN .

В этой версии централизуется ввод - в программе только одно обращение к функции GETCHAR - и ужимается программа. Вложение присваивания в проверяемое условие - это одно из тех мест языка "C", которое приводит к значительному сокра- щению программ. Однако, на этом пути можно увлечься и начать писать недоступные для понимания программы. Эту тенденцию мы будем пытаться сдерживать. Важно понять , что круглые скобки вокруг присваивания в условном выражении действительно необходимы. Старшинство операции != выше, чем операции присваивания =, а это означа- ет, что в отсутствие круглых скобок проверка условия != бу- дет выполнена до присваивания =. Таким образом, оператор

C = GETCHAR() != EOF

эквивалентен оператору

C = (GETCHAR() != EOF)

Это, вопреки нашему желанию, приведет к тому, что 'C' будет принимать значение 0 или 1 в зависимости от того, на- толкнется или нет GETCHAR на признак конца файла. Подробнее об этом будет сказано в главе 2/.




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


Лексическая область действия идентификаторов, описанных во внешних определениях, простирается от определения до кон- ца исходного файла, в котором он находится. Лексическая об- ласть действия идентификаторов, являющихся формальными пара- метрами, распространяется на ту функцию, к которой они отно- сятся. Лексическая область действия идентификаторов, описан- ных в начале блока, простирается до конца этого блока. Лек- сической областью действия меток является та функция, в ко- торой они находятся. Поскольку все обращения на один и тот же внешний иденти- фикатор обращаются к одному и тому же объекту (см. П. 19.2), Компилятор проверяет все описания одного и того же внешнего идентификатора на совместимость; в действительности их об- ласть действия распространяется на весь файл, в котором они находятся. Во всех случаях, однако, есть некоторый идентификатор, явным образом описан в начале блока, включая и блок, который образует функцию, то действие любого описания этого иденти- фикатора вне блока приостанавливается до конца этого блока. Напомним также (п. 16.5), Что идентификаторы, соответст- вующие обычным переменным, с одной стороны, и идентификато- ры, соответствующие членам и ярлыкам структур и объединений, с другой стороны, формируют два непересекающихся класса, ко- торые не вступают в противоречие. Члены и ярлыки подчиняются тем же самым правилам определения областей действия, как и другие идентификаторы. Имена, специфицируемые с помощью TYPEDEF, входят в тот же класс, что и обычные идентификато- ры. Они могут быть переопределены во внутренних блоках, но во внутреннем описании тип должен быть указан явно:

TYPEDEF FLOAT DISTANCE; ... \( AUTO INT DISTANCE; ...

Во втором описании спецификатор типа INT должен присутство- вать, так как в противном случае это описание будет принято за описание без описателей с типом DISTANCE (прим. Автора: согласитесь, что лед здесь тонок.).



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


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



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


Выражение-логического-и: выражение && выражение

Операция && группируется слева направо. Она возвращает 1, если оба ее операнда отличны от нуля, и 0 в противном слу- чае. В отличие от & операция && гарантирует вычисление слева направо; более того, если первый операнд равен 0, то значе- ние второго операнда вообще не вычисляется. Операнды не обязаны быть одинакового типа, но каждый из них должен быть либо одного из основных типов, либо указате- лем. результат всегда имеет тип ITN.



Макроподстановка


Определение вида

#DEFINE TES 1

приводит к макроподстановке самого простого вида - замене имени на строку символов. Имена в #DEFINE имеют ту же самую форму, что и идентификаторы в "с"; заменяющий текст совер- шенно произволен. Нормально заменяющим текстом является ос- тальная часть строки; длинное определение можно продолжить, поместив \ в конец продолжаемой строки. "Область действия" имени, определенного в #DEFINE, простирается от точки опре- деления до конца исходного файла. имена могут быть переопре- делены, и определения могут использовать определения, сде- ланные ранее. Внутри заключенных в кавычки строк подстановки не производятся, так что если, например, YES - определенное имя, то в PRINTF("YES") не будет сделано никакой подстанов- ки. Так как реализация #DEFINE является частью работы маKропредпроцессора, а не собственно компилятора, имеется очень мало грамматических ограничений на то, что может быть определено. Так, например, любители алгола могут объявить

#DEFINE THEN #DEFINE BEGIN { #DEFINE END ;}

и затем написать

IF (I > 0) THEN BEGIN A = 1; B = 2 END

Имеется также возможность определения макроса с аргумен- тами, так что заменяющий текст будет зависеть от вида обра- щения к макросу. Определим, например, макрос с именем MAX следующим образом:

#DEFINE MAX(A, B) ((A) > (B) ? (A) : (B))

когда строка

X = MAX(P+Q, R+S);

будет заменена строкой

X = ((P+Q) > (R+S) ? (P+Q) : (R+S));

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

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

#DEFINE SQUARE(X) X * X

при обращении к ней, как SQUARE(Z+1)). Здесь возникают даже некоторые чисто лексические проблемы: между именем макро и левой круглой скобкой, открывающей список ее аргументов, не должно быть никаких пробелов. Тем не менее аппарат макросов является весьма ценным. Один практический пример дает описываемая в главе 7 стандар- тная библиотека ввода-вывода, в которой GETCHAR и PUTCHAR определены как макросы (очевидно PUTCHAR должна иметь аргу- мент), что позволяет избежать затрат на обращение к функции при обработке каждого символа. Другие возможности макропроцессора описаны в приложении А.

Упражнение 4-9

--------------- Определите макрос SWAP(X, Y), который обменивает значе- ниями два своих аргумента типа INT. (В этом случае поможет блочная структура).



Массивы


Давайте напишем программу подсчета числа появлений каж- дой цифры, символов пустых промежутков/пробел, табуляции, новая строка/ и всех остальных символов. Конечно, такая за- дача несколько искусственна, но она позволит нам проиллюст- рировать в одной программе сразу несколько аспектов языка "C". Мы разбили вводимые символы на двенадцать категорий, и нам удобнее использовать массив для хранения числа появлений каждой цифры, а не десять отдельных переменных. Вот один из вариантов программы: 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'. К счастью, это верно для всех общепринятых наборов символов.


По определению перед проведением арифметических опера- ций, вовлекающих переменные типа 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.

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

----------------

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




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


По-видимому самым общим типом массива в "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); } 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"

то компилятор создает массив символов, содержащий символы этой строки, и заканчивает его символом \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 в обратном порядке. С ее помощью напишите программу, которая обратит каждую строку из файла ввода.




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


Структуры особенно подходят для управления массивами связанных переменных. Рассмотрим, например, программу подс- чета числа вхождений каждого ключевого слова языка "C". Нам нужен массив символьных строк для хранения имен и массив це- лых для подсчета. одна из возможностей состоит в использова- нии двух параллельных массивов KEYWORD и KEYCOUNT:

CHAR *KEYWORD [NKEYS]; INT KEYCOUNT [NKEYS];

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

CHAR *KEYWORD; INT KEYCOUNT;

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

STRUCT KEY \( CHAR *KEYWORD; INT KEYCOUNT; \) KEYTAB [NKEYS];

оперделяет массив KEYTAB структур такого типа и отводит для них память. Каждый элемент массива является структурой. Это можно было бы записать и так:

STRUCT KEY \( CHAR *KEYWORD; INT KEYCOUNT; \); STRUCT KEY KEYTAB [NKEYS];

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

STRUCT KEY \( CHAR *KEYWORD; INT KEYCOUNT; \) KEYTAB[] =\( "BREAK", 0, "CASE", 0, "CHAR", 0, "CONTINUE", 0, "DEFAULT", 0, /* ... */ "UNSIGNED", 0, "WHILE", 0 \);

Инициализаторы перечисляются парами соответственно членам структуры. Было бы более точно заключать в фигурные скобки инициализаторы для каждой "строки" или структуры следующим образом:

\( "BREAK", 0 \), \( "CASE", 0 \), . . .

Но когда инициализаторы являются простыми переменными или символьными строками и все они присутствуют, то во внутрен- них фигурных скобках нет необходимости. Как обычно, компиля- тор сам вычислит число элементов массива KEYTAB, если иници- ализаторы присутствуют, а скобки [] оставлены пустыми. Программа подсчета ключевых слов начинается с определе- ния массива KEYTAB. ведущая программа читает свой файл вво- да, последовательно обращаясь к функции GETWORD, которая из- влекает из ввода по одному слову за обращение. Каждое слово ищется в массиве KEYTAB с помощью варианта функции бинарного поиска, написанной нами в главе 3. (Конечно, чтобы эта функ- ция работала, список ключевых слов должен быть расположен в порядке возрастания).


#DEFINE MAXWORD 20

MAIN() /* COUNT "C" KEYWORDS */ \( INT N, T; CHAR WORD[MAXWORD];

WHILE ((T = GETWORD(WORD,MAXWORD)) != EOF) IF (T == LETTER) IF((N = BINARY(WORD,KEYTAB,NKEYS)) >= 0) KEYTAB[N].KEYCOUNT++; FOR (N =0; N < NKEYS; N++) IF (KEYTAB[N].KEYCOUNT > 0) PRINTF("%4D %S\N", KEYTAB[N].KEYCOUNT, KEYTAB[N].KEYWORD); \) BINARY(WORD, TAB, N) /* FIND WORD IN TAB[0]...TAB[N-1] */ CHAR *WORD; STRUCT KEY TAB[]; INT N; \( INT LOW, HIGH, MID, COND;

LOW = 0; HIGH = N - 1; WHILE (LOW <= HIGH) \( MID = (LOW+HIGH) / 2; IF((COND = STRCMP(WORD, TAB[MID].KEYWORD)) < 0) HIGH = MID - 1; ELSE IF (COND > 0) LOW = MID + 1; ELSE RETURN (MID); \) RETURN(-1); \) Мы вскоре приведем функцию GETWORD; пока достаточно сказать, что она возвращает LETTER каждый раз, как она находит слово, и копирует это слово в свой первый аргумент.

Величина NKEYS - это количество ключевых слов в массиве KEYTAB . Хотя мы можем сосчитать это число вручную, гораздо легче и надежнее поручить это машине, особенно в том случае, если список ключевых слов подвержен изменениям. Одной из возможностей было бы закончить список инициализаторов указа- нием на нуль и затем пройти в цикле сквозь массив KEYTAB, пока не найдется конец. Но, поскольку размер этого массива полностью определен к моменту компиляции, здесь имеется более простая возможность. Число элементов просто есть

SIZE OF KEYTAB / SIZE OF STRUCT KEY

дело в том, что в языке "C" предусмотрена унарная операция SIZEOF, выполняемая во время компиляции, которая позволяет вычислить размер любого объекта. Выражение

SIZEOF(OBJECT)

выдает целое, равное размеру указанного объекта. (Размер оп- ределяется в неспецифицированных единицах, называемых "бай- тами", которые имеют тот же размер, что и переменные типа CHAR). Объект может быть фактической переменной, массивом и структурой, или именем основного типа, как INT или DOUBLE, или именем производного типа, как структура. В нашем случае число ключевых слов равно размеру массива, деленному на раз- мер одного элемента массива. Это вычисление используется в утверждении #DEFINE для установления значения NKEYS:



#DEFINE NKEYS (SIZEOF(KEYTAB) / SIZEOF(STRUCT KEY))

Теперь перейдем к функции GETWORD. Мы фактически написа- ли более общий вариант функции GETWORD, чем необходимо для этой программы, но он не на много более сложен. Функция GETWORD возвращает следующее "слово" из ввода, где словом считается либо строка букв и цифр, начинающихся с буквы, ли- бо отдельный символ. Тип объекта возвращается в качетве зна- чения функции; это - LETTER, если найдено слово, EOF для конца файла и сам символ, если он не буквенный.

GETWORD(W, LIM) /* GET NEXT WORD FROM INPUT */ CHAR *W; INT LIM; \( INT C, T; IF (TYPE(C=*W++=GETCH()) !=LETTER) \( *W='\0'; RETURN(C); \)

WHILE (--LIM > 0) \( T = TYPE(C = *W++ = GETCH()); IF (T ! = LETTER && T ! = DIGIT) \( UNGETCH(C); BREAK; \) \) *(W-1) - '\0'; RETURN(LETTER); \)

Функция GETWORD использует функции GETCH и UNGETCH, которые мы написали в главе 4: когда набор алфавитных символов пре- рывается, функция GETWORD получает один лишний символ. В ре- зультате вызова UNGETCH этот символ помещается назад во ввод для следующего обращения. Функция GETWORD обращается к функции TYPE для определе- ния типа каждого отдельного символа из файла ввода. Вот ва- риант, справедливый только для алфавита ASCII.

TYPE(C) /* RETURN TYPE OF ASCII CHARACTER */ INT C; \( IF (C>= 'A' && C<= 'Z' \!\! C>= 'A' && C<= 'Z') RETURN(LETTER); ELSE IF (C>= '0' && C<= '9') RETURN(DIGIT); ELSE RETURN(C); \)

Символические константы LETTER и DIGIT могут иметь любые значения, лишь бы они не вступали в конфликт с символами, отличными от буквенно-цифровых, и с EOF; очевидно возможен следующий выбор

#DEFINE LETTER 'A' #DEFINE DIGIT '0'

функция GETWORD могла бы работать быстрее, если бы обращения к функции TYPE были заменены обращениями к соответствующему массиву TYPE[ ]. В стандартной библиотеке языка "C" предус- мотрены макросы ISALPHA и ISDIGIT, действующие необходимым образом.

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

-------------- Сделайте такую модификацию функции GETWORD и оцените, как изменится скорость работы программы.

Упражнение 6-2

-------------- Напишите вариант функции TYPE, не зависящий от конкрет- ного наборасимволов.

Упражнение 6-3

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




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


Так как указатели сами являются переменными, то вы впол- не могли бы ожидать использования массива указателей. Это действительно так. Мы проиллюстрируем это написанием прог- раммы сортировки в алфавитном порядке набора текстовых строк, предельно упрощенного варианта утилиты SORT операци- онной систем UNIX. В главе 3 мы привели функцию сортировки по шеллу, кото- рая упорядочивала массив целых. Этот же алгоритм будет рабо- тать и здесь, хотя теперь мы будем иметь дело со строчками текста различной длины, которые, в отличие от целых, нельзя сравнивать или перемещать с помощью одной операции. Мы нуж- даемся в таком представлении данных, которое бы позволяло удобно и эффективно обрабатывать строки текста переменной длины. Здесь и возникают массивы указателей. Если подлежащие сортировке сроки хранятся одна за другой в длинном символь- ном массиве (управляемом, например, функцией ALLOC), то к каждой строке можно обратиться с помощью указателя на ее первый символ. Сами указатели можно хранить в массиве. две строки можно сравнить, передав их указатели функции STRCMP.

Если две расположенные в неправильном порядке строки должны быть переставлены, то фактически переставляются указатели в массиве указателей, а не сами тексты строк. Этим исключаются сразу две связанные проблемы: сложного управления памятью и больших дополнительных затрат на фактическую перестановку строк. Процесс сортировки включает три шага:

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

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


#DEFINE NULL 0 #DEFINE LINES 100 /* MAX LINES TO BE SORTED */

MAIN() /* SORT INPUT LINES */ \( CHAR *LINEPTR[LINES]; /*POINTERS TO TEXT LINES */ INT NLINES; /* NUMBER OF INPUT LINES READ */

IF ((NLINES = READLINES(LINEPTR, LINES)) >= 0) \( SORT(LINEPTR, NLINES); WRITELINES(LINEPTR, NLINES); \) ELSE PRINTF("INPUT TOO BIG TO SORT\N"); \)

#DEFINE MAXLEN 1000

READLINES(LINEPTR, MAXLINES) /* READ INPUT LINES */ CHAR *LINEPTR[]; /* FOR SORTING */ INT MAXLINES; \( INT LEN, NLINES; CHAR *P, *ALLOC(), LINE[MAXLEN];

NLINES = 0; WHILE ((LEN = GETLINE(LINE, MAXLEN)) > 0) IF (NLINES >= MAXLINES) RETURN(-1); ELSE IF ((P = ALLOC(LEN)) == NULL) RETURN (-1); ELSE \( LINE[LEN-1] = '\0'; /* ZAP NEWLINE */ STRCPY(P,LINE); LINEPTR[NLINES++] = P; \) RETURN(NLINES); \)

Символ новой строки в конце каждой строки удаляется, так что он никак не будет влиять на порядок, в котором сортируются строки.

WRITELINES(LINEPTR, NLINES) /* WRITE OUTPUT LINES */ CHAR *LINEPTR[]; INT NLINES; \( INT I;

FOR (I = 0; I < NLINES; I++) PRINTF("%S\N", LINEPTR[I]); \)

Существенно новым в этой программе является описание

CHAR *LINEPTR[LINES];

которое сообщает, что LINEPTR является массивом из LINES элементов, каждый из которых - указатель на переменные типа CHAR. Это означает, что LINEPTR[I] - указатель на символы, а *LINEPTR[I] извлекает символ. Так как сам LINEPTR является массивом, который передает- ся функции WRITELINES, с ним можно обращаться как с указате- лем точно таким же образом, как в наших более ранних приме-

рах. Тогда последнюю функцию можно переписать в виде:

WRITELINES(LINEPTR, NLINES) /* WRITE OUTPUT LINES */ CHAR *LINEPTR[]; INT NLINES; \( INT I;

WHILE (--NLINES >= 0) PRINTF("%S\N", *LINEPTR++); \)

здесь *LINEPTR сначала указывает на первую строку; каждое увеличение передвигает указатель на следующую строку, в то время как NLINES убывает до нуля. Справившись с вводом и выводом, мы можем перейти к сор- тировке. программа сортировки по шеллу из главы 3 требует очень небольших изменений: должны быть модифицированы описа- ния, а операция сравнения выделена в отдельную функцию. Ос- новной алгоритм остается тем же самым, и это дает нам опре- деленную уверенность, что он по-прежнему будет работать.



SORT(V, N) /* SORT STRINGS V[0] ... V[N-1] */ CHAR *V[]; /* INTO INCREASING ORDER */ INT N; \( INT GAP, I, J; CHAR *TEMP;

FOR (GAP = N/2; GAP > 0; GAP /= 2) FOR (I = GAP; I < N; I++) FOR (J = I - GAP; J >= 0; J -= GAP) \( IF (STRCMP(V[J], V[J+GAP]) <= 0) BREAK; TEMP = V[J]; V[J] = V[J+GAP]; V[J+GAP] = TEMP; \) \)

Так как каждый отдельный элемент массива V (имя формального параметра, соответствующего LINEPTR) является указателем на символы, то и TEMP должен быть указателем на символы, чтобы их было можно копировать друг в друга. Мы написали эту программу по возможности более просто с тем, чтобы побыстрее получить работающую программу. Она мог- ла бы работать быстрее, если, например, вводить строки не- посредственно в массив, управляемый функцией READLINES, а не копировать их в LINE, а затем в скрытое место с помощью фун-

кции ALLOC. но мы считаем, что будет разумнее первоначальный вариант сделать более простым для понимания, а об "эффектив- ности" позаботиться позднее. Все же, по-видимому, способ, позволяющий добиться заметного ускорения работы программы состоит не в исключении лишнего копирования вводимых строк. Более вероятно, что существенной разницы можно достичь за счет замены сортировки по шеллу на нечто лучшее, например, на метод быстрой сортировки. В главе 1 мы отмечали, что поскольку в циклах WHILE и FOR проверка осуществляется до того, как тело цикла выпол- нится хотя бы один раз, эти циклы оказываются удобными для обеспечения правильной работы программы при граничных значе- ниях, в частности, когда ввода вообще нет. Очень полезно просмотреть все функции программы сортировки, разбираясь, что происходит, если вводимый текст отсутствует.

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

-------------- Перепишите функцию READLINES таким образом, чтобы она помещала строки в массив, предоставляемый функцией MAIN, а не в память, управляемую обращениями к функции ALLOC. На- сколько быстрее стала программа?




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


Каждый раз, когда идентификатор, имеющий тип массива, появляется в выражении, он преобразуется в указатель на пер- вый член этого массива. Из-за этого преобразования массивы не являются L-значениями. По определению операция индексация [] интерпретируется таким образом, что E1[E2] считается идентичным выражению *((е1)+(е2)). Согласно правилам преоб- разований, применяемым при операции +, если E1 - массив, а е2 - целое, то е1[е2] ссылается на е2-й член массива е1. По- этому несмотря на несимметричный вид операция индексации яв- ляется коммутативной. В случае многомерных массивов применяется последователь- ное правило. Если е является N-мерным массивом размера I*J*...*K, то при появлении в выражении е преобразуется в указатель на (N-1)-мерный массив размера J*...*K. Если опе- рация * либо явно, либо неявно, как результат индексации, применяется к этому указателю, то результатом операции будет указанный (N-1)-мерный массив, который сам немедленно преоб- разуется в указатель. Рассмотрим, например, описание

INT X[3][5];

Здесь X массив целых размера 3*5. При появлении в выражении X преобразуется в указатель на первый из трех массивов из 5 целых. В выражении X[I], которое эквивалентно *(X+I), снача- ла X преобразуется в указатель так, как описано выше; затем I преобразуется к типу X, что вызывает умножение I на длину объекта, на который указывает указатель, а именно на 5 целых объектов. Результаты складываются, и применение косвенной адресации дает массив (из 5 целых), который в свою очередь преобразуется в указатель на первое из этих целых. Если в выражение входит и другой индекс, то таже самая аргументация применяется снова; результатом на этот раз будет целое. Из всего этого следует, что массивы в языке "C" хранятся построчно ( последний индекс изменяется быстрее всего) и что первый индекс в описании помогает определить общее количест- во памяти, требуемое для хранения массива, но не играет ни- какой другой роли в вычислениях, связанных с индексацией.



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


В языке "C" предусмотрены прямоугольные многомерные мас- сивы, хотя на практике существует тенденция к их значительно более редкому использованию по сравнению с массивами указа- телей. В этом разделе мы рассмотрим некоторые их свойства. Рассмотрим задачу преобразования дня месяца в день года и наоборот. Например, 1-ое марта является 60-м днем невисо- косного года и 61-м днем високосного года. Давайте введем две функции для выполнения этих преобразований: DAY_OF_YEAR преобразует месяц и день в день года, а MONTH_DAY преобразу- ет день года в месяц и день. Так как эта последняя функция возвращает два значения, то аргументы месяца и дня должны быть указателями:

MONTH_DAY(1977, 60, &M, &D)

Полагает M равным 3 и D равным 1 (1-ое марта). Обе эти функции нуждаются в одной и той же информацион- ной таблице, указывающей число дней в каждом месяце. Так как число дней в месяце в високосном и в невисокосном году отли- чается, то проще представить их в виде двух строк двумерного массива, чем пытаться прослеживать во время вычислений, что именно происходит в феврале. Вот этот массив и выполняющие эти преобразования функции:

STATIC INT DAY_TAB[2][13] = { (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31), (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) };

DAY_OF_YEAR(YEAR, MONTH, DAY) /* SET DAY OF YEAR */ INT YEAR, MONTH, DAY; /* FROM MONTH & DAY */ { INT I, LEAP; LEAP = YEAR%4 == 0 && YEAR%100 != 0 \!\! YEAR%400 == 0; FOR (I = 1; I < MONTH; I++) DAY += DAY_TAB[LEAP][I]; RETURN(DAY); {

MONTH_DAY(YEAR, YEARDAY, PMONTH, PDAY) /*SET MONTH,DAY */ INT YEAR, YEARDAY, *PMONTH, *PDAY; /* FROM DAY OF YEAR */ { LEAP = YEAR%4 == 0 && YEAR%100 != 0 \!\! YEAR%400 == 0; FOR (I = 1; YEARDAY > DAY_TAB[LEAP][I]; I++) YEARDAY -= DAY_TAB[LEAP][I]; *PMONTH = I; *PDAY = YEARDAY; }

Массив DAY_TAB должен быть внешним как для DAY_OF_YEAR, так и для MONTH_DAY, поскольку он используется обеими этими фун- кциями. Массив DAY_TAB является первым двумерным массивом, с ко- торым мы имеем дело. По определению в "C" двумерный массив по существу является одномерным массивом, каждый элемент ко- торого является массивом. Поэтому индексы записываются как


DAY_TAB[I][J] а не DAY_TAB [I, J]

как в большинстве языков. В остальном с двумерными массивами можно в основном обращаться таким же образом, как в других языках. Элементы хранятся по строкам, т.е. При обращении к элементам в порядке их размещения в памяти быстрее всего из- меняется самый правый индекс. Массив инициализируется с помощью списка начальных зна- чений, заключенных в фигурные скобки; каждая строка двумер- ного массива инициализируется соответствующим подсписком. Мы поместили в начало массива DAY_TAB столбец из нулей для то- го, чтобы номера месяцев изменялись естественным образом от 1 до 12, а не от 0 до 11. Так как за экономию памяти у нас пока не награждают, такой способ проще, чем подгонка индек- сов. Если двумерный массив передается функции, то описание соответствующего аргумента функции должно содержать количес- тво столбцов; количество строк несущественно, поскольку, как и прежде, фактически передается указатель. В нашем конкрет- ном случае это указатель объектов, являющихся массивами из

13 чисел типа INT. Таким образом, если бы требовалось пере- дать массив DAY_TAB функции F, то описание в F имело бы вид:

F(DAY_TAB) INT DAY_TAB[2][13]; { ... }

Так как количество строк является несущественным, то описа- ние аргумента в F могло бы быть таким:

INT DAY_TAB[][13];

или таким

INT (*DAY_TAB)[13];

в которм говорится, что аргумент является указателем массива из 13 целых. Круглые скобки здесь необходимы, потому что квадратные скобки [] имеют более высокий уровень старшинст- ва, чем *; как мы увидим в следующем разделе, без круглых скобок

INT *DAY_TAB[13];

является описанием массива из 13 указателей на целые.




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


Мультипликативные операции *, /, и % группируются слева направо. Выполняются обычные арифметические преобразования. Мультипликативное-выражение: выражение * выражение выражение / выражение выражение % выражение Бинарная операция * означает умножение. Операция * ассо- циативна, и выражения с несколькими умножениями на одном и том же уровне могут быть перегруппированы компилятором. Бинарная операция / означает деление. При делении поло- жительных целых осуществляется усечение по направлению к ну- лю, но если один из операндов отрицателен, то форма усечения зависит от используемой машины. На всех машинах, охватывае- мых настоящим руководством, остаток имеет тот же знак , что и делимое. Всегда справедливо, что (A/B)*B+A%B равно A (если B не равно 0). Бинарная операция % выдает остаток от деления первого выражения на второе. Выполняются обычные арифметические пре- образования. Операнды не должны быть типа FLOAT.



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


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



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


Не всегда является необходимым специфицировать и класс памяти и тип идентификатора в описании. Во внешних определе- ниях и описаниях формальных параметров и членов структур класс памяти определяется по контексту. Если в находящемся внутри функции описании не указан тип, а только класс памя- ти, то предполагается, что идентификатор имеет тип INT; если не указан класс памяти, а только тип, то идентификатор пред- полагается описанным как AUTO. Исключение из последнего пра- вила дается для функций, потому что спецификатор AUTO для функций является бессмысленным (язык "C" не в состоянии ком- пилировать программу в стек); если идентификатор имеет тип "функция, возвращающая ...", то он предполагается неявно описанным как EXTERN.

Входящий в выражение и неописанный ранее идентификатор, за которым следует скобка ( , считается описанным по контек- сту как "функция, возвращающая INT".



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


Стандартная библиотека предоставляет множество разнооб- разных функций, некоторые из которых оказываются особенно полезными. Мы уже упоминали функции для работы со строками: STRLEN, STRCPY, STRCAT и STRCMP. Вот некоторые другие.



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


Самый низкий уровень ввода/вывода в системе UNIX не пре- дусматривает ни какой-либо буферизации, ни какого-либо дру- гого сервиса; он по существу является непосредственным вхо- дом в операционную систему. Весь ввод и вывод осуществляется двумя функциями: READ и WRITE. Первым аргументом обеих функ- ций является дескриптор файла. Вторым аргументом является буфер в вашей программе, откуда или куда должны поступать данные. Третий аргумент - это число подлежащих пересылке байтов. Обращения к этим функциям имеют вид:

N_READ=READ(FD,BUF,N); N_WRITTEN=WRITE(FD,BUF,N);

При каждом обращении возвращается счетчик байтов, указываю- щий фактическое число переданных байтов. При чтении возвра- щенное число байтов может оказаться меньше, чем запрошенное число. Возвращенное нулевое число байтов означает конец фай- ла, а "-1" указывает на наличие какой-либо ошибки. При запи- си возвращенное значение равно числу фактически записанных байтов; несовпадение этого числа с числом байтов, которое предполагалось записать, обычно свидетельствует об ошибке. Количество байтов, подлежащих чтению или записи, может быть совершенно произвольным. Двумя самыми распространенными величинами являются "1", которая означает передачу одного символа за обращение (т.е. Без использования буфера), и "512", которая соответствует физическому размеру блока на многих периферийных устройствах. Этот последний размер будет наиболее эффективным, но даже ввод или вывод по одному сим- волу за обращение не будет необыкновенно дорогим. Объединив все эти факты, мы написали простую программу для копирования ввода на вывод, эквивалентную программе ко- пировки файлов, написанной в главе 1. На системе UNIX эта программа будет копировать что угодно куда угодно, потому что ввод и вывод могут быть перенаправлены на любой файл или устройство. #DEFINE BUFSIZE 512 /*BEST SIZE FOR PDP-11 UNIX*/ MAIN() /*COPY INPUT TO OUTPUT*/ \( CHAR BUF[BUFSIZE]; INT N; WHILE((N=READ(0,BUF,BUFSIZE))>0) WRITE(1,BUF,N); \)


Если размер файла не будет кратен BUFSIZE, то при некотором обращении к READ будет возвращено меньшее число байтов, ко- торые затем записываются с помощью WRITE; при следующем пос- ле этого обращении к READ будет возвращен нуль. Поучительно разобраться, как можно использовать функции READ и WRITE для построения процедур более высокого уровня, таких как GETCHAR, PUTCHAR и т.д. Вот, например, вариант функции GETCHAR, осуществляющий ввод без использования буфе- ра.

#DEFINE CMASK 0377 /*FOR MAKING CHAR'S > 0*/ GETCHAR() /*UNBUFFERED SINGLE CHARACTER INPUT*/ \( CHAR C; RETURN((READ(0,&C,1)>0 7 & CMASK : EOF); \)

Переменная "C" должна быть описана как CHAR, потому что фун- кция READ принимает указатель на символы. Возвращаемый сим- вол должен быть маскирован числом 0377 для гарантии его по- ложительности; в противном случае знаковый разряд может сде- лать его значение отрицательным. (Константа 0377 подходит для эвм PDP-11, но не обязательно для других машин). Второй вариант функции GETCHAR осуществляет ввод больши- ми порциями, а выдает символы по одному за обращение.

#DEFINE CMASK 0377 /*FOR MAKING CHAR'S>0*/ #DEFINE BUFSIZE 512 GETCHAR() /*BUFFERED VERSION*/ \( STATIC CHAR BUF[BUFSIZE]; STATIC CHAR *BUFP = BUF; STATIC INT N = 0; IF (N==0) \( /*BUFFER IS EMPTY*/ N=READ(0,BUF,BUFSIZE); BUFP = BUF; \) RETURN((--N>=0) ? *BUFP++ & CMASK : EOF); \)




Объединения


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

UNION U_TAG \( INT IVAL; FLOAT FVAL; CHAR *PVAL; \) UVAL;

Переменная UVAL будет иметь достаточно большой размер,чтобы хранить наибольший из трех типов, независимо от машины, на которой осуществляется компиляция, - программа не будет за- висить от характеристик аппаратных средств. Любой из этих трех типов может быть присвоен UVAR и затем использован в выражениях, пока такое использование совместимо: извлекаемый тип должен совпадать с последним помещенным типом. Дело программиста - следить за тем, какой тип хранится в объеди- нении в данный момент; если что-либо хранится как один тип, а извлекается как другой, то результаты будут зависеть от используемой машины.

Синтаксически доступ к членам объединения осуществляется следующим образом:

имя объединения.член -------------------- или указатель объединения ->член ----------------------------

то есть точно так же, как и в случае структур. если для отс- леживания типа, хранимого в данный момент в UVAL, использу- ется переменная UTYPE, то можно встретить такой участок программы:


IF (UTYPE == INT) PRINTF("%D\N", UVAL.IVAL); ELSE IF (UTYPE == FLOAT) PRINTF("%F\N", UVAL.FVAL); ELSE IF (UTYPE == STRING) PRINTF("%S\N", UVAL.PVAL); ELSE PRINTF("BAD TYPE %D IN UTYPE\N", UTYPE);

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

STRUCT \( CHAR *NAME; INT FLAGS; INT UTYPE; UNION \( INT IVAL; FLOAT FVAL; CHAR *PVAL; \) UVAL; \) SYMTAB[NSYM];

на переменную IVAL можно сослаться как

SYMTAB[I].UVAL.IVAL

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

*SYMTAB[I].UVAL.PVAL

В сущности объединение является структурой, в которой все члены имеют нулевое смещение. Сама структура достаточно ве- лика, чтобы хранить "самый широкий" член, и выравнивание пригодно для всех типов, входящих в объединение. Как и в случае структур, единственными операциями, которые в настоя- щее время можно проводить с объединениями, являются доступ к

члену и извлечение адреса; объединения не могут быть присво- ены, переданы функциям или возвращены ими. указатели объеди- нений можно использовать в точно такой же манере, как и ука- затели структур. Программа распределения памяти, приводимая в главе 8 , показывает, как можно использовать объединение, чтобы сде- лать некоторую переменную выровненной по определенному виду границы памяти.




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


Объект является доступным обработке участком памяти; L-значение - это выражение, ссылающееся на объект. Очевидным примером выражения L-значения является идентификатор. Сущес- твуют операции, результатом которых являются L-значения; ес- ли, например, E - выражение указанного типа, то *E является выражением L-значения, ссылающимся на объект E. Название "L-значение" происходит от выражения присваивания E1=E2, в котором левая часть должна быть выражением L-значения. При последующем обсуждении каждой операции будет указываться, ожидает ли она операндов L-значения и выдает ли она L-значе- ние.