Язык С

         

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


Областью действия имени является та часть программы, в которой это имя определено. Для автоматической переменной, описанной в начале функции, областью действия является та функция, в которой описано имя этой переменной, а переменные из разных функций, имеющие одинаковое имя, считаются не от- носящимися друг к другу. Это же справедливо и для аргументов функций. Область действия внешней переменной простирается от точ- ки, в которой она объявлена в исходном файле, до конца этого файла. Например, если VAL, SP, PUSH, POP и CLEAR определены в одном файле в порядке, указанном выше, а именно:

INT SP = 0; DOUBLE VAL[MAXVAL];

DOUBLE PUSH(F) {...}

DOUBLE POP() {...}

CLEAR() {...}

то переменные VAL и SP можно использовать в PUSH, POP и CLEAR прямо по имени; никакие дополнительные описания не нужны. С другой стороны, если нужно сослаться на внешнюю пере- менную до ее определения, или если такая переменная опреде- лена в файле, отличном от того, в котором она используется, то необходимо описание EXTERN.

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

INT SP; DOUBLE VAL[MAXVAL];

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

EXTERN INT SP; EXTERN DOUBLE VAL[];

описывают в остальной части этого исходного файла переменную SP как INT, а VAL как массив типа DOUBLE /размер которого указан в другом месте/, но не создают переменных и не отво- дят им места в памяти. Во всех файлах, составляющих исходную программу, должно содержаться только одно определение внешней переменной; дру- гие файлы могут содержать описания EXTERN для доступа к ней. /Описание EXTERN может иметься и в том файле, где находится определение/. Любая инициализация внешней переменной прово- дится только в определении. В определении должны указываться размеры массивов, а в описании EXTERN этого можно не делать. Хотя подобная организация приведенной выше программы и маловероятна, но VAL и SP могли бы быть определены и инициа- лизированы в одном файле, а функция PUSH, POP и CLEAR опре- делены в другом. В этом случае для связи были бы необходимы следующие определения и описания:


в файле 1: ----------

INT SP = 0; /* STACK POINTER */ DOUBLE VAL[MAXVAL]; /* VALUE STACK */

в файле 2: ----------

EXTERN INT SP; EXTERN DOUBLE VAL[];

DOUBLE PUSH(F) {...}

DOUBLE POP() {...}

CLEAR() {...}

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



Для программ большого размера обсуждаемая позже в этой главе возможность включения файлов, #INCLUDE, позволяет иметь во всей программе только одну копию описаний EXTERN и вставлять ее в каждый исходный файл во время его компиляции. Обратимся теперь к функции GETOP, выбирающей из файла ввода следующую операцию или операнд. Основная задача прос- та: пропустить пробелы, знаки табуляции и новые строки. Если следующий символ отличен от цифры и десятичной точки, то возвратить его. В противном случае собрать строку цифр /она может включать десятичную точку/ и возвратить NUMBER как сигнал о том, что выбрано число. Процедура существенно усложняется, если стремиться пра- вильно обрабатывать ситуацию, когда вводимое число оказыва- ется слишком длинным. Функция GETOP считывает цифры подряд /возможно с десятичной точкой/ и запоминает их, пока после- довательность не прерывается. Если при этом не происходит переполнения, то функция возвращает NUMBER и строку цифр. Если же число оказывается слишком длинным, то GETOP отбрасы- вает остальную часть строки из файла ввода, так что пользо- ватель может просто перепечатать эту строку с места ошибки; функция возвращает TOOBIG как сигнал о переполнении.

GETOP(S, LIM) /* GET NEXT OPRERATOR OR OPERAND */ CHAR S[]; INT LIM; { INT I, C;

WHILE((C=GETCH())==' '\!\! C=='\T' \!\! C=='\N') ; IF (C != '.' && (C < '0' \!\! C > '9')) RETURN(C); S[0] = C; FOR(I=1; (C=GETCHAR()) >='0' && C <= '9'; I++) IF (I < LIM)

S[I] = C; IF (C == '.') { /* COLLECT FRACTION */ IF (I < LIM) S[I] = C; FOR(I++;(C=GETCHAR()) >='0' && C<='9';I++) IF (I < LIM) S[I] =C; } IF (I < LIM) { /* NUMBER IS OK */ UNGETCH(C); S[I] = '\0'; RETURN (NUMBER);



} ELSE { /* IT' S TOO BIG; SKIP REST OF LINE */ WHILE (C != '\N' && C != EOF) C = GETCHAR(); S[LIM-1] = '\0'; RETURN (TOOBIG); } }

Что же представляют из себя функции 'GETCH' и 'UNGETCH'? Часто так бывает, что программа, считывающая входные данные, не может определить, что она прочла уже достаточно, пока она не прочтет слишком много. Одним из примеров является выбор символов, составляющих число: пока не появится символ, от- личный от цифры, число не закончено. Но при этом программа считывает один лишний символ, символ, для которого она еще не подготовлена. Эта проблема была бы решена, если бы было бы возможно "прочесть обратно" нежелательный символ. Тогда каждый раз, прочитав лишний символ, программа могла бы поместить его об- ратно в файл ввода таким образом, что остальная часть прог- раммы могла бы вести себя так, словно этот символ никогда не считывался. к счастью, такое неполучение символа легко имми- тировать, написав пару действующих совместно функций. Функ- ция GETCH доставляет следующий символ ввода, подлежащий рас- смотрению; функция UNGETCH помещает символ назад во ввод, так что при следующем обращении к GETCH он будет возвращен. То, как эти функции совместно работают, весьма просто. Функция UNGETCH помещает возвращаемые назад символы в сов- местно используемый буфер, являющийся символьным массивом. Функция GETCH читает из этого буфера, если в нем что-либо имеется; если же буфер пуст, она обращается к GETCHAR. При этом также нужна индексирующая переменная, которая будет фиксировать позицию текущего символа в буфере. Так как буфер и его индекс совместно используются функ- циями GETCH и UNGETCH и должны сохранять свои значения в пе- риод между обращениями, они должны быть внешними для обеих функций. Таким образом, мы можем написать GETCH, UNGETCH и эти переменные как:

#DEFINE BUFSIZE 100 CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */ INT BUFP = 0; /* NEXT FREE POSITION IN BUF */

GETCH() /* GET A (POSSIBLY PUSHED BACK) CHARACTER */ { RETURN((BUFP > 0) ? BUF[--BUFP] : GETCHAR()); }

UNGETCH(C) /* PUSH CHARACTER BACK ON INPUT */ INT C; { IF (BUFP > BUFSIZE) PRINTF("UNGETCH: TOO MANY CHARACTERS\N"); ELSE BUF [BUFP++] = C; }

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

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

---------------- Напишите функцию UNGETS(S) , которая будет возвращать во ввод целую строку. Должна ли UNGETS иметь дело с BUF и BUFP или она может просто использовать UNGETCH ?

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

---------------- Предположите, что может возвращаться только один символ. Из- мените GETCH и UNGETCH соответствующим образом.

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

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




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


Переменные в MAIN(LINE, SAVE и т.д.) являются внутренни- ми или локальными по отношению к функции MAIN, потому что они описаны внутри MAIN и никакая другая функция не имеет к ним прямого доступа. Это же верно и относительно переменных в других функциях; например, переменная I в функции GETLINE никак не связана с I в COPY. Каждая локальная переменная су- ществует только тогда, когда произошло обращение к соответс- твующей функции, и исчезает, как только закончится выполне- ние этой функции. По этой причине такие переменные, следуя терминологии других языков, обычно называют автоматическими. Мы впредь будем использовать термин автоматические при ссыл- ке на эти динамические локальные переменные. /в главе 4 об- суждается класс статической памяти, когда локальные перемен- ные все же оказываются в состоянии сохранить свои значения между обращениями к функциям/. Поскольку автоматические переменные появляются и исчеза- ют вместе с обращением к функции, они не сохраняют своих значений в промежутке от одного вызова до другого, в силу чего им при каждом входе нужно явно присваивать значения. Если этого не сделать, то они будут содержать мусор. В качестве альтернативы к автоматическим переменным мож- но определить переменные, которые будут внешними для всех функций, т.е. Глобальными переменными, к которым может обра- титься по имени любая функция, которая пожелает это сделать. (этот механизм весьма сходен с "COMMON" в фортране и "EXTERNAL" в PL/1). Так как внешние переменные доступны всю- ду, их можно использовать вместо списка аргументов для пере- дачи данных между функциями. Кроме того, поскольку внешние переменные существуют постоянно, а не появляются и исчезают вместе с вызываемыми функциями, они сохраняют свои значения и после того, как функции, присвоившие им эти значения, за- вершат свою работу. Внешняя переменная должна быть определена вне всех функ- ций; при этом ей выделяется фактическое место в памяти. Та- кая переменная должна быть также описана в каждой функции, которая собирается ее использовать; это можно сделать либо явным описанием EXTERN, либо неявным по контексту. Чтобы сделать обсуждение более конкретным, давайте перепишем прог- рамму поиска самой длинной строки, сделав LINE, SAVE и MAX внешними переменными. Это потребует изменения описаний и тел всех трех функций, а также обращений к ним.


#DEFINE MAXLINE 1000 /* MAX. INPUT LINE SIZE*/

CHAR LINE[MAXLINE]; /* INPUT LINE */ CHAR SAVE[MAXLINE];/* LONGEST LINE SAVED HERE*/ INT MAX;/* LENGTH OF LONGEST LINE SEEN SO FAR*/ MAIN() /*FIND LONGEST LINE; SPECIALIZED VERSION*/ { INT LEN; EXTERN INT MAX; EXTERN CHAR SAVE[]; MAX = 0;

WHILE ( (LEN = GETLINE()) > 0 ) IF ( LEN > MAX ) { MAX = LEN; COPY(); } IF ( MAX > 0 ) /* THERE WAS A LINE */ PRINTF( "%S", SAVE ); }

GETLINE() /* SPECIALIZED VERSION */ { INT C, I; EXTERN CHAR LINE[];

FOR (I = 0; I < MAXLINE-1

&& (C=GETCHAR()) !=EOF && C!='\N'; ++I) LINE[I] = C; ++I; } LINE[I] = '\0' RETURN(I) } COPY() /* SPECIALIZED VERSION */ { INT I; EXTERN CHAR LINE[], SAVE[];

I = 0; WHILE ((SAVE[I] = LINE[I]) !='\0') ++I; }

Внешние переменные для функций MAIN, GETLINE и COPY оп- ределены в первых строчках приведенного выше примера, кото- рыми указывается их тип и вызывается отведение для них памя- ти. синтаксически внешние описания точно такие же, как опи- сания, которые мы использовали ранее, но так как они распо- ложены вне функций, соответствующие переменные являются внешними. Чтобы функция могла использовать внешнюю переме- ную, ей надо сообщить ее имя. Один способ сделать это - включить в функцию описание EXTERN; это описание отличается от предыдущих только добавлением ключевого слова EXTERN.

В определенных ситуациях описание EXTERN может быть опу- щено: если внешнее определение переменной находится в том же исходном файле, раньше ее использования в некоторой конкрет- ной функции, то не обязательно включать описание EXTERN для этой переменной в саму функцию. Описания EXTERN в функциях MAIN, GETLINE и COPY являются, таким образом, излишними. Фактически, обычная практика заключается в помещении опреде- лений всех внешних переменных в начале исходного файла и последующем опускании всех описаний EXTERN. Если программа находится в нескольких исходных файлах, и некоторая переменная определена, скажем в файле 1, а исполь- зуется в файле 2, то чтобы связать эти два вхождения пере- менной, необходимо в файле 2 использовать описание EXTERN. Этот вопрос подробно обсуждается в главе 4. Вы должно быть заметили, что мы в этом разделе при ссыл- ке на внешние переменные очень аккуратно используем слова описание и определение. "Определение" относится к тому мес- ту, где переменная фактически заводится и ей выделяется па- мять; "описание" относится к тем местам, где указывается природа переменной, но никакой памяти не отводится. Между прочим, существует тенденция объявлять все, что ни попадется, внешними переменными, поскольку кажется, что это упрощает связи, - списки аргументов становятся короче и пе- ременные всегда присутствуют, когда бы вам они ни понадоби- лись. Но внешние переменные присутствуют и тогда, когда вы в них не нуждаетесь. Такой стиль программирования чреват опас- ностью, так как он приводит к программам, связи данных внут- ри которых не вполне очевидны. Переменные при этом могут из- меняться неожиданным и даже неумышленным образом, а програм- мы становится трудно модифицировать, когда возникает такая необходимость. Вторая версия программы поиска самой длинной строки уступает первой отчасти по этим причинам, а отчасти потому, что она лишила универсальности две весьма полезные функции, введя в них имена переменных, с которыми они будут манипулировать.

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

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




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


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

Появление ключевого слова EBTERN во внешнем определении указывает на то, что память для описанных в нем идентифика- торов будет выделена в другом файле. Следовательно, в состо- ящей из многих файлов программе внешнее определение иденти- фикатора, не содержащее спецификатора EXTERN, должно появ- ляться ровно в одном из этих файлов. любые другие файлы, ко- торые желают дать внешнее определение этого идентификатора, должны включать в это определение слово EXTERN. Идентифика- тор может быть инициализирован только в том описании, кото- рое приводит к выделению памяти. Идентификаторы, внешнее определение которых начинается со слова STATIC, недоступны из других файлов. Функции могут быть описаны как STATIC.



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


Обработка ошибок в CAT неидеальна. Неудобство заключает- ся в том, что если один из файлов по некоторой причине ока- зывается недоступным, диагностическое сообщение об этом пе- чатается в конце объединенного вывода. Это приемлемо, если вывод поступает на терминал, но не годится, если вывод пос- тупает в некоторый файл или через поточный (PIPELINE) меха- низм в другую программу. Чтобы лучше обрабатывать такую ситуацию, к программе точно таким же образом, как STDIN и STDOUT, присоединяется второй выходной файл, называемый STDERR. Если это вообще возможно, вывод, записанный в файле STDERR, появляется на терминале пользователя, даже если стандартный вывод направ- ляется в другое место. Давайте переделаем программу 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(STDERR, "CAT: CAN'T OPEN,%S\N", ARGV); EXIT(1); \) ELSE \( FILECOPY(FP); \) EXIT(0); \)

Программа сообщает об ошибках двумя способами. Диагностичес- кое сообщение, выдаваемое функцией FPRINTF, поступает в STDERR и, таким образом, оказывается на терминале пользова- теля, а не исчезает в потоке (PIPELINE) или в выходном фай- ле. Программа также использует функцию EXIT из стандартной библиотеки, обращение к которой вызывает завершение выполне- ния программы. Аргумент функции EXIT доступен любой програм- ме, обращающейся к данной функции, так что успешное или неу- дачное завершение данной программы может быть проверено дру- гой программой, использующей эту в качестве подзадачи. По соглашению величина 0 в качетсве возвращаемого значения сви- детельствует о том, что все в порядке, а различные ненулевые значения являются признаками нормальных ситуаций.

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



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


Функция SYSTEM(S) выполняет команду, содержащуюся в сим- вольной строке S, и затем возобновляет выполнение текущей программы. Содержимое S сильно зависит от используемой опе- рационной системы. В качестве тривиального примера, укажем, что на системе UNIX строка

SYSTEM("DATE");

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



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


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

#INCLUDE <STDIO.H>

в файле STDIO.H определяются некоторые макросы и переменные, используемые библиотекой ввода/вывода. Использование угловых скобок вместо обычных двойных кавычек - указание компилятору искать этот файл в справочнике, содержащем заголовки стан- дартной информации (на системе UNIX обычно LUSRLINELUDE). Кроме того, при загрузке программы может оказаться необ- ходимым указать библиотеку явно; на системе PDP-11 UNIX, например, команда компиляции программы имела бы вид:

CC исходные файлы и т.д. -LS

где -LS указывает на загрузку из стандартной библиотеки.



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


Такие выражения, как

I = I + 2

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

I += 2

используя операцию присваивания вида +=. Большинству бинарных операций (операций подобных +, ко- торые имеют левый и правый операнд) соответствует операция присваивания вида оп=, где оп - одна из операций

+ - * / % << >> & \^ \!

Если е1 и е2 - выражения, то е1 оп= е2

эквивалентно

е1 = (е1) оп (е2)

за исключением того, что выражение е1 вычисляется только один раз. Обратите внимание на круглые скобки вокруг е2:

X *= Y + 1

то

X = X * (Y + 1)

не

X = X * Y + 1

В качестве примера приведем функцию BITCOUNT, которая подсчитывает число равных 1 битов у целого аргумента.

BITCOUNT(N) /* COUNT 1 BITS IN N */ UNSIGNED N; ( INT B; FOR (B = 0; N != 0; N >>= 1) IF (N & 01) B++; RETURN(B); )

Не говоря уже о краткости, такие операторы приваивания имеют то преимущество, что они лучше соответствуют образу человеческого мышления. Мы говорим: "прибавить 2 к I" или "увеличить I на 2", но не "взять I, прибавить 2 и поместить результат опять в I". Итак, I += 2. Кроме того, в громоздких выражениях, подобных

YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2

Tакая операция присваивания облегчает понимание программы, так как читатель не должен скрупулезно проверять, являются ли два длинных выражения действительно одинаковыми, или за- думываться, почему они не совпадают. Такая операция присваи- вания может даже помочь компилятору получить более эффектив- ную программу. Мы уже использовали тот факт, что операция присваивания имеет некоторое значение и может входить в выражения; самый типичный пример WHILE ((C = GETCHAR()) != EOF)

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

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

--------------- В двоичной системе счисления операция X&(X-1) обнуляет самый правый равный 1 бит переменной X.(почему?) используйте это замечание для написания более быстрой версии функции BITCOUNT.



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


Операции отношения группируются слева направо, но этот факт не очень полезен; выражение A<B<C не означает того, что оно казалось бы должно означать. Выражение-отношения: выражение < выражение выражение > выражение выражение <= выражение выражение >= выражение

Операции < (меньше), > (больше), <= (меньше или равно) и >= (больше или равно) все дают 0, если указанное отношение лож- но, и 1, если оно истинно. Результат имеет тип ITN. Выполня- ются обычные арифметические преобразования. Могут сравни- ваться два указателя; результат зависит от относительного расположения указываемых объектов в адресном пространстве. Сравнение указателей переносимо только в том случае, если указатели указывают на объекты из одного и того же массива.



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


Операциями отношения являются

=> > =< <

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

== !=

которые тоже имеют одинаковое старшинство. операции отноше- ния младше арифметических операций, так что выражения типа I<LIM-1 понимаются как I<(LIM-1), как и предполагается. Логические связки && и \!\! более интересны. Выражения, связанные операциями && и \!\!, вычисляются слева направо, причем их рассмотрение прекращается сразу же как только ста- новится ясно, будет ли результат истиной или ложью. учет этих свойств очень существенен для написания правильно рабо- тающих программ. Рассмотрим, например, оператор цикла в счи- тывающей строку функции GETLINE, которую мы написали в главе 1. FOR(I=0;I<LIM-1 && (C=GETCHAR()) != '\N' && C != EOF; ++I) S[I]=C;

Ясно, что перед считыванием нового символа необходимо проверить, имеется ли еще место в массиве S, так что условие I<LIM-1 должно проверяться первым. И если это условие не вы- полняется, мы не должны считывать следующий символ. Так же неудачным было бы сравнение 'C' с EOF до обраще- ния к функции GETCHAR : прежде чем проверять символ, его нужно считать. Старшинство операции && выше, чем у \!\!, и обе они младше операций отношения и равенства. Поэтому такие выраже- ния, как

I<LIM-1 && (C = GETCHAR()) != '\N' && C != EOF

не нуждаются в дополнительных круглых скобках. Но так как операция != старше операции присваивания, то для достижения правильного результата в выражении

(C = GETCHAR()) != '\N'

кобки необходимы.

Унарная операция отрицания ! Преобразует ненулевой или истинный операнд в 0, а нулевой или ложный операнд в 1. Обычное использование операции ! Заключается в записи

IF( ! INWORD )

Вместо

IF( INWORD == 0 )

Tрудно сказать, какая форма лучше. Конструкции типа ! INWORD Читаются довольно удобно ("если не в слове"). Но в более сложных случаях они могут оказаться трудными для понимания.

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

--------------- Напишите оператор цикла, эквивалентный приведенному выше оператору FOR, не используя операции &&.



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


Выражение-равенства: выражение == выражение выражение != выражение

Операции == (равно) и != (не равно) в точности аналогичны операциям отношения, за исключением того, что они имеют бо- лее низкий уровень старшинства. (Поэтому значение выражения A<B==C<D равно 1 всякий раз, когда выражение A<B и C<D имеют одинаковое значение истинности). Указатель можно сравнивать с целым, но результат будет машинно- независимым только в том случае, если целым являет- ся константа 0. Гарантируется, что указатель, которому прис- воено значение 0, не указывает ни на какой объект и на самом деле оказывается равным 0; общепринято считать такой указа- тель нулем.



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


Операции сдвига << и >> группируются слева направо. Для обеих операций проводятся обычные арифметические преобразо- вания их операндов, каждый из которых должен быть целочис- ленного типа. Затем правый операнд преобразуется к типу INT; результат имеет тип левого операнда. Результат не определен, если правый операнд отрицателен или больше или равен, чем длина объекта в битах. Выражение-сдвига: выражение << выражение выражение >> выражение

Значением выражения E1<<E2 является E1 (интерпретируемое как комбинация битов), сдвинутое влево на E2 битов; освобождаю- щиеся биты заполняются нулем. значением выражения E1>>E2 яв- ляется E1, сдвинутое вправо на E2 битовых позиций. Если E1 имеет тип UNSIGNE, то сдвиг вправо гарантированно будет ло- гическим (заполнение нулем); в противном случае сдвиг может быть (и так и есть на PDP-11) арифметическим (освобождающие- ся биты заполняются копией знакового бита).



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


В языке "C" предусмотрены две необычные операции для увеличения и уменьшения значений переменных. Операция увели- чения ++ добавляет 1 к своему операнду, а операция уменьше- ния -- вычитает 1. Мы часто использовали операцию ++ для увеличения переменных, как, например, в

IF(C == '\N') ++I;

Необычный аспект заключается в том, что ++ и -- можно использовать либо как префиксные операции (перед переменной, как в ++N), либо как постфиксные (после переменной: N++). Эффект в обоих случаях состоит в увеличении N. Но выражение ++N увеличивает переменную N до использования ее значения, в то время как N++ увеличивает переменную N после того, как ее значение было использовано. Это означает, что в контексте, где используется значение переменной, а не только эффект увеличения, использование ++N и N++ приводит к разным ре- зультатам. Если N = 5, то

х = N++;

устанавливает х равным 5, а

х = ++N;

полагает х равным 6. В обоих случаях N становится равным 6. Операции увеличения и уменьшения можно применять только к переменным; выражения типа х=(I+J)++ являются незаконными. В случаях, где нужен только эффект увеличения, а само значение не используется, как, например, в

IF ( C == '\N' ) NL++;

выбор префиксной или постфиксной операции является делом вкуса. но встречаются ситуации, где нужно использовать имен- но ту или другую операцию. Рассмотрим, например, функцию SQUEEZE(S,C), которая удаляет символ 'с' из строки S, каждый раз, как он встречается.

SQUEEZE(S,C) /* DELETE ALL C FROM S */ CHAR S[]; INT C; { INT I, J;

FOR ( I = J = 0; S[I] != '\0'; I++) IF ( S[I] != C ) S[J++] = S[I]; S[J] = '\0'; }

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

IF ( S[I] != C ) { S[J] = S[I]; J++; }

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


IF ( C == '\N' ) { S[I] = C; ++I; }

более компактной записью

IF ( C == '\N' ) S[I++] = C; В качестве третьего примера рассмотрим функцию STRCAT(S,T), которая приписывает строку т в конец строки S, образуя конкатенацию строк S и т. При этом предполагается, что в S достаточно места для хранения полученной комбинации.

STRCAT(S,T) /* CONCATENATE T TO END OF S */ CHAR S[], T[]; /* S MUST BE BIG ENOUGH */ { INT I, J;

I = J = 0; WHILE (S[I] != '\0') / *FIND END OF S */ I++; WHILE((S[I++] = T[J++]) != '\0') /*COPY T*/ ; }

Tак как из T в S копируется каждый символ, то для подготовки к следующему прохождению цикла постфиксная операция ++ при- меняется к обеим переменным I и J.

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

--------------- Напишите другой вариант функции SQUEEZE(S1,S2), который удаляет из строки S1 каждый символ, совпадающий с каким-либо символом строки S2.

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

--------------- Напишите программу для функции ANY(S1,S2), которая нахо- дит место первого появления в строке S1 какого-либо символа из строки S2 и, если строка S1 не содержит символов строки S2, возвращает значение -1.




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


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

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



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


Имеется ряд операций присваивания, каждая из которых группируется слева направо. Все операции требуют в качестве своего левого операнда L-значение, а типом выражения присва- ивания является тип его левого операнда. Значением выражения присваивания является значение, хранимое в левом операнде после того, как присваивание уже будет произведено. Две час- ти составной операции присваивания являются отдельными лек- семами. Выражение-присваивания: L-значение = выражение L-значение += выражение L-значение -= выражение L-значение *= выражение L-значение /= выражение L-значение %= выражение L-значение >>= выражение L-значение <<= выражение L-значение &= выражение L-значение ^= выражение L-значение \!= выражение

Когда производится простое присваивание C'=', значение выражения заменяет значение объекта, на которое ссылается L-значение. Если оба операнда имеют арифметический тип, то перед присваиванием правый операнд преобразуется к типу ле- вого операнда. О свойствах выражения вида E1 оп = E2, где Oп - одна из перечисленных выше операций, можно сделать вывод, если учесть, что оно эквивалентно выражению E1 = E1 оп (E2); од- нако выражение E1 вычисляется только один раз. В случае опе- раций += и -= левый операнд может быть указателем, причем при этом (целочисленный) правый операнд преобразуется таким образом, как объяснено в п. 15.4; все правые операнды и все отличные от указателей левые операнды должны иметь арифмети- ческий тип. Используемые в настоящее время компиляторы допускают присваивание указателя целому, целого указателю и указателя указателю другого типа. такое присваивание является чистым копированием без каких-либо преобразований. Такое употребле- ние операций присваивания является непереносимым и может приводить к указателям, которые при использовании вызывают ошибки адресации. Тем не менее гарантируется, что присваива- ние указателю константы 0 дает нулевой указатель, который можно отличать от указателя на любой объект.



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


Выражение-с-запятой: выражение , выражение

Пара выражений, разделенных запятой, вычисляется слева нап- раво и значение левого выражения отбрасывается. Типом и зна- чением результата является тип и значение правого операнда. Эта операция группируется слева направо. В контексте, где запятая имеет специальное значение, как, например, в списке фактических аргументов функций (п. 15.1) Или в списках ини- циализаторов (п. 16.6), Операция запятая, описываемая в этом разделе, может появляться только в круглых скобках; напри- мер, функция

F(A,(T=3,T+2),C)

имеет три аргумента, второй из которых имеет значение 5.



Оператор BREAK


Иногда бывает удобным иметь возможность управлять выхо- дом из цикла иначе, чем проверкой условия в начале или в конце. Оператор BRеак позволяет выйти из операторов FOR, WHILE и DO до окончания цикла точно так же, как и из перек- лючателя. Оператор BRеак приводит к немедленному выходу из самого внутреннего охватывающего его цикла (или переключате- ля). Следующая программа удаляет хвостовые пробелы и табуля- ции из конца каждой строки файла ввода. Она использует опе- ратор BRеак для выхода из цикла, когда найден крайний правый отличный от пробела и табуляции символ.

#DEFINE MAXLINE 1000 MAIN() /* REMOVE TRAILING BLANKS AND TABS */ { INT N; CHAR LINE[MAXLINE];

WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) { WHILE (--N >= 0) IF (LINE[N] != ' ' && LINE[N] != '\T' && LINE[N] != '\N') BREAK; LINE[N+1] = '\0'; PRINTF("%S\N",LINE); } }

Функция GETLINE возвращает длину строки. Внутренний цикл начинается с последнего символа LINE (напомним, что --N уменьшает N до использования его значения) и движется в об- ратном направлении в поиске первого символа , который отли- чен от пробела, табуляции или новой строки. Цикл прерывает- ся, когда либо найден такой символ, либо N становится отри- цательным (т.е., когда просмотрена вся строка). Советуем вам убедиться, что такое поведение правильно и в том случае, когда строка состоит только из символов пустых промежутков. В качестве альтернативы к BRеак можно ввести проверку в сам цикл:

WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) { WHILE (--N >= 0 && (LINE[N] == ' ' \!\! LINE[N] == '\T' \!\! LINE[N] == '\N')) ; ... }

Это уступает предыдущему варианту, так как проверка стано- вится труднее для понимания. Проверок, которые требуют пе- реплетения &&, \!\!, ! И круглых скобок, по возможности сле- дует избегать.


Оператор

BREAK;

вызывает завершение выполнения наименьшего охватывающего этот оператор оператора WHILE, DO, FOR или SWITCH; управле- ние передается оператору, следующему за завершенным операто- ром.



Оператор CONTINUE


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

FOR (I = 0; I < N; I++) { IF (A[I] < 0) /* SKIP NEGATIVE ELEMENTS */ CONTINUE; ... /* DO POSITIVE ELEMENTS */ }

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

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

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

Напишите программу копирования ввода на вывод, с тем ис- ключением, что из каждой группы последовательных одинаковых строк выводится только одна. (Это простой вариант утилиты UNIQ систем UNIX).


Оператор

CONTINUE;

приводит к передаче управления на продолжающую цикл часть наименьшего охватывающего этот оператор оператора WHILE, DO или FOR; то есть на конец цикла. Более точно, в каждом из операторов

WHILE(...) \( DO \( FOR(...) \( ... ... ... CONTIN: ; CONTIN: ; CONTIN: ; \) \) WHILE(...); \)

Оператор CONTINUE эквивалентен оператору GOTO CONTIN. (За CONTIN: следует пустой оператор; см. П. 17.13.).



Оператор DO


Оператор DO имеет форму

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

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



Оператор FOR


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

MAIN() /* FAHRENHEIT-CELSIUS TABLE */ { INT FAHR; FOR (FAHR = 0; FAHR <= 300; FAHR = FAHR + 20) PRINTF("%4D %6.1F\N", FAHR, (5.0/9.0)*(FAHR-32.0)); }

Эта программа выдает те же самые результаты, но выглядит безусловно по-другому. Главное изменение - исключение боль- шинства переменных; осталась только переменная FAHR , причем типа INT (это сделано для того, чтобы продемонстрировать преобразование %D в функции PRINTF). Нижняя и верхняя грани- цы и размер щага появляются только как константы в операторе FOR , который сам является новой конструкцией, а выражение, вычисляющее температуру по цельсию, входит теперь в виде третьего аргумента функции PRINTF , а не в виде отдельного оператора присваивания. Последнее изменение является примером вполне общего пра- вила языка "C" - в любом контексте, в котором допускается использование значения переменной некоторого типа, вы можете использовать выражение этого типа. Так как третий аргумент функции PRINTF должен иметь значение с плавающей точкой, чтобы соответствовать спецификации %6.1F, то в этом месте может встретиться любое выражение плавающего типа. Сам оператор FOR - это оператор цикла, обобщающий опера- тор WHILE. Его функционирование должно стать ясным, если вы сравните его с ранее описанным оператором WHILE . Оператор FOR содержит три части, разделяемые точкой с запятой. Первая часть

FAHR = 0

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

FAHR <= 300

это условие проверяется и, если оно истинно, то выполняется тело цикла (в данном случае только функция PRINTF ). Затем выполняется шаг реинициализации FAHR =FAHR + 20

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

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

--------------- Модифицируйте программу перевода температур таким обра- зом, чтобы она печатала таблицу в обратном порядке, т.е. От 300 градусов до 0.


Оператор FOR имеет форму

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

Оператор FOR эквивалентен следующему

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

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



Оператор GOTO


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

GOTO идентификатор1

идентификатор должен быть меткой (п. 9.12), Локализованной в данной функции.



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


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

FOR ( ... ) FOR ( ... ) { ... IF (DISASTER) GOTO ERROR; } ...

ERROR: CLEAN UP THE MESS

Если программа обработки ошибок нетривиальна и ошибки могут возникать в нескольких местах, то такая организация оказыва- ется удобной. Метка имеет такую же форму, что и имя перемен- ной, и за ней всегда следует двоеточие. Метка может быть приписана к любому оператору той же функции, в которой нахо- дится оператор GOTO. В качестве другого примера рассмотрим задачу нахождения первого отрицательного элемента в двумерном массиве. (Много- мерные массивы рассматриваются в главе 5). Вот одна из воз- можностей:

FOR (I = 0; I < N; I++) FOR (J = 0; J < M; J++) IF (V[I][J] < 0) GOTO FOUND; /* DIDN'T FIND */ ... FOUND: /* FOUND ONE AT POSITION I, J */ ...

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

FOUND = 0; FOR (I = 0; I < N && !FOUND; I++) FOR (J = 0; J < M && !FOUND; J++) FOUND = V[I][J] < 0; IF (FOUND) /* IT WAS AT I-1, J-1 */ ... ELSE /* NOT FOUND */ ... Хотя мы не являемся в этом вопросе догматиками, нам все же кажется, что если и нужно использовать оператор GOTO, то весьма умеренно.



Оператор SWITCH


Оператор SWITCH (переключатель), вызывает передачу уп- равления к одному из нескольких операторов, в зависимости от значения выражения. Оператор имеет форму

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

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

CASE констанстное выражение:

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

DEFAULT:

При выполнении оператора SWITCH вычисляется входящее в него выражение и сравнивается с каждой вариантной констан- той. Если одна из вариантных констант оказывается равной значению этого выражения, то управление передается операто- ру, который следует за совпадающим вариантным префиксом. Ес- ли ни одна из вариантных констант не совпадает со значением выражения и если при этом присутствует префикс DEFAULT, то управление передается оператору, помеченному этим префиксом. если ни один из вариантов не подходит и префикс DEFAULT от- сутствует, то ни один из операторов в переключателе не вы- полняется. Сами по себе префиксы CASE и DEFAULT не изменяют поток управления, которое беспрепятсвенно проходит через такие префиксы. Для выхода из переключателя смотрите оператор BREAK, п. 17.8. Обычно оператор, который входит в переключатель, являет- ся составным. Описания могут появляться в начале этого опе- ратора, но инициализации автоматических и регистровых пере- менных будут неэффективными.



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


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

RETURN; RETURN выражение;

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



Оператор WHILE


Оператор WHILE имеет форму

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

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



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


Большинство операторов являются операторными выражения- ми, которые имеют форму

выражение;

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



Операторы


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


составной-оператор: \(список-описаний список-операторов необ необ\) список-описаний: описание описание список-описаний список-операторов: оператор оператор список-операторов оператор: составной оператор выражение;

IF (выражение) оператор IF (выражение) оператор ELSE оператор WHILE (выражение) оператор DO оператор WHILE (выражение); FOR(выражение-1 ;выражение-2 ;выражение-3 ) необ необ необ оператор SWITCH (выражение) оператор CASE константное-выражение : оператор DEFAULT: оператор BREAK; CONTINUE; RETURN; RETURN выражение; GOTO идентификатор; идентификатор : оператор ;



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


Такие выражения, как X=0, или I++, или PRINTF(...), становятся операторами, если за ними следует точка с запя- той, как, например,

X = 0; I++; PRINTF(...);

В языке "C" точка с запятой является признаком конца опера- тора, а не разделителем операторов, как в языках типа алго- ла. Фигурные скобки /( и /) используются для объединения описаний и операторов в составной оператор или блок, так что они оказываются синтаксически эквивалентны одному оператору. Один явный пример такого типа дают фигурные скобки, в кото- рые заключаются операторы, составляющие функцию, другой - фигурные скобки вокруг группы операторов в конструкциях IF, ELSE, WHILE и FOR.(на самом деле переменные могут быть опи- саны внутри любого блока; мы поговорим об этом в главе 4). Точка с запятой никогда не ставится после первой фигурной скобки, которая завершает блок.



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


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

структура-или-объединение \( список-описаний-структуры\)

идентификатор структуры-или-объединения \(список-описаний-структуры\) идентификатор структуры-или-объединения

Структура-или-объединение:

STRUCT UNION

Список-описаний-структуры является последовательностью опи- саний членов структуры или объединения:

Список-описаний-структуры: описание-структуры описание-структуры список-описаний-структуры описание-структуры: спецификатор-типа список-описателей-структуры список-описателей-структуры: описатель-структуры описатель-структуры, список-описателей-структуры

В обычном случае описатель структуры является просто описа- телем члена структуры или объединения. Член структуры может также состоять из специфицированного числа битов. Такой член называется также полем; его длина отделяется от имени поля двоеточием.

Описатель-структуры: описатель описатель: константное выражение : константное выражение

Внутри структуры описанные в ней объекты имеют адреса, кото- рые увеличиваются в соответствии с чтением их описаний слева направо. Каждый член структуры, который не является полем, начинается с адресной границы, соответствующей его типу; следовательно в структуре могут оказаться неименованные ды- ры. Члены, являющиеся полями, помещаются в машинные целые; они не перекрывают границы слова. Поле, которое не умещается в оставшемся в данном слове пространстве, помещается в сле- дующее слово. Поля выделяются справа налево на PDP-11 и сле- ва направо на других машинах. Описатель структуры, который не содержит описателя, а только двоеточие и ширину, указывает неименованное поле, по- лезное для заполнения свободного пространства с целью соот- ветствия задаваемых извне схемам. Специальный случай неиме- нованного поля с шириной 0 используется для указания о вы- равнивании следующего поля на границу слова. При этом пред- полагается, что "следующее поле" действиетльно является по- лем, а не обычным членом структуры, поскольку в последнем случае выравнивание осуществляется автоматически. Сам язык не накладывает ограничений на типы объектов, описанных как поля, но от реализаций не требуется обеспечи- вать что-либо отличное от целых полей. Более того, даже поля типа INT могут рассматриваться как неимеющие знака. На PDP-11 поля не имеют знака и могут принимать только целые значения. Во всех реализациях отсутствуют массивы полей и к полям не применима операция взятия адреса &, так что не су- ществует и указателей на поля. Объединение можно представить себе как структуру, все члены которой начинаются со смещения 0 и размер которой дос- таточен, чтобы содержать любой из ее членов. В каждый момент объединение может содержать не более одного из своих членов. Спецификатор структуры или объединения во второй форме, т.е. Один из


STRUCT идентификатор \(список-описаний-структуры\)

UNION идентификатор \(список-описаний-структуры\)

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

STRUCT идентификатор

UNION идентификатор

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

STRUCT TNODE \( CHAR TWORD[20]; INT COUNT; STRUCT TNODE *LEFT; STRUCT TNODE *RIGHT; \); Такая структура содержит массив из 20 символов, целое и два указателя на подобные структуры. Как только приведено такое описание, описание

STRUCT TNODE S, *SP;

говорит о том, что S является структурой указанного вида, а SP является указателем на структуру указанного вида. При на- личии этих описаний выражение

SP->COUNT

ссылается к полю COUNT структуры, на которую указывает SP; выражение

S.LEFT

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

S.RIGHT->TWORD[0]

ссылается на первый символ члена TWORD правого поддерева из S.




Описания


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

INT LOWER, UPPER, STEP; CHAR C, LINE[1000];

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

INT LOWER; INT UPPER; INT STEP; CHAR C; CHAR LINE[1000];

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

CHAR BACKSLASH = '\\'; INT I = 0; FLOAT EPS = 1.0E-5;

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



Описатели


Входящий в описание список описателей представляет собой последовательность разделенных запятыми описателей, каждый из которых может иметь инициализатор. Список-описателей: инициализируемый-описатель инициализируемый-описатель, список-описателей инициализируемый-описатель: описатель-инициализатор необ

Инициализаторы описываются в п. 16.6. Спецификаторы и описа- ния указывают тип и класс памяти объектов, на которые ссыла- ются описатели. Описатели имеют следующий синтаксис:

описатель: идентификатор ( описатель ) * описатель описатель () описатель [константное-выражение необ]

Группирование такое же как и в выражениях.



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


В языке "C" предусмотрена возможность, называемая TYPEDEF для введения новых имен для типов данных. Например, описание

TYPEDEF INT LENGTH;

делает имя LENGTH синонимом для INT. "Тип" LENGTH может быть использован в описаниях, переводов типов и т.д. Точно таким же образом, как и тип INT:

LENGTH LEN, MAXLEN; LENGTH *LENGTHS[];

Аналогично описанию

TYPEDEF CHAR *STRING;

делает STRING синонимом для CHAR*, то есть для указателя на символы, что затем можно использовать в описаниях вида

STRING P, LINEPTR[LINES], ALLOC();

Обратите внимание, что объявляемый в конструкции TYPEDEF тип появляется в позиции имени переменной, а не сразу за словом TYPEDEF. Синтаксически конструкция TYPEDEF подобна описаниям класса памяти EXTERN, STATIC и т. Д. мы также ис- пользовали прописные буквы, чтобы яснее выделить имена. В качестве более сложного примера мы используем конст- рукцию TYPEDEF для описания узлов дерева, рассмотренных ра- нее в этой главе:

TYPEDEF STRUCT TNODE \( /* THE BASIC NODE */ CHAR *WORD; /* POINTS TO THE TEXT */ INT COUNT; /* NUMBER OF OCCURRENCES */ STRUCT TNODE *LEFT; /* LEFT CHILD */ STRUCT TNODE *RIGHT; /* RIGHT CHILD */ \) TREENODE, *TREEPTR;

В результате получаем два новых ключевых слова: TREENODE (структура) и TREEPTR (указатель на структуру). Тогда функ- цию TALLOC можно записать в виде

TREEPTR TALLOC() \( CHAR *ALLOC(); RETURN((TREEPTR) ALLOC(SIZEOF(TREENODE))); \)

Необходимо подчеркнуть, что описание TYPEDEF не приводит к созданию нового в каком-либо смысле типа; оно только до- бавляет новое имя для некоторого существующего типа. при этом не возникает и никакой новой семантики: описанные таким способом переменные обладают точно теми же свойствами, что и переменные, описанные явным образом. По существу конструкция TYPEDEF сходна с #DEFINE за исключением того, что она интер- претируется компилятором и потому может осуществлять подста- новки текста, которые выходят за пределы возможностей мак- ропроцессора языка "C". Например,


TYPEDEF INT (*PFI) ();

создает тип PFI для " указателя функции, возвращающей значе- ние типа INT", который затем можно было бы использовать в программе сортировки из главы 5 в контексте вида

PFI STRCMP, NUMCMP, SWAP;

Имеются две основные причины применения описаний TYPEDEF. Первая причина связана с параметризацией программы, чтобы облегчить решение проблемы переносимости. Если для ти- пов данных, которые могут быть машинно-зависимыми, использо- вать описание TYPEDEF, то при переносе программы на другую машину придется изменить только эти описания. Одна из типич- ных ситуаций состоит в использовании определяемых с помощью TYPEDEF имен для различных целых величин и в последующем подходящем выборе типов SHORT, INT и LONG для каждой имею- щейся машины. Второе назначение TYPEDEF состоит в обеспечении лучшей доку- ментации для программы - тип с именем TREEPTR может оказать- ся более удобным для восприятия, чем тип, который описан только как указатель сложной структуры. И наконец, всегда существует вероятность, что в будущем ком- пилятор или некоторая другая программа, такая как LINT, смо- жет использовать содержащуюся в описаниях TYPEDEF информацию для проведения некоторой дополнительной проверки программы.




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


Для начала давайте разработаем и составим программу пе- чати каждой строки ввода, которая содержит определенную ком- бинацию символов. /Это - специальный случай утилиты GREP системы "UNIX"/. Например, при поиске комбинации "THE" в на- боре строк

NOW IS THE TIME FOR ALL GOOD MEN TO COME TO THE AID OF THEIR PARTY в качестве выхода получим

NOW IS THE TIME MEN TO COME TO THE AID OF THEIR PARTY

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

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

"Пока имеется еще строка" - это GETLINE, функция, кото- рую мы запрограммировали в главе 1, а "вывод этой строки" - это функция PRINTF, которую уже кто-то подготовил для нас. Это значит, что нам осталось только написать процедуру для определения, содержит ли строка данную комбинацию символов или нет. Мы можем решить эту проблему, позаимствовав разра- ботку из PL/1: функция INDEX(S,т) возвращает позицию, или индекс, строки S, где начинается строка T, и -1, если S не содержит т . В качестве начальной позиции мы используем 0, а не 1, потому что в языке "C" массивы начинаются с позиции нуль. Когда нам в дальнейшем понадобится проверять на совпа- дение более сложные конструкции, нам придется заменить толь- ко функцию INDEX; остальная часть программы останется той же самой. После того, как мы потратили столько усилий на разработ- ку, написание программы в деталях не представляет затрудне- ний. ниже приводится целиком вся программа, так что вы може- те видеть, как соединяются вместе отдельные части. Комбина- ция символов, по которой производится поиск, выступает пока в качестве символьной строки в аргументе функции INDEX, что не является самым общим механизмом. Мы скоро вернемся к об- суждению вопроса об инициализации символьных массивов и в главе 5 покажем, как сделать комбинацию символов параметром, которому присваивается значение в ходе выполнения программы. Программа также содержит новый вариант функции GETLINE; вам может оказаться полезным сравнить его с вариантом из главы 1.


#DEFINE MAXLINE 1000 MAIN() /* FIND ALL LINES MATCHING A PATTERN */ { CHAR LINE[MAXLINE];

WHILE (GETLINE(LINE, MAXLINE) > 0) IF (INDEX(LINE, "THE") >= 0) PRINTF("%S", LINE); } GETLINE(S, LIM) /* GET LINE INTO S, RETURN LENGTH * CHAR S[]; INT LIM; { INT C, I;

I = 0; WHILE(--LIM>0 && (C=GETCHAR()) != EOF && C != '\N') S[I++] = C; IF (C == '\N') S[I++] = C; S[I] = '\0'; RETURN(I); }

INDEX(S,T) /* RETURN INDEX OF T IN S,-1 IF NONE */ CHAR S[], T[]; { INT I, J, K;

FOR (I = 0; S[I] != '\0'; I++) { FOR(J=I, K=0; T[K] !='\0' && S[J] == T[K]; J++; K++) ; IF (T[K] == '\0') RETURN(I); } RETURN(-1); }

Каждая функция имеет вид имя (список аргументов, если они имеются) описания аргументов, если они имеются

{ описания и операторы , если они имеются }

Как и указывается, некоторые части могут отсутство- вать; минимальной функцией является

DUMMY () { }

которая не совершает никаких действий.

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

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

RETURN (выражение)

Вызывающая функция может игнорировать возвращаемое значение, если она этого пожелает. Более того, после RETURN может не быть вообще никакого выражения; в этом случае в вы- зывающую программу не передается никакого значения. Управле- ние также возвращется в вызывающую программу без передачи какого-либо значения и в том случае, когда при выполнении мы "проваливаемся" на конец функции, достигая закрывающейся правой фигурной скобки. EСли функция возвращает значение из одного места и не возвращает никакого значения из другого места, это не является незаконным, но может быть признаком каких-то неприятностей. В любом случае "значением" функции, которая не возвращает значения, несомненно будет мусор. От- ладочная программа LINT проверяет такие ошибки. Механика компиляции и загрузки "C"-программ, располо- женных в нескольких исходных файлах, меняется от системы к системе. В системе "UNIX", например, эту работу выполняет команда 'CC', упомянутая в главе 1. Предположим, что три функции находятся в трех различных файлах с именами MAIN.с, GETLINE.C и INDEX.с . Тогда команда

CC MAIN.C GETLINE.C INDEX.C

компилирует эти три файла, помещает полученный настраиваемый объектный код в файлы MAIN.O, GETLINE.O и INDEX.O и загружа- ет их всех в выполняемый файл, называемый A.OUT . Если имеется какая-то ошибка, скажем в MAIN.C, то этот файл можно перекомпилировать отдельно и загрузить вместе с предыдущими объектными файлами по команде

CC MAIN.C GETLIN.O INDEX.O

Команда 'CC' использует соглашение о наименовании с ".с" и ".о" для того, чтобы отличить исходные файлы от объектных.

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

---------------- Составьте программу для функции RINDEX(S,T), которая возвращает позицию самого правого вхождения т в S и -1, если S не содержит T.




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


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

Функция OPEN весьма сходна с функцией FOPEN, рассмотрен- ной в главе 7, за исключением того, что вместо возвращения указателя файла она возвращает дескриптор файла, который яв- ляется просто целым типа INT.

INT FD; FD=OPEN(NAME,RWMODE);

Как и в случае FOPEN, аргумент NAME является символьной строкой, соответствующей внешнему имени файла. Однако аргу- мент, определяющий режим доступа, отличен: RWMODE равно: 0 - для чтения, 1 - для записи, 2 - для чтения и записи. Если происходит какая-то ошибка, функция OPEN возвращает "-1"; в противном случае она возвращает действительный дескриптор файла. Попытка открыть файл, который не существует, является ошибкой. Точка входа CREAT предоставляет возможность созда- ния новых файлов или перезаписи старых. В результате обраще- ния

FD=CREAT(NAME,PMODE);

возвращает дескриптор файла, если оказалось возможным соз- дать файл с именем NAME, и "-1" в противном случае. Если файл с таким именем уже существует, CREAT усечет его до ну- левой длины; создание файла, который уже существует, не яв- ляется ошибкой. Если файл является совершенно новым, то CREAT создает его с определенным режимом защиты, специфицируемым аргумен- том PMODE. В системе файлов на UNIX с файлом связываются де- вять битов защиты информации, которые управляют разрешением на чтение, запись и выполнение для владельца файла, для группы владельцев и для всех остальных пользователей. Таким образом, трехзначное восьмеричное число наиболее удобно для спецификации разрешений. Например, число 0755 свидетельству- ет о разрешении на чтение, запись и выполнение для владельца и о разрешении на чтение и выполнение для группы и всех ос- тальных. Для иллюстрации ниже приводится программа копирования одного файла в другой, являющаяся упрощенным вариантом ути- литы CP системы UNIX. (Основное упрощение заключается в том, что наш вариант копирует только один файл и что второй аргу- мент не должен быть справочником).


#DEFINE NULL 0 #DEFINE BUFSIZE 512 #DEFINE PMODE 0644/*RW FOR OWNER,R FOR GROUP,OTHERS*/ MAIN(ARGC,ARGV) /*CP: COPY F1 TO F2*/ INT ARGC; CHAR *ARGV[]; \( INT F1, F2, N; CHAR BUF[BUFSIZE];

IF (ARGC ! = 3) ERROR("USAGE:CP FROM TO", NULL); IF ((F1=OPEN(ARGV[1],0))== -1) ERROR("CP:CAN'T OPEN %S", ARGV[1]); IF ((F2=CREAT(ARGV[2],PMODE))== -1) ERROR("CP: CAN'T CREATE %S", ARGV[2]); WHILE ((N=READ(F1,BUF,BUFSIZE))>0) IF (WRITE(F2,BUF,N) !=N) ERROR("CP: WRITE ERROR", NULL); EXIT(0); \) ERROR(S1,S2) /*PRINT ERROR MESSAGE AND DIE*/ CHAR *S1, S2; \( PRINTF(S1,S2); PRINTF("\N"); EXIT(1); \)

Существует ограничение (обычно 15 - 25) на количество файлов, которые программа может иметь открытыми одновремен- но. В соответствии с этим любая программа, собирающаяся ра- ботать со многими файлами, должна быть подготовлена к пов- торному использованию дескрипторов файлов. Процедура CLOSE прерывает связь между дескриптором файла и открытым файлом и освобождает дескриптор файла для использования с некоторым другим файлом. Завершение выполнения программы через EXIT или в результате возврата из ведущей программы приводит к закрытию всех открытых файлов. Функция расцепления UNLINK (FILENAME) удаляет из системы файлов файл с именем FILENAME ( из данного справочного фай- ла. Файл может быть сцеплен с другим справочником, возможно, под другим именем - примеч.переводчика).

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

-------------- Перепишите программу CAT из главы 7, используя функции READ, WRITE, OPEN и CLOSE вместо их эквивалентов из стандар- тной библиотеки. Проведите эксперименты для определения от- носительной скорости работы этих двух вариантов.




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


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

MAIN() /* COUNT DIGITS,WHITE SPACE, OTHERS */ { INT C, I, NWHITE, NOTHER, NDIGIT[10];

NWHITE = NOTHER = 0; FOR (I = 0; I < 10; I++) NDIGIT[I] = 0;

WHILE ((C = GETCHAR()) != EOF) SWITCH (C) { CASE '0': CASE '1': CASE '2': CASE '3': CASE '4': CASE '5': CASE '6': CASE '7': CASE '8': CASE '9': NDIGIT[C-'0']++; BREAK; CASE ' ': CASE '\N': CASE '\T': NWHITE++; BREAK; DEFAULT : NOTHER++; BREAK; } PRINTF("DIGITS ="); FOR (I = 0; I < 10; I++) PRINTF(" %D", NDIGIT[I]); PRINTF("\NWHITE SPACE = %D, OTHER = %D\N", NWHITE, NOTHER);

Переключатель вычисляет целое выражение в круглых скоб- ках (в данной программе - значение символа с) и сравнивает его значение со всеми случаями (CASE). Каждый случай должен быть помечен либо целым, либо символьной константой, либо константным выражением. Если значение константного выраже- ния, стоящего после вариантного префикса CASE, совпадает со значением целого выражения, то выполнение начинается с этого случая. Если ни один из случаев не подходит, то выполняется оператор после префикса DEFAULT. Префикс DEFAULT является необязательным ,если его нет, и ни один из случаев не подхо- дит, то вообще никакие действия не выполняются. Случаи и вы- бор по умолчанию могут располагаться в любом порядке. Все случаи должны быть различными.

Оператор BREAK приводит к немедленному выходу из перек- лючателя. Поскольку случаи служат только в качестве меток, то если вы не предпримите явных действий после выполнения операторов, соответствующих одному случаю, вы провалитесь на следующий случай. Операторы BREAK и RETURN являются самым обычным способом выхода из переключателя. Как мы обсудим позже в этой главе, оператор BREAк можно использовать и для немедленного выхода из операторов цикла WHILE, FOR и DO. Проваливание сквозь случаи имеет как свои достоинства, так и недостатки. К положительным качествам можно отнести то, что оно позволяет связать несколько случаев с одним дей- ствием, как было с пробелом, табуляцией и новой строкой в нашем примере. Но в то же время оно обычно приводит к необ- ходимости заканчивать каждый случай оператором BREAK, чтобы избежать перехода к следующему случаю. Проваливание с одного случая на другой обычно бывает неустойчивым, так как оно склонно к расщеплению при модификации программы. За исключе- нием, когда одному вычислению соответствуют несколько меток, проваливание следует использовать умеренно. Заведите привычку ставить оператор BREAK после последне- го случая (в данном примере после DEFAULT), даже если это не является логически необходимым. В один прекрасный день, ког- да вы добавите в конец еще один случай, эта маленькая мера предосторожности избавит вас от неприятностей.

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

-------------- Напишите программу для функции EXPAND(S, T), которая ко- пирует строку S в т, заменяя при этом символы табуляции и новой строки на видимые условные последовательности, как \N и \т. используйте переключатель.



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


Следующая программа печатает приведенную ниже таблицу температур по Фаренгейту и их эквивалентов по стоградусной шкале Цельсия, используя для перевода формулу

C = (5/9)*(F-32). 0 -17.8 20 -6.7 40 4.4 60 15.6 ... ... 260 126.7 280 137.8 300 140.9

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

/* PRINT FAHRENHEIT-CELSIUS TABLE FOR F = 0, 20, ..., 300 */ MAIN() { INT LOWER, UPPER, STEP; FLOAT FAHR, CELSIUS; LOWER = 0; /* LOWER LIMIT OF TEMPERATURE TABLE */ UPPER =300; /* UPPER LIMIT */ STEP = 20; /* STEP SIZE */ FAHR = LOWER; WHILE (FAHR <= UPPER) { CELSIUS = (5.0/9.0) * (FAHR -32.0); PRINTF("%4.0F %6.1F\N", FAHR, CELSIUS); FAHR = FAHR + STEP; } }

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

/* PRINT FAHRENHEIT-CELSIUS TABLE FOR F = 0, 20, ..., 300 */

являются комментарием, который в данном случае кратко пояс- няет, что делает программа. Любые символы между /* и */ иг- норируются компилятором; можно свободно пользоваться коммен- тариями для облегчения понимания программы. Комментарии мо- гут появляться в любом месте, где возможен пробел или пере- ход на новую строку. В языке "C" все переменные должны быть описаны до их ис- пользования, обычно это делается в начале функции до первого выполняемого оператора. Если вы забудете вставить описание, то получите диагностическое сообщение от компилятора. Описа- ние состоит из типа и списка переменных, имеющих этот тип, как в

INT LOWER, UPPER, STEP; FLOAT FAHR, CELSIUS;

Тип INT означает, что все переменные списка целые; тип FLOAT предназначен для чисел с плавающей точкой, т.е. для чисел, которые могут иметь дробную часть. Точность как INT , TAK и FLOAT зависит от конкретной машины, на которой вы ра- ботаете. На PDP-11, например, тип INT соответствует 16-бито- вому числу со знаком, т.е. числу, лежащему между -32768 и +32767. Число типа FLOAT - это 32-битовое число, имеющее около семи значащих цифр и лежащее в диапазоне от 10е-38 до 10е+38. В главе 2 приводится список размеров для других ма- шин. В языке "C" предусмотрено несколько других основных ти- пов данных, кроме INT и FLOAT: CHAR символ - один байт SHORT короткое целое LONG длинное целое DOUBLE плавающее с двойной точностью Размеры этих объектов тоже машинно-независимы; детали приведены в главе 2. Имеются также массивы, структуры и об- ъединения этих основных типов, указатели на них и функ- ции,которые их возвращают; со всеми ними мы встретимся в свое время. Фактически вычисления в программе перевода температур начинаются с операторов присваивания LOWER = 0; UPPER =300; STEP = 20; FAHR =LOWER; которые придают переменным их начальные значения. каждый от- дельный оператор заканчивается точкой с запятой. Каждая строка таблицы вычисляется одинаковым образом, так что мы используем цикл, повторяющийся один раз на стро- ку. В этом назначение оператора WHILE:


WHILE (FAHR <= UPPER) { .... }

проверяется условие в круглых скобках. Если оно истинно (FAHR меньше или равно UPPER), то выполняется тело цикла (все операторы, заключенные в фигурные скобки { и } ). Затем вновь проверяется это условие и, если оно истинно, опять вы- полняется тело цикла. Если же условие не выполняется ( FAHR превосходит UPPER ), цикл заканчивается и происходит переход к выполнению оператора, следующего за оператором цикла. Так как в настоящей программе нет никаких последующих операто- ров, то выполнение программы завершается. Тело оператора WHILE может состоять из одного или более операторов, заключенных в фигурные скобки, как в программе перевода температур, или из одного оператора без скобок, как, например, в

WHILE (I < J) I = 2 * I;

В обоих случаях операторы, управляемые оператором WHILE, сдвинуты на одну табуляцию, чтобы вы могли с первого взгляда видеть, какие операторы находятся внутри цикла. Такой сдвиг подчеркивает логическую структуру программы. Хотя в языке "C" допускается совершенно произвольное расположение опера- торов в строке, подходящий сдвиг и использование пробелов значительно облегчают чтение программ. Мы рекомендуем писать только один оператор на строке и (обычно) оставлять пробелы вокруг операторов. Расположение фигурных скобок менее сущес- твенно; мы выбрали один из нескольких популярных стилей. Вы- берите подходящий для вас стиль и затем используйте его пос- ледовательно. Основная часть работы выполняется в теле цикла. Темпера- тура по Цельсию вычисляется и присваивается переменной CELAIUS оператором

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

причина использования выражения 5.0/9.0 вместо выглядящего проще 5/9 заключается в том, что в языке "C", как и во мно- гих других языках, при делении целых происходит усечение, состоящее в отбрасывании дробной части результата. Таким об- разом, результат операции 5/9 равен нулю, и, конечно, в этом случае все температуры оказались бы равными нулю. Десятичная точка в константе указывает, что она имеет тип с плавающей точкой, так что, как мы и хотели, 5.0/9.0 равно 0.5555... . Мы также писали 32.0 вместо 32 , несмотря на то, что так как переменная FAHR имеет тип FLOAT , целое 32 автоматически бы преобразовалось к типу FLOAT ( в 32.0) перед вычитанием. С точки зрения стиля разумно писать плавающие константы с явной десятичной точкой даже тогда, когда они имеют целые значения; это подчеркивает их плавающую природу для просмат- ривающего программу и обеспечивает то, что компилятор будет смотреть на вещи так же, как и Вы.



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

FAHR = LOWER;

проверка

WHILE (FAHR <= UPPER)

работают, как ожидается, - перед выполнением операций целые преобразуются в плавающую форму. Этот же пример сообщает чуть больше о том, как работает PRINTF. Функция PRINTF фактически является универсальной функцией форматных преобразований, которая будет полностью описана в главе 7. Ее первым аргументом является строка сим- волов, которая должна быть напечатана, причем каждый знак % указывает, куда должен подставляться каждый из остальных ар- гументов /второй, третий, .../ и в какой форме он должен пе- чататься. Например, в операторе

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

спецификация преобразования %4.0F говорит, что число с пла- вающей точкой должно быть напечатано в поле шириной по край- ней мере в четыре символа без цифр после десятичной точки. спецификация %6.1F описывает другое число, которое должно занимать по крайней мере шесть позиций с одной цифрой после десятичной точки, аналогично спецификациям F6.1 в фортране или F(6,1) в PL/1. Различные части спецификации могут быть опущены: спецификация %6F говорит, что число будет шириной по крайней мере в шесть символов; спецификация %2 требует двух позиций после десятичной точки, но ширина при этом не ограничивается; спецификация %F говорит только о том, что нужно напечатать число с плавающей точкой. Функция PRINTF также распознает следующие спецификации: %D - для десятично- го целого, %о - для восьмеричного числа, %х - для шестнадца- тиричного, %с - для символа, %S - для символьной строки и %% - для самого символа %. Каждая конструкция с символом % в первом аргументе функ- ции PRINTF сочетается с соответствующим вторым, третьим, и т.д. Аргументами; они должны согласовываться по числу и ти- пу; в противном случае вы получите бессмысленные результаты. Между прочим, функция PRINTF не является частью языка "C"; в самом языке "C" не определены операции ввода-вывода. Нет ничего таинственного и в функции PRINTF ; это - просто полезная функция, являющаяся частью стандартной библиотеки подпрограмм, которая обычно доступна "C"-программам. Чтобы сосредоточиться на самом языке, мы не будем подробно оста- навливаться на операциях ввода-вывода до главы 7. В частнос- ти, мы до тех пор отложим форматный ввод. Если вам надо ввести числа - прочитайте описание функции SCANF в главе 7, раздел 7.4. Функция SCANF во многом сходна с PRINTF , но она считывает входные данные, а не печатает выходные.

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

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

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

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




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


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

Первичное выражение: идентификатор константа строка (выражение) первичное-выражение [выражение] первичное-выражение (список-выражений нео первичное-L-значение . Идентификатор первичное-выражение -> идентификатор список-выражений: выражение список-выражений, выражение

Идентификатор является первичным выражением при условии, что он описан подходящим образом, как это обсуждается ниже. тип идентификатора определяется его описанием. Если, однако, ти- пом идентификатора является "массив ...", то значением выра- жения, состоящего из этого идентификатора , является указа- тель на первый объект в этом массиве, а типом выражения бу- дет "указатель на ...". Более того, идентификатор массива не является выражением L-значения. подобным образом идентифика- тор, который описан как "функция, возвращающая ...", за иск- лючением того случая, когда он используется в позиции имени функции при обращении, преобразуется в "указатель на функ- цию, которая возвращает ...". Константа является первичным выражением. В зависимости от ее формы типом константы может быть INT, LONG или DOUBLE. Строка является первичным выражением. Исходным ее типом является "массив символов"; но следуя тем же самым правилам, которые приведены выше для идентификаторов, он модифицирует- ся в "указатель на символы", и результатом является указа- тель на первый символ строки. (имеется исключение в некото- рых инициализаторах; см. П. 16.6.) Выражение в круглых скобках является первичным выражени- ем, тип и значение которого идентичны типу и значению этого выражения без скобок. Наличие круглых скобок не влияет на то, является ли выражение L-значением или нет.

Первичное выражение, за которым следует выражение в квадратных скобках, является первичным выражением. Интуитив- но ясно, что это выражение с индексом. Обычно первичное вы- ражение имеет тип "указатель на ...", индексное выражение имеет тип INT, а типом результата является "...". Выражение E1[E2] по определению идентично выражению * ((E1) + (E2)). Все, что необходимо для понимания этой записи, содержится в этом разделе; вопросы, связанные с понятием идентификаторов и операций * и + рассматриваются в п.п. 15.1, 15.2 И 15.4 соответственно; выводы суммируются ниже в п. 22.3. Обращение к функции является первичным выражением, за которым следует заключенный в круглые скобки возможно пустой список выражений, разделенных запятыми, которые и представ- ляют собой фактические аргументы функции. Первичное выраже- ние должно быть типа "функция, возвращающая ...", а резуль- тат обращения к функции имеет тип "...". Как указывается ни- же, ранее не встречавщийся идентификатор, за которым непос- редственно следует левая круглая скобка, считается описанным по контексту, как представляющий функцию, возвращающую це- лое; следовательно чаще всего встречающийся случай функции, возвращающей целое значение, не нуждается в описании. Перед обращением любые фактические аргументы типа FLOAT преобразуются к типу DOUBLE, любые аргументы типа CHAR или SHORT преобразуются к типу INT, и, как обычно, имена масси- вов преобразуются в указатели. Никакие другие преобразования не выполняются автоматически; в частности, не сравнивает ти- пы фактических аргументов с типами формальных аргументов. Если преобразование необходимо, используйте явный перевод типа (CAST); см. П.п. 15.2, 16.7. При подготовке к вызову функции делается копия каждого фактического параметра; таким образом, все передачи аргумен- тов в языке "C" осуществляются строго по значению. функция может изменять значения своих формальных параметров, но эти изменения не влияют на значения фактических параметров. С другой строны имеется возможность передавать указатель при таком условии, что функция может изменять значение объекта, на который этот указатель указывает. Порядок вычисления ар- гументов в языке не определен; обратите внимание на то, что различные компиляторы вычисляют по разному. Допускаются рекурсивные обращения к любой функции. Первичное выражение, за которым следует точка и иденти- фикатор, является выражением. Первое выражение должно быть L-значением, именующим структуру или объединение, а иденти- фикатор должен быть именем члена структуры или объединения. Результатом является L-значение, ссылающееся на поименован- ный член структуры или объединения. Первичное выражение, за которым следует стрелка (состав- ленная из знаков - и >) и идентификатор, является выражени- ем. первое выражение должно быть указателем на структуру или объединение, а идентификатор должен именовать член этой структуры или объединения. Результатом является L-значение, ссылающееся на поименованный член структуры или объединения, на который указывает указательное выражение. Следовательно, выражение E1->MOS является тем же самым, что и выражение (*E1).MOS. Структуры и объединения рассмат- риваются в п. 16.5. Приведенные здесь правила использования структур и объединений не навязываются строго, для того что- бы иметь возможность обойти механизм типов. См. П. 22.1.



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


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



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


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



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


Выражение-и: выражение & выражение

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



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


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

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



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


Выражение-включающего-или: выражение \! Выражение

Операция \! Является ассоциативной, и содержащие \! Выраже- ния могут быть переупорядочены. выполняются обычные арифме- тические преобразования; результатом является побитовая фун- кция включающего 'или' операндов. Операция применима только к операндам целочисленного типа.



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


В языке предусмотрен ряд операций для работы с битами; эти операции нельзя применять к переменным типа FLOAT или DOUBLE.

& Побитовое AND \! Побитовое включающее OR ^ побитовое исключающее OR << сдвиг влево >> сдвиг вправо \^ дополнение (унарная операция)

"\" иммитирует вертикальную черту.

Побитовая операция AND часто используется для маскирования некоторого множества битов; например, оператор

C = N & 0177 передает в 'с' семь младших битов N , полагая остальные рав- ными нулю. Операция 'э' побитового OR используется для вклю- чения битов:

C = X э MASK

устанавливает на единицу те биты в х , которые равны единице в MASK. Следует быть внимательным и отличать побитовые операции & и 'э' от логических связок && и \!\! , Которые подразуме- вают вычисление значения истинности слева направо. Например, если х=1, а Y=2, то значение х&Y равно нулю , в то время как значение X&&Y равно единице./почему?/ Операции сдвига << и >> осуществляют соответственно сдвиг влево и вправо своего левого операнда на число битовых позиций, задаваемых правым операндом. Таким образом , х<<2 сдвигает х влево на две позиции, заполняя освобождающиеся биты нулями, что эквивалентно умножению на 4. Сдвиг вправо величины без знака заполняет освобождающиеся биты на некото- рых машинах, таких как PDP-11, заполняются содержанием зна- кового бита /"арифметический сдвиг"/, а на других - нулем /"логический сдвиг"/. Унарная операция \^ дает дополнение к целому; это озна- чает , что каждый бит со значением 1 получает значение 0 и наоборот. Эта операция обычно оказывается полезной в выраже- ниях типа

X & \^077

где последние шесть битов х маскируются нулем. Подчеркнем, что выражение X&\^077 не зависит от длины слова и поэтому предпочтительнее, чем, например, X&0177700, где предполага- ется, что х занимает 16 битов. Такая переносимая форма не требует никаких дополнительных затрат, поскольку \^077 явля- ется константным выражением и, следовательно, обрабатывается во время компиляции. Чтобы проиллюстрировать использование некоторых операций с битами, рассмотрим функцию GETBITS(X,P,N), которая возвра- щает /сдвинутыми к правому краю/ начинающиеся с позиции р поле переменной х длиной N битов. мы предполагаем , что крайний правый бит имеет номер 0, и что N и р - разумно за- данные положительные числа. например, GETBITS(х,4,3) возвра- щает сдвинутыми к правому краю биты, занимающие позиции 4,3 и 2.


GETBITS(X,P,N) /* GET N BITS FROM POSITION P */ UNSIGNED X, P, N; { RETURN((X >> (P+1-N)) & \^(\^0 << N)); }

Операция X >> (P+1-N) сдвигает желаемое поле в правый конец слова. Описание аргумента X как UNSIGNED гарантирует, что при сдвиге вправо освобождающиеся биты будут заполняться ну- лями, а не содержимым знакового бита, независимо от того, на какой машине пропускается программа. Все биты константного выражения \^0 равны 1; сдвиг его на N позиций влево с по- мощью операции \^0<<N создает маску с нулями в N крайних правых битах и единицами в остальных; дополнение \^ создает маску с единицами в N крайних правых битах.

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

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

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

--------------- Напишите программу для функции WORDLENGTH(), вычисляющей длину слова используемой машины, т.е. Число битов в перемен- ной типа INT. Функция должна быть переносимой, т.е. Одна и та же исходная программа должна правильно работать на любой машине.

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

--------------- Напишите программу для функции RIGHTROT(N,B), сдвигающей циклически целое N вправо на B битовых позиций.

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

--------------- Напишите программу для функции INVERT(X,P,N), которая инвертирует (т.е. Заменяет 1 на 0 и наоборот) N битов X, на- чинающихся с позиции P, оставляя другие биты неизмененными.




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


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

MAIN() /* COUNT CHARACTERS IN INPUT */ { LONG NC;

NC = 0; WHILE (GETCHAR() != EOF ) ++NC; PRINTF("%1D\N", NC); }

Оператор

++NC;

демонстрирует новую операцию, ++, которая означает увеличе- ние на единицу. Вы могли бы написать NC = NC + 1 , но ++NC более кратко и зачастую более эффективно. Имеется соответст- вующая операция -- уменьшение на единицу. Операции ++ и -- могут быть либо префиксными (++NC), либо постфиксными (NC++); эти две формы, как будет показано в главе 2, имеют в выражениях различные значения, но как ++NC, так и NC++ уве- личивают NC. Пока мы будем придерживаться префиксных опера- ций.

Программа подсчета символов накапливает их количество в переменной типа LONG, а не INT . На PDP-11 максимальное зна- чение равно 32767, и если описать счетчик как INT , то он будет переполняться даже при сравнительно малом файле ввода; на языке "C" для HONEYWELL и IBM типы LONG и INT являются синонимами и имеют значительно больший размер. Спецификация преобразования %1D указывает PRINTF , что соответствующий аргумент является целым типа LONG . Чтобы справиться с еще большими числами, вы можете ис- пользовать тип DOUBLE / FLOAT двойной длины/. мы также ис- пользуем оператор FOR вместо WHILE с тем, чтобы проиллюстри- ровать другой способ записи цикла.

MAIN() /* COUNT CHARACTERS IN INPUT */ { DOUBLE NC;

FOR (NC = 0; GETCHAR() != EOF; ++NC) ; PRINTF("%.0F\N", NC); }

Функция PRINTF использует спецификацию %F как для FLOAT , так и для DOUBLE ; спецификация %.0F подавляет печать не- существующей дробной части. Тело оператора цикла FOR здесь пусто, так как вся рабо- та выполняется в проверочной и реинициализационной частях. Но грамматические правила языка "C" требуют, чтобы оператор FOR имел тело. Изолированная точка с запятой, соответствую- шая пустому оператору, появляется здесь, чтобы удовлетворить этому требованию. Мы выделили ее на отдельную строку, чтобы сделать ее более заметной. Прежде чем мы распростимся с программой подсчета симво- лов, отметим, что если файл ввода не содержит никаких симво- лов, то условие в WHILE или FOR не выполнится при самом пер- вом обращении к GETCHAR , и, следовательно , программа вы- даст нуль, т.е. Правильный ответ. это важное замечание. од- ним из приятных свойств операторов WHILE и FOR является то, что они проверяют условие в начале цикла, т.е. До выполнения тела. Если делать ничего не надо, то ничего не будет сдела- но, даже если это означает, что тело цикла никогда не будет выполняться. программы должны действовать разумно, когда они обращаются с файлами типа "никаких символов". Операторы WHILE и FOR помогают обеспечить правильное поведение прог- рамм при граничных значениях проверяемых условий.



Подсчет слов


Четвертая программа из нашей серии полезных программ подсчитывает количество строк, слов и символов, используя при этом весьма широкое определение, что словом является лю- бая последовательность символов, не содержащая пробелов, та- буляций или новых строк. /Это - упрощенная версия утилиты 'WC' системы 'UNIX'/

#DEFINE YES 1 #DEFINE NO 0

MAIN() /* COUNT LINES, WORDS, CHARS IN INPUT */ { INT C, NL, NW, INWORD;

INWORD = NO; NL = NW = NC = 0; WHILE((C = GETCHAR()) != EOF) { ++NC; IF (C == '\N') ++NL; IF (C==' ' \!\! C=='\N' \!\! C=='\T') INWORD = NO; ELSE IF (INWORD == NO) { INWORD = YES; ++NW; } } PRINTF("%D %D %D\N", NL, NW, NC); }

Каждый раз, когда программа встречает первый символ слова, она увеличивает счетчик числа слов на единицу. Пере- менная INWORD следит за тем, находится ли программа в насто- ящий момент внутри слова или нет; сначала этой переменной присваивается " не в слове", чему соответствует значение NO. Мы предпочитаем символические константы YES и NO литерным значениям 1 и 0, потому что они делают программу более удоб- ной для чтения. Конечно, в такой крошечной программе, как эта, это не приводит к заметной разнице, но в больших прог- раммах увеличение ясности вполне стоит тех скромных дополни- тельных усилий, которых требует следование этому принципу с самого начала. Вы также обнаружите, что существенные измене- ния гораздо легче вносить в те программы, где числа фигури- руют только в качестве символьных констант. Строка

NL = NW = NC = 0;

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

NC = (NL = (NW = 0));

операция \!\! Означает OR , так что строка

IF( C==' ' \!\! C=='\N' \!\! C=='\T')

говорит "если с - пробел, или с - символ новой строки, или с -табуляция ..."./условная последовательность \T является изображением символа табуляции/.


Имеется соответствующая операция && для AND. Выражения, связанные операциями && или \!\! , Рассматриваются слева на право, и при этом гарантируется, что оценивание выражений будет прекращено, как только станет ясно, является ли все выражение истинным или ложным. Так, если 'C' оказывается пробелом, то нет никакой необходимости проверять, является ли 'C' символом новой строки или табуляции, и такие проверки действительно не делаются. В данном случае это не имеет принципиального значения, но, как мы скоро увидим, в более сложных ситуациях эта особенность языка весьма существенна. Этот пример также демонстрирует оператор ELSE языка "C", который указывает то действие, которое должно выполняться, если условие, содержащееся в операторе IF, окажется ложным. Общая форма такова:

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

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

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

---------------- Как бы вы стали проверять программу подсчета слов ? Kакие имеются ограничения ?

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

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

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

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




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


Следующая программа подсчитывает количество строк в файле ввода. Предполагается, что строки ввода заканчиваются символом новой строки \N, скрупулезно добавленным к каждой выписанной строке. MAIN() /* COUNT LINES IN INPUT */ { INT C,NL;

NL = 0; WHILE ((C = GETCHAR()) != EOF) IF (C =='\N') ++NL; PRINTF("%D\N", NL); }

Тело WHILE теперь содержит оператор IF , который в свою очередь управляет оператором увеличения ++NL. Оператор IF проверяет заключенное в круглые скобки условие и, если оно истинно, выполняет следующий за ним оператор /или группу операторов, заключенных в фигурные скобки/. Мы опять исполь- зовали сдвиг вправо, чтобы показать, что чем управляет. Удвоенный знак равенства == является обозначением в языке "C" для "равно" /аналогично .EQ. В фортране/. Этот символ введен для того, чтобы отличать проверку на равенство от одиночного =, используемого при присваивании. Поскольку в типичных "C" - программах знак присваивания встречается при- мерно в два раза чаще, чем проверка на равенство, то естест- венно, чтобы знак оператора был вполовину короче. Любой отдельный символ может быть записан внутри оди- ночных кавычек, и при этом ему соответствует значение, рав- ное численному значению этого символа в машинном наборе сим- волов; это называется символьной константой. Так, например, 'A' - символьная константа; ее значение в наборе символов ASCII /американский стандартный код для обмена информацией/ равно 65, внутреннему представлению символа а. Конечно, 'A' предпочтительнее, чем 65: его смысл очевиден и он не зависит от конкретного машинного набора символов. Условные последовательности, используемые в символьных строках, также занимают законное место среди символьных кон- стант. Так в проверках и арифметических выражениях '\N' представляет значение символа новой строки. Вы должны твердо уяснить, что '\N' - отдельный символ, который в выражениях эквивалентен одиночному целому; с другой стороны "\N" - это символьная строка, которая содержит только один символ. Воп- рос о сопоставлении строк и символов обсуждается в главе 2.

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

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

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

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



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


Для иллюстрации дальнейших аспектов использования струк- тур в этом разделе мы напишем программу, представляющую со- бой содержимое пакета поиска в таблице. Эта программа явля- ется типичным представителем подпрограмм управления символь- ными таблицами макропроцессора или компилятора. Рассмотрим, например, оператор #DEFINE языка "C". Когда встречается строка вида

#DEFINE YES 1

то имя YES и заменяющий текст 1 помещаются в таблицу. Позд- нее, когда имя YES появляется в операторе вида

INWORD = YES;

Oно должно быть замещено на 1. Имеются две основные процедуры, которые управляют имена- ми и заменяющими их текстами. Функция INSTALL(S,T) записыва- ет имя S и заменяющий текст T в таблицу; здесь S и T просто символьные строки. Функция LOOKUP(S) ищет имя S в таблице и возвращает либо указатель того места, где это имя найдено, либо NULL, если этого имени в таблице не оказалось. При этом используется поиск по алгоритму хеширования - поступающее имя преобразуется в маленькое положительное чис- ло, которое затем используется для индексации массива указа- телей. Элемент массива указывает на начало цепочных блоков, описывающих имена, которые имеют это значение хеширования. Если никакие имена при хешировании не получают этого значе- ния, то элементом массива будет NULL. Блоком цепи является структура, содержащая указатели на соответствующее имя, на заменяющий текст и на следующий блок в цепи. Нулевой указатель следующего блока служит признаком конца данной цепи.

STRUCT NLIST \( /* BASIC TABLE ENTRY */ CHAR *NAME; CHAR *DEF; STRUCT NLIST *NEXT; /* NEXT ENTRY IN CHAIN */ \);

Массив указателей это просто

DEFINE HASHSIZE 100 TATIC STRUCT NLIST *HASHTAB[HASHSIZE] /* POINTER TABLE */

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

HASH(S) /* FORM HASH VALUE FOR STRING */ CHAR *S; \( INT HASHVAL;


FOR (HASHVAL = 0; *S != '\0'; ) HASHVAL += *S++; RETURN(HASHVAL % HASHSIZE); \)

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

STRUCT NLIST *LOOKUP(S) /* LOOK FOR S IN HASHTAB */ CHAR *S; \( STRUCT NLIST *NP;

FOR (NP = HASHTAB[HASH(S)]; NP != NULL;NP=NP->NEXT) IF (STRCMP(S, NP->NAME) == 0) RETURN(NP); /* FOUND IT */ RETURN(NULL); /* NOT FOUND */

Функция INSTALL использует функцию LOOKUP для определе- ния, не присутствует ли уже вводимое в данный момент имя; если это так, то новое определение должно вытеснить старое. В противном случае создается совершенно новый элемент. Если по какой-либо причине для нового элемента больше нет места, то функция INSTALL возвращает NULL.

STRUCT NLIST *INSTALL(NAME, DEF) /* PUT (NAME, DEF) */ CHAR *NAME, *DEF; \( STRUCT NLIST *NP, *LOOKUP(); CHAR *STRSAVE(), *ALLOC(); INT HASHVAL;

IF((NP = LOOKUP(NAME)) == NULL) \( /* NOT FOUND */ NP = (STRUCT NLIST *) ALLOC(SIZEOF(*NP)); IF (NP == NULL) RETURN(NULL); IF ((NP->NAME = STRSAVE(NAME)) == NULL) RETURN(NULL); HASHVAL = HASH(NP->NAME); NP->NEXT = HASHTAB[HASHVAL]; HASHTAB[HASHVAL] = NP; \) ELSE /* ALREADY THERE */ FREE((NP->DEF);/* FREE PREVIOUS DEFINITION */ IF ((NP->DEF = STRSAVE(DEF)) == NULL) RETURN (NULL); RETURN(NP); \)

Функция STRSAVE просто копирует строку, указанную в ка- честве аргумента, в место хранения, полученное в результате обращения к функции ALLOC. Мы уже привели эту функцию в гла- ве 5. Так как обращение к функции ALLOC и FREE могут проис- ходить в любом порядке и в связи с проблемой выравнивания, простой вариант функции ALLOC из главы 5 нам больше не под- ходит; смотрите главы 7 и 8.

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

--------------- Напишите процедуру, которая будет удалять имя и опреде- ление из таблицы, управляемой функциями LOOKUP и INSTALL.

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

--------------- Разработайте простую, основанную на функциях этого раз- дела, версию процессора для обработки конструкций #DEFINE , пригодную для использования с "C"-программами. Вам могут также оказаться полезными функции GETCHAR и UNGETCH.




Поля


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

#DEFINE KEYWORD 01 #DEFINE EXTERNAL 02 #DEFINE STATIC 04

(числа должны быть степенями двойки). Тогда обработка битов сведется к "жонглированию битами" с помощью операций сдвига, маскирования и дополнения, описанных нами в главе 2. Некоторые часто встречающиеся идиомы:

FLAGS \!= EXTERNAL \! STATIC;

включает биты EXTERNAL и STATIC в FLAGS, в то время как

FLAGS &= \^(еXTERNAL \! STATIC);

их выключает, а

IF ((FLAGS & (EXTERNAL \! STATIC)) == 0) ...

истинно, если оба бита выключены. Хотя этими идиомами легко овладеть, язык "C" в качестве альтернативы предлагает возможность определения и обработки полей внутри слова непосредственно, а не посредством побито- вых логических операций. Поле - это набор смежных битов внутри одной переменной типа INT. Синтаксис определения и обработки полей основывается на структурах. Например, сим- вольную таблицу конструкций #DEFINE, приведенную выше, можно бы было заменить определением трех полей:

STRUCT \( UNSIGNED IS_KEYWORD : 1; UNSIGNED IS_EXTERN : 1; UNSIGNED IS_STATIC : 1; \) FLAGS;


Здесь определяется переменная с именем FLAGS, которая содер- жит три 1-битовых поля. Следующее за двоеточием число задает ширину поля в битах. Поля описаны как UNSIGNED, чтобы под- черкнуть, что они действительно будут величинами без знака. На отдельные поля можно ссылаться, как FLAGS.IS_STATIE, FLAGS. IS_EXTERN, FLAGS.IS_KEYWORD И т.д., то есть точно так же, как на другие члены структуры. Поля ведут себя подобно небольшим целым без знака и могут участвовать в арифметичес- ких выражениях точно так же, как и другие целые. Таким обра- зом, предыдущие примеры более естественно переписать так:

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 1;

для включения битов;

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 0;

для выключения битов;

IF (FLAGS.IS_EXTERN == 0 &&FLAGS.IS_STATIC == 0)...

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

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




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


Перед любым оператором может стоять помеченный префикс вида идентификатор:

который служит для описания идентификатора в качестве метки. Метки используются только для указания места, куда передает- ся управление оператором GOTO. Областью действия метки явля- ется данная функция, за исключением любых подблоков, в кото- рых тот же идентификатор описан снова. Смотри п. 19.



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


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



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


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


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



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


Если в выражениях встречаются операнды различных типов, то они преобразуются к общему типу в соответствии с неболь- шим набором правил. В общем, автоматически производятся только преобразования, имеющие смысл, такие как, например, преобразование целого в плавающее в выражениях типа F+I. Вы- ражения же, лишенные смысла, такие как использование пере- менной типа FLOAT в качестве индекса, запрещены. Во-первых, типы CHAR и INT могут свободно смешиваться в арифметических выражениях: каждая переменная типа CHAR авто- матически преобразуется в INT. Это обеспечивает значительную гибкость при проведении определенных преобразований симво- лов. Примером может служить функция ATOI, которая ставит в соответствие строке цифр ее численный эквивалент. ATOI(S) /* CONVERT S TO INTEGER */ CHAR S[]; { INT I, N;

N = 0; FOR ( I = 0; S[I]>='0' && S[I]<='9'; ++I) N = 10 * N + S[I] - '0'; RETURN(N); }

KAK Уже обсуждалось в главе 1, выражение

S[I] - '0'

имеет численное значение находящегося в S[I] символа, потому что значение символов '0', '1' и т.д. образуют возрастающую последовательность расположенных подряд целых положительных чисел. Другой пример преобразования CHAR в INT дает функция LOWER, преобразующая данную прописную букву в строчную. Если выступающий в качестве аргумента символ не является пропис- ной буквой, то LOWER возвращает его неизменным. Приводимая ниже программа справедлива только для набора символов ASCII.

LOWER(C) /* CONVERT C TO LOWER CASE; ASCII ONLY */ INT C; { IF ( C >= 'A' && C <= 'Z' ) RETURN( C + '@' - 'A'); ELSE /*@ Записано вместо 'A' строчного*/ RETURN(C); }

Эта функция правильно работает при коде ASCII, потому что численные значения, соответствующие в этом коде прописным и строчным буквам, отличаются на постоянную величину, а каждый алфавит является сплошным - между а и Z нет ничего, кроме букв. Это последнее замечание для набора символов EBCDIC систем IBM 360/370 оказывается несправедливым, в силу чего эта программа на таких системах работает неправильно - она преобразует не только буквы. При преобразовании символьных переменных в целые возни- кает один тонкий момент. Дело в том, что сам язык не указы- вает, должны ли переменным типа CHAR соответствовать числен- ные значения со знаком или без знака. Может ли при преобра- зовании CHAR в INT получиться отрицательное целое? К сожале- нию, ответ на этот вопрос меняется от машины к машине, отра- жая расхождения в их архитектуре. На некоторых машинах (PDP-11, например) переменная типа CHAR, крайний левый бит которой содержит 1, преобразуется в отрицательное целое ("знаковое расширение"). На других машинах такое преобразо- вание сопровождается добавлением нулей с левого края, в ре- зультате чего всегда получается положительное число.


Определение языка "C" гарантирует, что любой символ из стандартного набора символов машины никогда не даст отрица- тельного числа, так что эти символы можно свободно использо- вать в выражениях как положительные величины. Но произволь- ные комбинации двоичных знаков, хранящиеся как символьные переменные на некоторых машинах, могут дать отрицательные значения, а на других положительные. Наиболее типичным примером возникновения такой ситуации является сучай, когда значение 1 используется в качестве EOF. Рассмотрим программу

CHAR C; C = GETCHAR(); IF ( C == EOF ) ...

На машине, которая не осуществляет знакового расширения, переменная 'с' всегда положительна, поскольку она описана как CHAR, а так как EOF отрицательно, то условие никогда не выполняется. Чтобы избежать такой ситуации, мы всегда пре- дусмотрительно использовали INT вместо CHAR для любой пере- менной, получающей значение от GETCHAR. Основная же причина использования INT вместо CHAR не связана с каким-либо вопросом о возможном знаковом расшире- нии. просто функция GETCHAR должна передавать все возможные символы (чтобы ее можно было использовать для произвольного ввода) и, кроме того, отличающееся значение EOF. Следова- тельно значение EOF не может быть представлено как CHAR, а должно храниться как INT. Другой полезной формой автоматического преобразования типов является то, что выражения отношения, подобные I>J, и логические выражения, связанные операциями && и \!\!, по оп- ределению имеют значение 1, если они истинны, и 0, если они ложны. Таким образом, присваивание

ISDIGIT = C >= '0' && C <= '9';

полагает ISDIGIT равным 1, если с - цифра, и равным 0 в про- тивном случае. (В проверочной части операторов IF, WHILE, FOR и т.д. "Истинно" просто означает "не нуль"). Неявные арифметические преобразования работают в основ- ном, как и ожидается. В общих чертах, если операция типа + или *, которая связывает два операнда (бинарная операция), имеет операнды разных типов, то перед выполнением операции "низший" тип преобразуется к "высшему" и получается резуль- тат "высшего" типа. Более точно, к каждой арифметической операции применяется следующая последовательность правил преобразования. - Типы CHAR и SHORT преобразуются в INT, а FLOAT в DOUBLE.



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

INT I; CHAR C;

I = C; C = I;

значение 'с' не изменяется. Это верно независимо от того, вовлекается ли знаковое расширение или нет. Если х типа FLOAT, а I типа INT, то как

х = I; так и I = х;

приводят к преобразованиям; при этом FLOAT преобразуется в INT отбрасыванием дробной части. Тип DOUBLE преобразуется во FLOAT округлением. Длинные целые преобразуются в более ко- роткие целые и в переменные типа CHAR посредством отбрасыва- ния лишних битов высокого порядка. Так как аргумент функции является выражением, то при пе- редаче функциям аргументов также происходит преобразование типов: в частности, CHAR и SHORT становятся INT, а FLOAT становится DOUBLE. Именно поэтому мы описывали аргументы функций как INT и DOUBLE даже тогда, когда обращались к ним с переменными типа CHAR и FLOAT. Наконец, в любом выражении может быть осуществлено ("принуждено") явное преобразование типа с помощью конструк- ции, называемой перевод (CAST). В этой конструкции, имеющей вид

(имя типа) выражение Выражение преобразуется к указанному типу по правилам преобразования, изложенным выше. Фактически точный смысл операции перевода можно описать следующим образом: выражение как бы присваивается некоторой переменной указанного типа, которая затем используется вместо всей конструкции. Напри- мер, библиотечная процедура SQRT ожидает аргумента типа DOUBLE и выдаст бессмысленный ответ, если к ней по небреж- ности обратятся с чем-нибудь иным. таким образом, если N - целое, то выражение

SQRT((DOUBLE) N)

до передачи аргумента функции SQRT преобразует N к типу DOUBLE. (Отметим, что операция перевод преобразует значение N в надлежащий тип; фактическое содержание переменной N при этом не изменяется). Операция перевода имрация перевода име- ет тот же уровень старшинства, что и другие унарные опера- ции, как указывается в таблице в конце этой главы.

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

--------------- Составьте программу для функции HTOI(S), которая преоб- разует строку шестнадцатеричных цифр в эквивалентное ей це- лое значение. При этом допустимыми цифрами являются цифры от 1 до 9 и буквы от а до F.




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


Ряд операций может в зависимости от своих операндов вы- зывать преобразование значение операнда из одного типа в другой. В этом разделе объясняются результаты, которые сле- дует ожидать от таких преобразований. В п. 14.6 Подводятся итоги преобразований, требуемые большинством обычных опера- ций; эти сведения дополняются необходимым образом при обсуж- дении каждой операции.



Препроцессор


#DEFINE идентификатор строка-лексем #DEFINE #DEFINE идентификатор(идентификатор,...,идентификатор)стр #UNDEF идентификатор #INCLUDE "имя-файла" #INCLUDE <имя-файла>

#IF константное-выражение #IFDEF идентификатор #IFNDEF идентификатор #ELSE #ENDIF #LINE константа идентификатор

Последние изменения языка "C" (15 ноября 1978 г.)

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

спецификатор-перечисления -------------------------

с синтаксисом

пецификатор-перечисления: ------------------------- ENUM список-перечисления ------------------- ENUM идентификатор список-перечисления ------------- ------------------- ENUM идентификатор ------------- cписок-перечисления: ------------------- перечисляемое ------------- список-перечисления, перечисляемое ------------------- ------------- перечисляемое: -------------- идентификатор ------------- идентификатор = константное выражение ------------- ---------------------

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


ENUM COLOR \(RED, WHITE, BLACK, BLUE \); . . . ENUM COLOR *CP, COL;

Объявляет идентификатор COLOR ярлыком перечисления типа, описывающего различные цвета и затем объявляет CP указателем на объект этого типа, а COL - объектом этого типа. Идентификаторы в списке-перечисления описываются как константы и могут появиться там, где требуются (по контекс- ту) константы. Если не используется вторая форма перечисляе- мого (с равеством =), то величины констант начинаются с 0 и возрастают на 1 в соответствии с прочтением их описания сле- ва на право. Перечисляемое с присвоением = придает соответс- твующему идентификатору указанную величину; последующие идентификаторы продолжают прогрессию от приписанной величи- ны. Все ярлыки перечисления и константы могут быть различны- ми и непохожими на ярлыки и члены структур даже при условии использования одного и того же множества идентификаторов. Объекты данного типа перечисления рассматриваются как объекты, имеющие тип, отличный от любых типов и контролирую- щая программа LINT сообщает об ошибках несоответствия типов. В реализации на PDP-11 со всеми перечисляемыми переменными оперируют так, как если бы они имели тип INT. 29. Таблица изображений непечатных символов языка "C". В данной таблице приведены изображения некоторых симво- лов (фигурные скобки и т.д.) языка "C", которых может не оказаться в знаковом наборе дисплея или печатающего устройс- тва. ------------------------------------------------- ! Значение ! Изображение ** ! ! ! В тексте ! ------------------------------------------------- ! Фигурная открывающаяся ! ! ! Скобка ! \( ! ! ! ! ------------------------------------------------- ! Фигурная закрывающаяся ! ! ! Скобка ! \) ! ! ! ! ------------------------------------------------- ! Вертикальная ! ! ! Черта ! \! ! ! ! ! ------------------------------------------------- ! ! ! ! Апостороф ! \' ! ! ! ! ------------------------------------------------- ! Волнистая ! ! ! Черта ! \^ ! ! ! ! -------------------------------------------------

** П_р_и_м_е_ч_а_н_и_е: Изображения приведены для операционой системы UNIX. При работе компилятора "C" под управлением любой другой операци- онной системы, необходимо воспользоваться соответствующим руководством для данной системы. Обращений с начала месяца: 276, Last-modified: Sun, 31 Aug 2003 05:43:18 GMT Оцените этот текст:Не читал10987654321


Препроцессор языка "C"


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



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


Иногда требуется другой вид взаимодействия с системой файлов - определение информации о файле, а не того, что в нем содержится. Примером может служить команда LS ("список справочника") системы UNIX. По этой команде распечатываются имена файлов из справочника и, необязательно, другая инфор- мация, такая как размеры, разрешения и т.д. Поскольку, по крайней мере, на системе UNIX справочник является просто файлом, то в такой команде, как LS нет ниче- го особенного; она читает файл и выделяет нужные части из находящейся там информации. Однако формат информации опреде- ляется системой, так что LS должна знать, в каком виде все представляется в системе. Мы это частично проиллюстрируем при написании программы FSIZE. Программа FSIZE представляет собой специальную форму LS, которая печатает размеры всех файлов, указанных в списке ее аргументов. Если один из файлов является справочником, то для обработки этого справочника программа FSIZE обращается сама к себе рекурсивно. если же аргументы вообще отсутству- ют, то обрабатывается текущий справочник. Для начала дадим краткий обзор структуры системы файлов. Справочник - это файл, который содержит список имен файлов и некоторое указание о том, где они размещаются. Фактически это указание является индексом для другой таблицы, которую называют "I - узловой таблицей". Для файла I-узел - это то,

где содержится вся информация о файле, за исключением его имени. Запись в справочнике состоит только из двух элемен- тов: номера I-узла и имени файла. Точная спецификация посту- пает при включении файла SYS/DIR.H, который содержит

#DEFINE DIRSIZ 14 /*MAX LENGTH OF FILE NAME*/ STRUCT DIRECT /*STRUCTURE OF DIRECTORY ENTRY*/ \( INO_T&_INO; /*INODE NUMBER*/ CHAR &_NAME[DIRSIZ]; /*FILE NAME*/ \);

"Тип" INO_T - это определяемый посредством TYPEDEF тип, который описывает индекс I-узловой таблицы. На PDP-11 UNIX этим типом оказывается UNSIGNED, но это не тот сорт информа- ции, который помещают внутрь программы: на разных системах этот тип может быть различным. Поэтому и следует использо- вать TYPEDEF. Полный набор "системных" типов находится в файле SYS/TUPES.H. Функция STAT берет имя файла и возвращает всю содержащу- юся в I-ом узле информацию об этом файле (или -1, если име- ется ошибка). Таким образом, в результате


STRUCT STAT STBUF; CHAR *NAME; STAT(NAME,&STBUF);

структура STBUF наполняется информацией из I-го узла о файле с именем NAME. Структура, описывающая возвращаемую функцией STAT информацию, находится в файле SYS/STAT.H и выглядит следующим образом:

STRUCT STAT /*STRUCTURE RETURNED BY STAT*/ \( DEV_T ST_DEV; /* DEVICE OF INODE */ INO_T ST_INO; /* INODE NUMBER */ SHORT ST_MODE /* MODE BITS */ SHORT ST_NLINK; / *NUMBER OF LINKS TO FILE */ SHORT ST_UID; /* OWNER'S USER ID */ SHORT ST_GID; /* OWNER'S GROUP ID */ DEV_T ST_RDEV; /* FOR SPECIAL FILES */ OFF_T ST_SIZE; /* FILE SIZE IN CHARACTERS */ TIME_T ST_ATIME; /* TIME LAST ACCESSED */ TIME_T ST_MTIME; /* TIME LAST MODIFIED */ TIME_T ST_CTIME; /* TIME ORIGINALLY CREATED */ \)

Большая часть этой информации объясняется в комментариях. Элемент ST.MODE содержит набор флагов, описывающих файл; для удобства определения флагов также находятся в файле SYS/STAT.H. #DEFINE S_IFMT 0160000 /* TYPE OF FILE */ #DEFINE S_IFDIR 0040000 /* DIRECTORY */ #DEFINE S_IFCHR 0020000 /* CHARACTER SPECIAL */ #DEFINE S_IFBLK 0060000 /* BLOCK SPECIAL */ #DEFINE S_IFREG 0100000 /* REGULAR */ #DEFINE S_ISUID 04000 /* SET USER ID ON EXECUTION */ #DEFINE S_ISGID 02000 /* SET GROUP ID ON EXECUTION */ #DEFINE S_ISVTX 01000 /*SAVE SWAPPED TEXT AFTER USE*/ #DEFINE S_IREAD 0400 /* READ PERMISSION */ #DEFINE S_IWRITE 0200 /* WRITE PERMISSION */ #DEFINE S_IEXEC 0100 /* EXECUTE PERMISSION */

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



#INCLUDE <STDIO.H.>

#INCLUDE <SYS/TYPES.H> /*TYPEDEFS*/ #INCLUDE <SYS/DIR.H> /*DIRECTORY ENTRY STRUCTURE*/ #INCLUDE <SYS/STAT.H> /*STRUCTURE RETURNED BY STAT*/ #DEFINE BUFSIZE 256 MAIN(ARGC,ARGV) /*FSIZE:PRINT FILE SIZES*/ CHAR *ARGV[]; \( CHAR BUF[BUFSIZE]; IF(ARGC==1) \( /*DEFAULT:CURRENT DIRECTORY*/ ATRCPY(BUF,"."); FSIZE(BUF); \) ELSE WHILE(--ARGC>0) \( STRCPY(BUF,*++ARGV); FSIZE(BUF); \) \)

Функция FSIZE печатает размер файла. Если однако файл оказывается справочником, то FSIZE сначала вызывает функцию DIRECTORY для обработки всех указанных в нем файлов. Обрати- те внимание на использование имен флагов S_IFMT и _IFDIR из файла STAT.H.

FSIZE(NAME) /*PRINT SIZE FOR NAME*/ CHAR *NAME; \( STRUCT STAT STBUF; IF(STAT(NAME,&STBUF)== -1) \( FPRINTF(STDERR,"FSIZE:CAN'T FIND %S\N",NAME); RETURN; \) IF((STBUF.ST_MODE & S_IFMT)==S_IFDIR) DIRECTORY(NAME); PRINTF("%8LD %S\N",STBUF.ST_SIZE,NAME); \) Функция DIRECTORY является самой сложной. Однако значи- тельная ее часть связана с созданием для обрабатываемого в данный момент файла его полного имени, по которому можно восстановить путь в дереве.

DIRECTORY(NAME) /*FSIZE FOR ALL FILES IN NAME*/ CHAR *NAME; ( STRUCT DIRECT DIRBUF; CHAR *NBP, *NEP; INT I, FD; NBP=NAME+STRLEN(NAME); *NBP++='/'; /*ADD SLASH TO DIRECTORY NAME*/ IF(NBP+DIRSIZ+2>=NAME+BUFSIZE) /*NAME TOO LONG*/ RETURN; IF((FD=OPEN(NAME,0))== -1) RETURN; WHILE(READ(FD,(CHAR *)&DIRBUF,SIZEOF(DIRBUF))>0) \( IF(DIRBUF.D_INO==0) /*SLOT NOT IN USE*/ CONTINUE; IF(STRCMP (DIRBUF.D_NAME,".")==0 \!\! STRCMP(DIRBUF.D_NAME,"..")==0 CONTINUE; /*SKIP SELF AND PARENT*/ FOR (I=0,NEP=NBP;I<DIRSIZ;I++) *NEP++=DIRBUF.D_NAME[I]; *NEP++='\0'; FSIZE(NAME); \) CLOSE(FD); *--NBP='\0'; /*RESTORE NAME*/ )

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

Хотя программа FSIZE довольно специализированна, она все же демонстрирует пару важных идей. во-первых, многие прог- раммы не являются "системными программами"; они только ис- пользуют информацию, форма или содержание которой определя- ется операционной системой. Во-вторых, для таких программ существенно, что представление этой информации входит только в стандартные "заголовочные файлы", такие как STAT.H и DIR.H, и что программы включают эти файлы, а не помещают фактические описания внутрь самих программ.




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


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


Одна из проблем, о которой мы упоминали в главе 5, зак- лючается в обеспечении того, чтобы возвращаемая функцией ALLOC память была выровнена подходящим образом для тех объектов, которые будут в ней храниться. Хотя машины и раз- личаются, для каждой машины существует тип, требующий наи- больших ограничений по размещению памяти, если данные самого ограничительного типа можно поместить в некоторый определен- ный адрес, то это же возможно и для всех остальных типов. Например, на IBM 360/370,HONEYWELL 6000 и многих других ма- шинах любой объект может храниться в границах, соответствую- щим переменным типа DOUBLE; на PDP-11 будут достаточны пере- менные типа INT. Свободный блок содержит указатель следующего блока в це- почке, запись о размере блока и само свободное пространство; управляющая информация в начале называется заголовком. Для упрощения выравнивания все блоки кратны размеру заголовка, а сам заголовок выровнен надлежащим образом. Это достигается с помощью объединения, которое содержит желаемую структуру за- головка и образец наиболее ограничительного по выравниванию типа:

TYPEDEF INT ALIGN; /*FORCES ALIGNMENT ON PDP-11*/ UNION HEADER \( /*FREE BLOCK HEADER*/ STRUCT \( UNION HEADER *PTR; /*NEXT FREE BLOCK*/ UNSIGNED SIZE; /*SIZE OF THIS FREE BLOCK*/ \) S; ALIGN X; /*FORCE ALIGNMENT OF BLOCKS*/ \); TYPEDEF UNION HEADER HEADER;

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

STATIC HEADER BASE; /*EMPTY LIST TO GET STARTED*/ STATIC HEADER *ALLOCP=NULL; /*LAST ALLOCATED BLOCK*/ CHAR *ALLOC(NBYTES)/*GENERAL-PURPOSE STORAGE ALLOCATOR*/ UNSIGNED NBYTES; \( HEADER *MORECORE(); REGISTER HEADER *P, *G; REGISTER INT NUNITS; NUNITS=1+(NBYTES+SIZEOF(HEADER)-1)/SIZEOF(HEADER); IF ((G=ALLOCP)==NULL) \( /*NO FREE LIST YET*/ BASE.S PTR=ALLOCP=G=&BASE; BASE.S.SIZE=0; \)



FOR (P=G>S.PTR; ; G=P, P=P->S.PTR) \( IF (P->S.SIZE>=NUNITS) \( /*BIG ENOUGH*/ IF (P->S.SIZE==NUNITS) /*EXACTLY*/ G->S.PTR=P->S.PTR; ELSE \( /*ALLOCATE TAIL END*/ P->S.SIZE-=NUNITS; P+=P->S.SIZE; P->S.SIZE=NUNITS; \) ALLOCP=G; RETURN((CHAR *)(P+1)); \) IF(P==ALLOCP) /*WRAPPED AROUND FREE LIST*/ IF((P=MORECORE(NUNITS))==NULL) RETURN(NULL); /*NONE LEFT*/ \) \)

Переменная BASE используется для начала работы. Если ALLOCP имеет значение NULL, как в случае первого обращения к ALLOC, то создается вырожденный свободный список: он состоит из свободного блока размера нуль и указателя на самого себя. В любом случае затем исследуется свободный список. Поиск свободного блока подходящего размера начинается с того места (ALLOCP), где был найден последний блок; такая стратегия по- могает сохранить однородность диска. Если найден слишком большой блок, то пользователю предлагается его хвостовая часть; это приводит к тому, что в заголовке исходного блока нужно изменить только его размер. Во всех случаях возвращае- мый пользователю указатель указывает на действительно сво- бодную область, лежащую на единицу дальше заголовка. Обрати- те внимание на то, что функция ALLOC перед возвращением "P" преобразует его в указатель на символы. Функция MORECORE получает память от операционной систе- мы. Детали того, как это осуществляется, меняются, конечно, от системы к системе. На системе UNIX точка входа SBRK(N) возвращает указатель на "N" дополнительных байтов памя- ти.(указатель удволетворяет всем ограничениям на выравнива- ние). Так как запрос к системе на выделение памяти является сравнительно дорогой операцией, мы не хотим делать это при каждом обращении к функции ALLOC. Поэтому функция MORECORE округляет затребованное число единиц до большего значения; этот больший блок будет затем разделен так, как необходимо. Масштабирующая величина является параметром, который может быть подобран в соответствии с необходимостью.

#DEFINE NALLOC 128 /*#UNITS TO ALLOCATE AT ONCE*/ STATIC HEADER *MORECORE(NU) /*ASK SYSTEM FOR MEMORY*/ UNSIGNED NU; \( CHAR *SBRK(); REGISTER CHAR *CP; REGISTER HEADER *UP; REGISTER INT RNU; RNU=NALLOC*((NU+NALLOC-1)/NALLOC); CP=SBRK(RNU*SIZEOF(HEADER)); IF ((INT)CP==-1) /*NO SPACE AT ALL*/ RETURN(NULL); UP=(HEADER *)CP; UP->S.SIZE=RNU; FREE((CHAR *)(UP+1)); RETURN(ALLOCP); \)



Если больше не осталось свободного пространства, то фун- кция SBRK возвращает "-1", хотя NULL был бы лучшим выбором. Для надежности сравнения "-1" должна быть преобразована к типу INT. Снова приходится многократно использовать явные преобразования (перевод) типов, чтобы обеспечить определен- ную независимость функций от деталей представления указате- лей на различных машинах. И последнее - сама функция FREE. Начиная с ALLOCP, она просто просматривает свободный список в поиске места для введения свободного блока. Это место находится либо между двумя существующими блоками, либо в одном из концов списка. В любом случае, если освободившийся блок примыкает к одному из соседних, смежные блоки объединяются. Следить нужно толь- ко затем, чтобы указатели указывали на то, что нужно, и что- бы размеры были установлены правильно.

FREE(AP) /*PUT BLOCKE AP IN FREE LIST*/ CHAR *AP; \( REGISTER HEADER *P, *G; P=(HEADER*)AP-1; /*POINT TO HEADER*/ FOR (G=ALLOCP; !(P>G && P>G->S.PTR);G=G->S.PTR) IF (G>=G->S.PTR && (P>G \!\! P<G->S.PTR)) BREAK; /*AT ONE END OR OTHER*/ IF (P+P->S.SIZE==G->S.PTR)\(/*JOIN TO UPPER NBR*/ P->S.SIZE += G->S.PTR->S.SIZE; P->S.PTR = G->S.PTR->S.PTR; \) ELSE P->S.PTR = G->S.PTR; IF (G+G->S.SIZE==P) \( /*JOIN TO LOWER NBR*/ G->S.SIZE+=P->S.SIZE; G->S.PTR=P->S.PTR; \) ELSE G->S.PTR=P; ALLOCP = G; \)

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



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

-------------- Функция из стандартной библиотеки CALLOC(N,SIZE) возвра- щает указатель на "N" объектов размера SIZE, причем соответ- ствующая память инициализируется на нуль. напишите программу для CALLOC, используя функцию ALLOC либо в качестве образца, либо как функцию, к которой происходит обращение.

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

--------------- Функция ALLOC принимает затребованный размер, не прове- ряя его правдоподобности; функция FREE полагает, что тот блок, который она должна освободить, содержит правильное значение в поле размера. Усовершенствуйте эти процедуры, затратив больше усилий на проверку ошибок.

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

--------------- Напишите функцию BFREE(P,N), которая включает произволь- ный блок "P" из "N" символов в список свободных блоков, уп- равляемый функциями ALLOC и FREE. С помощью функции BFREE пользователь может в любое время добавлять в свободный спи- сок статический или внешний массив.




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


Давайте теперь на примере реализации функций FOPEN и GETC из стандартной библиотеки подпрограмм продемонстрируем, как некоторые из описанных элементов объединяются вместе. Напомним, что в стандартной библиотеке файлы описыватся посредством указателей файлов, а не дескрипторов. Указатель файла является указателем на структуру, которая содержит несколько элементов информации о файле: указатель буфера, чтобы файл мог читаться большими порциями; счетчик числа символов, оставшихся в буфере; указатель следующей позиции символа в буфере; некоторые признаки, указывающие режим чте- ния или записи и т.д.; дескриптор файла. Описывающая файл структура данных содержится в файле STDIO.H, который должен включаться (посредством #INCLUDE) в любой исходный файл, в котором используются функции из стан- дартной библиотеки. Он также включается функциями этой биб- лиотеки. В приводимой ниже выдержке из файла STDIO.H имена, предназначаемые только для использования функциями библиоте- ки, начинаются с подчеркивания, с тем чтобы уменьшить веро- ятность совпадения с именами в программе пользователя.

DEFINE _BUFSIZE 512 DEFINE _NFILE 20 /*FILES THAT CAN BE HANDLED*/ TYPEDEF STRUCT _IOBUF \( CHAR *_PTR; /*NEXT CHARACTER POSITION*/ INT _CNT; /*NUMBER OF CHARACTERS LEFT*/ CHAR *_BASE; /*LOCATION OF BUFFER*/ INT _FLAG; /*MODE OF FILE ACCESS*/ INT _FD; /*FILE DESCRIPTOR*/ ) FILE; XTERN FILE _IOB[_NFILE];

DEFINE STDIN (&_IOB[0]) DEFINE STDOUT (&_IOB[1]) DEFINE STDERR (&_IOB[2])

DEFINE _READ 01 /* FILE OPEN FOR READING */ DEFINE _WRITE 02 /* FILE OPEN FOR WRITING */ DEFINE _UNBUF 04 /* FILE IS UNBUFFERED */ DEFINE _BIGBUF 010 /* BIG BUFFER ALLOCATED */ DEFINE _EOF 020 /* EOF HAS OCCURRED ON THIS FILE */ DEFINE _ERR 040 /* ERROR HAS OCCURRED ON THIS FILE */ DEFINE NULL 0 DEFINE EOF (-1)

DEFINE GETC(P) (--(P)->_CNT >= 0 \ ? *(P)->_PTR++ & 0377 : _FILEBUF(P)) DEFINE GETCHAR() GETC(STDIN)

DEFINE PUTC(X,P) (--(P)->_CNT >= 0 \ ? *(P)->_PTR++ = (X) : _FLUSHBUF((X),P)) DEFINE PUTCHAR(X) PUTC(X,STDOUT)


В нормальном состоянии макрос GETC просто уменьшает счетчик, передвигает указатель и возвращает символ. (Если определение #DEFINE слишком длинное, то оно продолжается с помощью обратной косой черты). Если однако счетчик становит- ся отрицательным, то GETC вызывает функцию _FILEBUF, которая снова заполняет буфер, реинициализирует содержимое структуры и возвращает символ. Функция может предоставлять переносимый интерфейс и в то же время содержать непереносимые конструк- ции: GETC маскирует символ числом 0377, которое подавляет знаковое расширение, осуществляемое на PDP-11, и тем самым гарантирует положительность всех символов. Хотя мы не собираемся обсуждать какие-либо детали, мы все же включили сюда определение макроса PUTC, для того что- бы показать, что она работает в основном точно также, как и GETC, обращаясь при заполнении буфера к функции _FLUSHBUF. Теперь может быть написана функция FOPEN. Большая часть программы функции FOPEN связана с открыванием файла и распо- ложением его в нужном месте, а также с установлением битов признаков таким образом, чтобы они указывали нужное состоя- ние. Функция FOPEN не выделяет какой-либо буферной памяти; это делается функцией _FILEBUF при первом чтении из файла.

#INCLUDE <STDIO.H>

#DEFINE PMODE 0644 /*R/W FOR OWNER;R FOR OTHERS*/ FILE *FOPEN(NAME,MODE) /*OPEN FILE,RETURN FILE PTR*/ REGISTER CHAR *NAME, *MODE; \( REGISTER INT FD; REGISTER FILE *FP; IF(*MODE !='R'&&*MODE !='W'&&*MODE !='A') \( FPRINTF(STDERR,"ILLEGAL MODE %S OPENING %S\N", MODE,NAME); EXIT(1); \) FOR (FP=_IOB;FP<_IOB+_NFILE;FP++) IF((FP->_FLAG & (_READ \! _WRITE))==0) BREAK; /*FOUND FREE SLOT*/ IF(FP>=_IOB+_NFILE) /*NO FREE SLOTS*/ RETURN(NULL); IF(*MODE=='W') /*ACCESS FILE*/ FD=CREAT(NAME,PMODE); ELSE IF(*MODE=='A') \( IF((FD=OPEN(NAME,1))==-1) FD=CREAT(NAME,PMODE); LSEEK(FD,OL,2); \) ELSE FD=OPEN(NAME,0); IF(FD==-1) /*COULDN'T ACCESS NAME*/ RETURN(NULL); FP->_FD=FD; FP->_CNT=0; FP->_BASE=NULL; FP->_FLAG &=(_READ \! _WRITE); FP->_FLAG \!=(*MODE=='R') ? _READ : _WRITE; RETURN(FP); \)



Функция _FILEBUF несколько более сложная. Основная труд- ность заключается в том, что _FILEBUF стремится разрешить доступ к файлу и в том случае, когда может не оказаться дос- таточно места в памяти для буферизации ввода или вывода. ес- ли пространство для нового буфера может быть получено обра- щением к функции CALLOC, то все отлично; если же нет, то _FILEBUF осуществляет небуферизованный ввод/ вывод, исполь- зуя отдельный символ, помещенный в локальном массиве.

#INCLUDE <STDIO.H>

_FILLBUF(FP) /*ALLOCATE AND FILL INPUT BUFFER*/ REGISTER FILE *FP; ( STATIC CHAR SMALLBUF(NFILE);/*FOR UNBUFFERED 1/0*/ CHAR *CALLOC(); IF((FR->_FLAG&_READ)==0\!\!(FP->_FLAG&(EOF\!_ERR))\!=0 RETURN(EOF); WHILE(FP->_BASE==NULL) /*FIND BUFFER SPACE*/ IF(FP->_FLAG & _UNBUF) /*UNBUFFERED*/ FP->_BASE=&SMALLBUF[FP->_FD]; ELSE IF((FP->_BASE=CALLOC(_BUFSIZE,1))==NULL) FP->_FLAG \!=_UNBUF; /*CAN'T GET BIG BUF*/ ELSE FP->_FLAG \!=_BIGBUF; /*GOT BIG ONE*/ FP->_PTR=FP->_BASE; FP->_CNT=READ(FP->_FD, FP->_PTR, FP->_FLAG & _UNBUF ? 1 : _BUFSIZE); FF(--FP->_CNT<0) \( IF(FP->_CNT== -1) FP->_FLAG \! = _EOF; ELSE FP->_FLAG \! = _ ERR; FP->_CNT = 0; RETURN(EOF); \) RETURN(*FP->_PTR++ & 0377); /*MAKE CHAR POSITIVE*/ )

При первом обращении к GETC для конкретного файла счетчик оказывается равным нулю, что приводит к обращению к _FILEBUF. Если функция _FILEBUF найдет, что этот файл не от- крыт для чтения, она немедленно возвращает EOF. В противном случае она пытается выделить большой буфер, а если ей это не удается, то буфер из одного символа. При этом она заносит в _FLAG соответствующую информацию о буферизации. Раз буфер уже создан, функция _FILEBUF просто вызывает функцию READ для его заполнения, устанавливает счетчик и указатели и возвращает символ из начала буфера. Единственный оставшийся невыясненным вопрос состоит в том, как все начинается. Массив _IOB должен быть определен и инициализирован для STDIN, STDOUT и STDERR:

FILE _IOB[NFILE] = \( (NULL,0,_READ,0), /*STDIN*/ (NULL,0,NULL,1), /*STDOUT*/ (NULL,0,NULL,_WRITE \! _UNBUF,2) /*STDERR*/ );

Из инициализации части _FLAG этого массива структур видно, что файл STDIN предназначен для чтения, файл STDOUT - для записи и файл STDERR - для записи без использования буфера.

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

-------------- Перепишите функции FOPEN и _FILEBUF, используя поля вместо явных побитовых операций.

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

--------------- Разработайте и напишите функции _FLUSHBUF и FCLOSE.

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

--------------- Стандартная библиотека содержит функцию

FSEEK(FP, OFFSET, ORIGIN)

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




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


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

LSEEK(FD,OFFSET,ORIGIN);

текущая позиция в файле с дескриптором FD передвигается на позицию OFFSET (смещение), которая отсчитывается от места, указываемого аргументом ORIGIN (начало отсчета). Последующее чтение или запись будут теперь начинаться с этой позиции. Аргумент OFFSET имеет тип LONG; FD и ORIGIN имеют тип INT. Аргумент ORIGIN может принимать значения 0,1 или 2, указывая на то, что величина OFFSET должна отсчитываться соответст- венно от начала файла, от текущей позиции или от конца фай- ла. Например, чтобы дополнить файл, следует перед записью найти его конец:

LSEEK(FD,0L,2);

чтобы вернуться к началу ("перемотать обратно"), можно напи- сать:

LSEEK(FD,0L,0);

обратите внимание на аргумент 0L; его можно было бы записать и в виде (LONG) 0. Функция LSEEK позволяет обращаться с файлами примерно так же, как с большими массивами, правда ценой более медлен- ного доступа. следующая простая функция, например, считывает любое количество байтов, начиная с произвольного места в файле.

GET(FD,POS,BUF,N) /*READ N BYTES FROM POSITION POS*/ INT FD, N; LONG POS; CHAR *BUF; \( LSEEK(FD,POS,0); /*GET TO POS*/ RETURN(READ(FD,BUF,N)); \)

В более ранних редакциях, чем редакция 7 системы UNIX, основная точка входа в систему ввода-вывода называется SEEK. Функция SEEK идентична функции LSEEK, за исключением того, что аргумент OFFSET имеет тип INT, а не LONG. в соответствии с этим, поскольку на PDP-11 целые имеют только 16 битов, ар- гумент OFFSET, указываемый функции SEEK, ограничен величиной 65535; по этой причине аргумент ORIGIN может иметь значения 3, 4, 5, которые заставляют функцию SEEK умножить заданное значение OFFSET на 512 (количество байтов в одном физическом блоке) и затем интерпретировать ORIGIN, как если это 0, 1 или 2 соответственно. Следовательно, чтобы достичь произ- вольного места в большом файле, нужно два обращения к SEEK: сначала одно, которое выделяет нужный блок, а затем второе, где ORIGIN имеет значение 1 и которое осуществляет передви- жение на желаемый байт внутри блока.

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

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