: D - на вводе ожидается десятичное целое; соответствующий ар- гумент должен быть указателем на целое. O - На вводе ожидается восьмеричное целое (с лидирующим ну- лем или без него); соответствующий аргумент должен быть указателем на целое. X - На вводе ожидается шестнадцатеричное целое (с лидирующи- ми 0X или без них); соответствующий аргумент должен быть указателем на целое. H - На вводе ожидается целое типа SHORT; соответсвующий ар- гумент должен быть указателем на целое типа SHORT. C - Ожидается отдельный символ; соответствующий аргумент должен быть указателем на символы; следующий вводимый символ помещается в указанное место. Обычный пропуск сим- волов пустых промежутков в этом случае подавляется; для чтения следующего символа, который не является символом пустого промежутка, пользуйтесь спецификацией преобразо- вания %1S. S - Ожидается символьная строка; соответствующий аргумент должен быть указателем символов, который указывает на массив символов, который достаточно велик для принятия строки и добавляемого в конце символа \0. F - Ожидается число с плавающей точкой; соответствующий ар- гумент должен быть указателем на переменную типа FLOAT. Е - символ преобразования E является синонимом для F. Формат ввода переменной типа FLOAT включает необязательный знак, строку цифр, возможно содержащую десятичную точку и нео- бязательное поле экспоненты, состоящее из буквы E, за ко- торой следует целое, возможно имеющее знак. Перед символами преобразования D, O и X может стоять L, которая означает , что в списке аргументов должен находиться указатель на переменную типа LONG, а не типа INT. Аналогич- но, буква L может стоять перед символами преобразования E или F, говоря о том, что в списке аргументов должен нахо- диться указатель на переменную типа DOUBLE, а не типа FLOAT. Например, обращение INT I; FLOAT X; CHAR NAME[50]; SCANF("&D %F %S", &I, &X, NAME); со строкой на вводе 25 54.32E-1 THOMPSON приводит к присваиванию I значения 25,X - значения 5.432 и NAME - строки "THOMPSON", надлежащим образом законченной символом \ 0. эти три поля ввода можно разделить столькими пробелами, табуляциями и символами новых строк, сколько вы пожелаете. Обращение INT I; FLOAT X; CHAR NAME[50]; SCANF("%2D %F %*D %2S", &I, &X, NAME); с вводом 56789 0123 45A72 присвоит I значение 56, X - 789.0, пропустит 0123 и поместит в NAME строку "45". при следующем обращении к любой процеду- ре ввода рассмотрение начнется с буквы A. В этих двух приме- рах NAME является указателем и, следовательно, перед ним не нужно помещать знак &. В качестве другого примера перепишем теперь элементарный калькулятор из главы 4, используя для преобразования ввода функцию SCANF: #INCLUDE <STDIO.H> MAIN() /* RUDIMENTARY DESK CALCULATOR */ \( DOUBLE SUM, V; SUM =0; WHILE (SCANF("%LF", &V) !=EOF) PRINTF("\T%.2F\N", SUM += V); \) выполнение функции SCANF заканчивается либо тогда, когда она исчерпывает свою управляющую строку, либо когда некоторый элемент ввода не совпадает с управляющей спецификацией. В качестве своего значения она возвращает число правильно сов- падающих и присвоенных элементов ввода. Это число может быть использовано для определения количества найденных элементов ввода. при выходе на конец файла возвращается EOF; подчерк- нем, что это значение отлично от 0, что следующий вводимый символ не удовлетворяет первой спецификации в управляющей строке. При следующем обращении к SCANF поиск возобновляется непосредственно за последним введенным символом. Заключительное предостережение: аргументы функции SCANF должны быть указателями. Несомненно наиболее распространен- ная ошибка состоит в написании SCANF("%D", N); вместо SCANF("%D", &N); 7.5. Форматное преобразование в памяти От функции SCANF и PRINTF происходят функции SSCANF и SPRINTF, которые осуществляют аналогичные преобразования, но оперируют со строкой, а не с файлом. Обращения к этим функ- циям имеют вид: SPRINTF(STRING, CONTROL, ARG1, ARG2, ...) SSCANF(STRING, CONTROL, ARG1, ARG2, ...) Как и раньше , функция SPRINTF преобразует свои аргументы ARG1, ARG2 и т.д. В соответствии с форматом, указанным в CONTROL, но помещает результаты в STRING, а не в стандартный вывод. KОнечно, строка STRING должна быть достаточно велика, чтобы принять результат. Например, если NAME - это символь- ный массив, а N - целое, то SPRINTF(NAME, "TEMP%D", N); создает в NAME строку вида TEMPNNN, где NNN - значение N. Функция SSCANF выполняет обратные преобразования - она просматривает строку STRING в соответствии с форматом в ар- гументе CONTROL и помещает результирующие значения в аргу- менты ARG1, ARG2 и т.д.эти аргументы должны быть указателя- ми. В результате обращения SSCANF(NAME, "TEMP%D", &N); переменная N получает значение строки цифр, следующих за TEMP в NAME. Упражнение 7-2 -------------- Перепишите настольный калькулятор из главы 4, используя для ввода и преобразования чисел SCANF и/или SSCANF. 7.6. Доступ к файлам Все до сих пор написанные программы читали из стандарт- ного ввода и писали в стандартный вывод, относительно кото- рых мы предполагали, что они магическим образом предоставле- ны программе местной операционной системой. Следующим шагом в вопросе ввода-вывода является написа- ние программы, работающей с файлом, который не связан зара- нее с программой. одной из программ, которая явно демонстри- рует потребность в таких операциях, является CAT, которая объединяет набор из нескольких именованных файлов в стандар- тный вывод. Программа CAT используется для вывода файлов на терминал и в качестве универсального сборщика ввода для программ, которые не имеют возможности обращаться к файлам по имени. Например, команда CAT X.C.Y.C печатает содержимое файлов X.C и Y.C в стандартный вывод. Вопрос состоит в том, как организовать чтение из имено- ванных файлов, т.е., как связать внешние имена, которыми мыслит пользователь, с фактически читающими данные операто- рами. Эти правила просты. Прежде чем можно считывать из неко- торого файла или записывать в него, этот файл должен быть открыт с помощью функции FOPEN из стандартной библиотеки. функция FOPEN берет внешнее имя (подобное X.C или Y.C), про- водит некоторые обслуживающие действия и переговоры с опера- ционной системой (детали которых не должны нас касаться) и возвращает внутреннее имя, которое должно использоваться при последующих чтениях из файла или записях в него. Это внутреннее имя, называемое "указателем файла", фак- тически является указателем структуры, которая содержит ин- формацию о файле, такую как место размещения буфера, текущая позиция символа в буфере, происходит ли чтение из файла или запись в него и тому подобное. Пользователи не обязаны знать эти детали, потому что среди определений для стандартного ввода-вывода, получаемых из файла STDIO.H, содержится опре- деление структуры с именем FILE. Единственное необходимое для указателя файла описание демонстрируется примером: FILE *FOPEN(), *FP; Здесь говорится, что FP является указателем на FILE и FOPEN возвращает указатель на FILE. Oбратите внимание, что FILE является именем типа, подобным INT, а не ярлыку струк- туры; это реализовано как TYPEDEF. (Подробности того, как все это работает на системе UNIX, приведены в главе 8). Фактическое обращение к функции FOPEN в программе имеет вид: FP=FOPEN(NAME,MODE); Первым аргументом функции FOPEN является "имя" файла, кото- рое задается в виде символьной строки. Второй аргумент MODE ("режим") также является символьной строкой, которая указы- вает, как этот файл будет использоваться. Допустимыми режи- мами являются: чтение ("R"), запись ("W") и добавление ("A"). Если вы откроете файл, который еще не сущетвует, для за- писи или добавления, то такой файл будет создан (если это возможно). Открытие существующего файла на запись приводит к отбрасыванию его старого содержимого. Попытка чтения несу- ществующего файла является ощибкой. Ошибки могут быть обус- ловлены и другими причинами (например, попыткой чтения из файла, не имея на то разрешения). При наличии какой-либо ошибки функция возвращает нулевое значение указателя NULL (которое для удобства также определяется в файле STDIO.H). Другой необходимой вещью является способ чтения или за- писи, если файл уже открыт. Здесь имеется несколько возмож- ностей, из которых GETC и PUTC являются простейшими.функция GETC возвращает следующий символ из файла; ей необходим ука- затель файла, чтобы знать, из какого файла читать. Таким об- разом, C=GETC(FP) помещает в "C" следующий символ из файла, указанного посред- ством FP, и EOF, если достигнут конец файла. Функция PUTC, являющаяся обращением к функции GETC, PUTC(C,FP) помещает символ "C" в файл FP и возвращает "C". Подобно фун- кциям GETCHAR и PUTCHAR, GETC и PUTC могут быть макросами, а не функциями. При запуске программы автоматически открываются три фай- ла, которые снабжены определенными указателями файлов. Этими файлами являются стандартный ввод, стандартный вывод и стан- дартный вывод ошибок; соответствующие указатели файлов назы- ваются STDIN, STDOUT и STDERR. Обычно все эти указатели свя- заны с терминалом, но STDIN и STDOUT могут быть перенаправ- лены на файлы или в поток (PIPE), как описывалось в разделе 7.2. Функции GETCHAR и PUTCHAR могут быть определены в терми- налах GETC, PUTC, STDIN и STDOUT следующим образом: #DEFINE GETCHAR() GETC(STDIN) #DEFINE PUTCHAR(C) PUTC(C, STDOUT) При работе с файлами для форматного ввода и вывода можно ис- пользовать функции FSCANF и FPRINTF. Они идентичны функциям SCANF и PRINTF, за исключением того, что первым аргументом является указатель файла, определяющий тот файл, который бу- дет читаться или куда будет вестись запись; управляющая строка будет вторым аргументом. Покончив с предварительными замечаниями, мы теперь в состоянии написать программу CAT для конкатенации файлов. Используемая здесь основная схема оказывается удобной во многих программах: если имеются аргументы в командной стро- ке, то они обрабатываются последовательно. Если такие аргу- менты отсутствуют, то обрабатывается стандартный ввод. Это позволяет использовать программу как самостоятельно, так и как часть большей задачи. #INCLUDE <STDIO.H> MAIN(ARGC, ARGV) /*CAT: CONCATENATE FILES*/ INT ARGC; CHAR *ARGV[]; \( FILE *FP, *FOPEN(); IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/ FILECOPY(STDIN); ELSE WHILE (--ARGC > 0) IF ((FP=FOPEN(*++ARGV,"R"))==NULL) \( PRINTF("CAT:CAN'T OPEN %\N",*ARGV); BREAK; \) ELSE \( FILECOPY(FP); FCLOSE(FP); \) \) FILECOPY(FP) /*COPY FILE FP TO STANDARD OUTPUT*/ FILE *FP; \( INT C; WHILE ((C=GETC(FP)) !=EOF) PUTC(C, STDOUT); \) Указатели файлов STDIN и STDOUT заранее определены в библио- теке ввода-вывода как стандартный ввод и стандартный вывод; они могут быть использованы в любом месте, где можно исполь- зовать объект типа FILE*.они однако являются константами, а не переменными, так что не пытайтесь им что-либо присваи- вать. Функция FCLOSE является обратной по отношению к FOPEN; она разрывает связь между указателем файла и внешним именем, установленную функцией FOPEN, и высвобождает указатель файла для другого файла.большинство операционных систем имеют не- которые ограничения на число одновременно открытых файлов, которыми может распоряжаться программа. Поэтому, то как мы поступили в CAT, освободив не нужные нам более объекты, яв- ляется хорошей идеей. Имеется и другая причина для примене- ния функции FCLOSE к выходному файлу - она вызывает выдачу информации из буфера, в котором PUTC собирает вывод. (При нормальном завершении работы программы функция FCLOSE вызы- вается автоматически для каждого открытого файла). 7.7. Обработка ошибок - 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 приводит к немедленному завершению без очистки каких-либо буферов; конечно, при желании к этой функции мож- но обратиться непосредственно. 7.8. Ввод и вывод строк Стандартная библиотека содержит функцию FGETS, совершен- но аналогичную функции GETLINE, которую мы использовали на всем протяжении книги. В результате обращения FGETS(LINE, MAXLINE, FP) следующая строка ввода (включая символ новой строки) считы- вается из файла FP в символьный массив LINE; самое большое MAXLINE_1 символ будет прочитан. Результирующая строка за- канчивается символом \ 0. Нормально функция FGETS возвращает LINE; в конце файла она возвращает NULL. (Наша функция GETLINE возвращает длину строки, а при выходе на конец файла - нуль). Предназначенная для вывода функция FPUTS записывает строку (которая не обязана содержать символ новой строки) в файл: FPUTS(LINE, FP) Чтобы показать, что в функциях типа FGETS и FPUTS нет ничего таинственного, мы приводим их ниже, скопированными непосредственно из стандартной библиотеки ввода-вывода: #INCLUDE <STDIO.H> CHAR *FGETS(S,N,IOP) /*GET AT MOST N CHARS FROM IOP*/ CHAR *S; INT N; REGISTER FILE *IOP; \( REGISTER INT C; REGISTER CHAR *CS; CS = S; WHILE(--N>0&&(C=GETC(IOP)) !=EOF) IF ((*CS++ = C)=='\N') BREAK; *CS = '\0'; RETURN((C==EOF && CS==S) 7 NULL : S); \) FPUTS(S,IOP) /*PUT STRING S ON FILS IOP*/ REGISTER CHAR *S; REGISTER FILE *IOP; \( REGISTER INT C; WHILE (C = *S++) PUTC(C,IOP); \) Упражнение 7-3 --------------- Напишите программу сравнения двух файлов, которая будет печатать первую строку и позицию символа, где они различают- ся. Упражнение 7-4 --------------- Переделайте программу поиска заданной комбинации симво- лов из главы 5 таким образом, чтобы в качестве ввода исполь- зовался набор именованных файлов или, если никакие файлы не указаны как аргументы, стандартный ввод. Следует ли печатать имя файла при нахождении подходящей строки? Упражнение 7-5 -------------- Напишите программу печати набора файлов, которая начина- ет каждый новый файл с новой страницы и печатает для каждого файла заголовок и счетчик текущих страниц. 7.9. Несколько разнообразных функций Стандартная библиотека предоставляет множество разнооб- разных функций, некоторые из которых оказываются особенно полезными. Мы уже упоминали функции для работы со строками: STRLEN, STRCPY, STRCAT и STRCMP. Вот некоторые другие. 7.9.1. Проверка вида символов и преобразования Некоторые макросы выполняют проверку символов и преобра- зования: SALPHA(C) не 0, если "C" алфавитный символ, 0 - если нет. SUPPER(C) Не 0, если "C" буква верхнего регистра, 0 - если нет. SLOWER(C) Не 0, если "C" буква нижнего регистра, 0 - если нет. SDIGIT(C) Не 0, если "C" цифра, 0 - если нет. SSPACL(C) Не 0, если "C" пробел, табуляция или новая строка, 0 - если нет. OUPPER(C) Преобразует "C" в букву верхнего регистра. OLOWER(C) Преобразует "C" в букву нижнего регистра. 7.9.2. Функция UNGETC Стандартная библиотека содержит довольно ограниченную версию функции UNGETCH, написанной нами в главе 4; она назы- вается UNGETC. В результате обращения UNGETC(C,FP) символ "C" возвращается в файл FP. Позволяется возвращать в каждый файл только один символ. Функция UNGETC может быть использована в любой из функций ввода и с макросами типа SCANF, GETC или GETCHAR. 7.9.3. Обращение к системе Функция SYSTEM(S) выполняет команду, содержащуюся в сим- вольной строке S, и затем возобновляет выполнение текущей программы. Содержимое S сильно зависит от используемой опе- рационной системы. В качестве тривиального примера, укажем, что на системе UNIX строка SYSTEM("DATE"); приводит к выполнению программы DATE, которая печатает дату и время дня. 7.9.4. Управление памятью Функция CALLOC весьма сходна с функцией ALLOC, использо- ванной нами в предыдущих главах. В результате обращения CALLOC(N, SIZEOF(OBJCCT)) возвращается либо указатель пространства, достаточного для размещения N объектов указанного размера, либо NULL, если запрос не может быть удволетворен. Отводимая память инициа- лизируется нулевыми значениями. Указатель обладает нужным для рассматриваемых объектов выравниванием, но ему следует приписывать соответствующий тип, как в CHAR *CALLOC(); INT *IP; IP=(INT*) CALLOC(N,SIZEOF(INT)); Функция CFREE(P) освобождает пространство, на которое указывает "P", причем указатель "P" певоначально должен быть получен в результате обращения к CALLOC. Здесь нет никаких ограничений на порядок освобождения пространства, но будет неприятнейшей ошибкой освободить что-нибудь, что не было по- лучено обращением к CALLOC. Реализация программы распределения памяти, подобной CALLOC, в которой размещенные блоки могут освобождаться в произвольном порядке, продемонстрирована в главе 8.  * 8. Интерфейс системы UNIX *  Материал этой главы относится к интерфейсу между с-прог- раммами и операционной системой UNIX. Так как большинство пользователей языка "C" работают на системе UNIX, эта глава окажется полезной для большинства читателей. даже если вы используете с-компилятор на другой машине, изучение приводи- мых здесь примеров должно помочь вам глубже проникнуть в ме- тоды программирования на языке "C". Эта глава делится на три основные части: ввод/вывод, система файлов и распределение памяти. Первые две части предполагают небольшое знакомство с внешними характеристика- ми системы UNIX. В главе 7 мы имели дело с системным интерфейсом, который одинаков для всего многообразия операционных систем. На каж- дой конкретной системе функции стандартной библиотеки должны быть написаны в терминах ввода-вывода, доступных на данной машине. В следующих нескольких разделах мы опишем основную систему связанных с вводом и выводом точек входа операцион- ной системы UNIX и проиллюстрируем, как с их помощью могут быть реализованы различные части стандартной библиотеки. 8.1. Дескрипторы файлов В операционной системе UNIX весь ввод и вывод осуществ- ляется посредством чтения файлов или их записи, потому что все периферийные устройства, включая даже терминал пользова- теля, являются файлами определенной файловой системы. Это означает, что один однородный интерфейс управляет всеми свя- зями между программой и периферийными устройствами. В наиболее общем случае перед чтением из файла или за- писью в файл необходимо сообщить системе о вашем намерении; этот процесс называется "открытием" файла. Система выясня- ет,имеете ли вы право поступать таким образом (существует ли этот файл? имеется ли у вас разрешение на обращение к не- му?), и если все в порядке, возвращает в программу небольшое положительное целое число, называемое дескриптором файла. всякий раз, когда этот файл используется для ввода или выво- да, для идентификации файла употребляется дескриптор файла, а не его имя. (Здесь существует примерная аналогия с исполь- зованием READ (5,...) и WRITE (6,...) в фортране). Вся ин- формация об открытом файле содержится в системе; программа пользователя обращается к файлу только через дескриптор фай- ла. Для удобства выполнения обычных операций ввода и вывода с помощью терминала пользователя существуют специальные сог- лашения. Когда интерпретатор команд ("SHELL") прогоняет программу, он открывает три файла, называемые стандартным вводом, стандартным выводом и стандартным выводом ошибок, которые имеют соответственно числа 0, 1 и 2 в качестве деск- рипторов этих файлов. В нормальном состоянии все они связаны с терминалом, так что если программа читает с дескриптором файла 0 и пишет с дескрипторами файлов 1 и 2, то она может осуществлять ввод и вывод с помощью терминала, не заботясь об открытии соответствующих файлов. Пользователь программы может перенаправлять ввод и вывод на файлы, используя операции командного интерпретатора SHELL "<" и ">" : PROG <INFILE>OUTFILE В этом случае интерпретатор команд SHELL изменит присваива- ние по умолчанию дескрипторов файлов 0 и 1 с терминала на указанные файлы. Нормально дескриптор файла 2 остается свя- занным с терминалом, так что сообщения об ошибках могут пос- тупать туда. Подобные замечания справедливы и тогда, когда ввод и вывод связан с каналом. Следует отметить, что во всех случаях прикрепления файлов изменяются интерпретатором SHELL, а не программой. Сама программа, пока она использует файл 0 для ввода и файлы 1 и 2 для вывода, не знает ни отку- да приходит ее ввод, ни куда поступает ее выдача. 8.2. Низкоуровневый ввод/вывод - операторы READ и WRITE Самый низкий уровень ввода/вывода в системе UNIX не пре- дусматривает ни какой-либо буферизации, ни какого-либо дру- гого сервиса; он по существу является непосредственным вхо- дом в операционную систему. Весь ввод и вывод осуществляется двумя функциями: READ и WRITE. Первым аргументом обеих функ- ций является дескриптор файла. Вторым аргументом является буфер в вашей программе, откуда или куда должны поступать данные. Третий аргумент - это число подлежащих пересылке байтов. Обращения к этим функциям имеют вид: N_READ=READ(FD,BUF,N); N_WRITTEN=WRITE(FD,BUF,N); При каждом обращении возвращается счетчик байтов, указываю- щий фактическое число переданных байтов. При чтении возвра- щенное число байтов может оказаться меньше, чем запрошенное число. Возвращенное нулевое число байтов означает конец фай- ла, а "-1" указывает на наличие какой-либо ошибки. При запи- си возвращенное значение равно числу фактически записанных байтов; несовпадение этого числа с числом байтов, которое предполагалось записать, обычно свидетельствует об ошибке. Количество байтов, подлежащих чтению или записи, может быть совершенно произвольным. Двумя самыми распространенными величинами являются "1", которая означает передачу одного символа за обращение (т.е. Без использования буфера), и "512", которая соответствует физическому размеру блока на многих периферийных устройствах. Этот последний размер будет наиболее эффективным, но даже ввод или вывод по одному сим- волу за обращение не будет необыкновенно дорогим. Объединив все эти факты, мы написали простую программу для копирования ввода на вывод, эквивалентную программе ко- пировки файлов, написанной в главе 1. На системе UNIX эта программа будет копировать что угодно куда угодно, потому что ввод и вывод могут быть перенаправлены на любой файл или устройство. #DEFINE BUFSIZE 512 /*BEST SIZE FOR PDP-11 UNIX*/ MAIN() /*COPY INPUT TO OUTPUT*/ \( CHAR BUF[BUFSIZE]; INT N; WHILE((N=READ(0,BUF,BUFSIZE))>0) WRITE(1,BUF,N); \) Если размер файла не будет кратен BUFSIZE, то при некотором обращении к READ будет возвращено меньшее число байтов, ко- торые затем записываются с помощью WRITE; при следующем пос- ле этого обращении к READ будет возвращен нуль. Поучительно разобраться, как можно использовать функции READ и WRITE для построения процедур более высокого уровня, таких как GETCHAR, PUTCHAR и т.д. Вот, например, вариант функции GETCHAR, осуществляющий ввод без использования буфе- ра. #DEFINE CMASK 0377 /*FOR MAKING CHAR'S > 0*/ GETCHAR() /*UNBUFFERED SINGLE CHARACTER INPUT*/ \( CHAR C; RETURN((READ(0,&C,1)>0 7 & CMASK : EOF); \) Переменная "C" должна быть описана как CHAR, потому что фун- кция READ принимает указатель на символы. Возвращаемый сим- вол должен быть маскирован числом 0377 для гарантии его по- ложительности; в противном случае знаковый разряд может сде- лать его значение отрицательным. (Константа 0377 подходит для эвм PDP-11, но не обязательно для других машин). Второй вариант функции GETCHAR осуществляет ввод больши- ми порциями, а выдает символы по одному за обращение. #DEFINE CMASK 0377 /*FOR MAKING CHAR'S>0*/ #DEFINE BUFSIZE 512 GETCHAR() /*BUFFERED VERSION*/ \( STATIC CHAR BUF[BUFSIZE]; STATIC CHAR *BUFP = BUF; STATIC INT N = 0; IF (N==0) \( /*BUFFER IS EMPTY*/ N=READ(0,BUF,BUFSIZE); BUFP = BUF; \) RETURN((--N>=0) ? *BUFP++ & CMASK : EOF); \) 8.3. Открытие, создание, закрытие и расцепление (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 вместо их эквивалентов из стандар- тной библиотеки. Проведите эксперименты для определения от- носительной скорости работы этих двух вариантов. 8.4. Произвольный доступ - 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 и наоборот. напишите каждую функцию через другую. 8.5. Пример - реализация функций 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 правильно согласуется с бу- феризацией, сделанной для других функций библиотеки. 8.6. Пример - распечатка справочников Иногда требуется другой вид взаимодействия с системой файлов - определение информации о файле, а не того, что в нем содержится. Примером может служить команда 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, и что программы включают эти файлы, а не помещают фактические описания внутрь самих программ. 8.7. Пример - распределитель памяти В главе 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 пользователь может в любое время добавлять в свободный спи- сок статический или внешний массив.  * 9. Приложение А: справочное руководство по языку 'C' *  9.1. Введение Это руководство описывает язык 'с' для компьютеров DEC PDP-11, HONEYWELL 6000, IBM система/370 и INTERDATA 8/32. там, где есть расхождения, мы сосредотачиваемся на версии для PDP-11, стремясь в то же время указать детали, которые зависят от реализации. За малым исключением, эти расхождения непосредственно обусловлены основными свойствами используе- мого аппаратного оборудования; различные компиляторы обычно вполне совместимы. 10. Лексические соглашения Имеется шесть классов лексем: идентификаторы, ключевые слова, константы, строки, операции и другие разделители. Пробелы, табуляции , новые строки и комментарии (совместно, "пустые промежутки"), как описано ниже, игнорируются, за ис- ключением тех случаев, когда они служат разделителями лек- сем. Необходим какой-то пустой промежуток для разделения идентификаторов, ключевых слов и констант, которые в против- ном случае сольются. Если сделан разбор входного потока на лексемы вплоть до данного символа, то в качестве следующей лексемы берется са- мая длинная строка символов, которая еще может представлять собой лексему. 10.1. Комментарии Комментарий открывается символами /* и заканчивается символами /*. Комментарии не вкладываются друг в друга. 10.2. Идентификаторы (имена) Идентификатор - это последовательность букв и цифр; пер- вый символ должен быть буквой. Подчеркивание _ считается буквой. Буквы нижнего и верхнего регистров различаются. зна- чащими являются не более, чем первые восемь символов, хотя можно использовать и больше. На внешние идентификаторы, ко- торые используются различными ассемблерами и загрузчиками, накладыватся более жесткие ограничения: DEC PDP-11 7 символов, 2 регистра HONEYWELL 6000 6 символов, 1 регистр IBM 360/370 7 символов, 1 регистр INTERDATA 8/32 8 символов, 2 регистра 10.3. Ключевые слова Следующие идентификаторы зарезервированы для использова- ния в качестве ключевых слов и не могут использоваться иным образом: INT EXTERN ELSE CHAR REGISTER FOR FLOAT TYPEDEF DO DOUBLE STATIC WHILE STRUCT GOTO SWITCH UNION RETURN CASE LONG SIZEOF DEFAULT SHORT BREAK ENTRY UNSIGNED CONTINUE *AUTO IF Ключевое слово ENTRY в настоящее время не используется ка- ким-либо компилятором; оно зарезервировано для использования в будущем. В некоторых реализациях резервируется также слова FORTRAN и ASM 10.4. Константы Имеется несколько видов констант, которые перечислены ниже. В пункте 10.6 резюмируются характеристики аппаратных сред- ств, которые влияют на размеры. 10.4.1. Целые константы Целая константа, состоящая из последовательности цифр, считается восьмеричной, если она начинается с 0 (цифра нуль), и десятичной в противном случае. Цифры 8 и 9 имеют восьмеричные значения 10 и 11 соответственно. Последователь- ность цифр, которой предшествуют символы 0х (нуль, х-малень- кое) или 0х (нуль х-большое), рассматривается как шестнадца- тиричное целое. Шестнадцатиричные цифры включают буквы от а (маленькое) или а (большое) до F (маленькое) или F (большое) со значениями от 10 до 15. Десятичная константа, величина которой превышает наибольшее машинное целое со знаком, счи- тается длинной; восмеричная или шестнадцатиричная константа, которое превышает наибольшее машинное целое без знака, также считается длинной. 10.4.2. Явные длинные константы Десятичная, восмеричная или шестнадцатиричная константа, за которой непосредственно следует L (эль-маленькое) или L (эль-большое), является длинной константой. Как обсуждается ниже, на некоторых машинах целые и длинные значения могут рассматриваться как идентичные. 10.4.3. Символьные константы Символьная константа - это символ, заключенный в одиноч- ные кавычки, как, например, 'X'. Значением символьной конс- танты является численное значение этого символа в машинном представлении набора символов. Некоторые неграфические символы, одиночная кавычка ' и обратная косая черта \ могут быть представлены в соответст- вии со следующей таблицей условных последовательностей: новая строка NL/LF/ \N горизонтальная табуляция HT \T символ возврата на одну позицию BS \B возврат каретки CR \R переход на новую страницу FF \F обратная косая черта \ \\ одиночная кавычка ' \' комбинация битов DDD \DDD Условная последовательность \DDD состоит из обратной ко- сой черты, за которой следуют 1,2 или 3 восмеричных цифры, которые рассмативаются как задающие значение желаемого сим- вола. Специальным случаем этой конструкции является последо- вательность \0 (за нулем не следует цифра), которая опреде- ляет символ NUL. если следующий за обратной косой чертой символ не совпадает с одним из указанных, то обратная косая черта игнорируется. 10.4.4. Плавающие константы Плавающая константа состоит из целой части, десятичной точки, дробной части, буквы E (маленькая) или E (большая) и целой экспоненты с необязательным знаком. Как целая, так и дробная часть являются последовательностью цифр. Либо целая, либо дробная часть (но не обе) может отсутствовать; либо де- сятичная точка, либо е (маленькая) и экспонента (но не то и другое одновременно) может отсутствовать. Каждая плавающая константа считается имеющей двойную точность. 10.5. Строки Строка - это последовательность символов, заключенная в двойные кавычки, как, наприимер,"...". Строка имеет тип "массив массивов" и класс памяти STATIC (см. Пункт 4 ниже). Строка инициализирована указанными в ней символами. Все строки, даже идентично записанные, считаются различными. Компилятор помещает в конец каждой строки нулевой байт \0, с тем чтобы просматривающая строку программа могла определить ее конец. Перед стоящим внутри строки символом двойной ка- вычки " должен быть поставлен символ обратной косой черты \; кроме того, могут использоваться те же условия последова- тельности, что и в символьных константах. И последнее, об- ратная косая черта \, за которой непосредственно следует символ новой строки, игнорируется. 10.6. Характеристики аппаратных средств Следующая ниже таблица суммирует некоторые свойства ап- паратного оборудования, которые меняются от машины к машине. Хотя они и влияют на переносимость программ, на практике они представляют маленькую проблему, чем это может казаться за- ранее. Таблица 1 ------------------------------------------------------- DEC PDP-11 HONEYWELL IBM 370 INTERDATA 8/32 ASCII ASCII EBCDIC ASCII CHAR 8 BITS 9 BITS 8 BITS 8 BITS INT 16 36 32 32 SHORT 16 36 16 16 LONG 32 36 32 32 FLOAT 32 36 32 32 DOUBLE 64 72 64 64 RANGE -38/+38 -38/+38 -76/+76 -76/+76 -------------------------------------------------------- 11. Синтаксическая нотация В используемой в этом руководстве синтаксической нотации синтаксические категории выделяются курсивом (прим. перев.: в настоящее время синтексические категории вместо курсивом выделяются подчеркиванием), а литерные слова и символы - жирным шрифтом. Альтернативные категории перечисляются на отдельных строчках. Необязательный символ, терминальный или нетерминальный, указывается индексом "необ", так что \( выражение --------- необ \) указывает на необязательное выражение, заключенное в фигур- ных скобках. Синтаксис суммируется в пункте 18. 12. Что в имени тебе моем? Язык "C" основывает интерпретацию идентификатора на двух признаках идентификатора: его классе памяти и его типе. Класс памяти определяет место и время хранения памяти, свя- занной с идентификатором; тип определяет смысл величин, на- ходящихся в памяти, определенной под идентификатором. Имеются четыре класса памяти: автоматическая, статичес- кая, внешняя и регистровая. Автоматические переменные явля- ются локальными для каждого вызова блока и исчезают при вы- ходе из этого блока. Статические переменные являются локаль- ными, но сохраняют свои значения для следующего входа в блок даже после того, как управление передается за пределы блока. Внешние переменные существуют и сохраняют свои значения в течение выполнения всей программы и могут использоваться для связи между функциями, в том числе и между независимо ском- пилированными функциями. Регистровые переменные хранятся (ели это возможно) в быстрых регистрах машины; подобно авто- матическим переменным они являются локальными для каждого блока и исчезают при выходе из этого блока. В языке "C" предусмотрено несколько основных типов объектов: объекты, написанные как символы (CHAR), достаточно вели- ки, чтобы хранить любой член из соответствующего данной реа- лизации внутреннего набора символов, и если действительный символ из этого набора символов хранится в символьной пере- менной, то ее значение эквивалентно целому коду этого симво- ла. В символьных переменных можно хранить и другие величины, но реализация будет машинно-зависимой. Можно использовать до трех размеров целых, описываемых как SHORT INT, INT и LONG INT. Длинные целые занимают не меньше памяти, чем короткие, но в конкретной реализации мо- жет оказаться, что либо короткие целые, либо длинные целые, либо те и другие будут эквивалентны простым целым. "Простые" целые имеют естественный размер, предусматриваемый архиитек- турой используемой машины; другие размеры вводятся для удво- летворения специальных потребностей. Целые без знака, описываемые как UNSIGNED, подчиняются законам арифметики по модулю 2**N, где N - число битов в их представлении. (На PDP-11 длинные величины без знака не пре- дусмотрены). Плавающие одинарной точности (FLOAT) и плавающие двойной точности (DOUBLE) в некоторых реализациях могут быть синони- мами. Поскольку объекты упомянутых выше типов могут быть ра- зумно интерпретированы как числа, эти типы будут называться арифметическими. типы CHAR и INT всех размеров совместно бу- дут называться целочисленными. Типы FLOAT и DOUBLE совместно будут называться плавающими типами. Кроме основных арифметических типов существует концепту- ально бесконечный класс производных типов, которые образуют- ся из основных типов следующим образом: массивы объектов большинства типов; функции, которые возвращают объекты заданного типа; указатели на объекты данного типа; структуры, содержащие последовательность объектов различных типов; объединения, способные содержать один из нескольких объектов различных типов. Вообще говоря, эти методы построения объектов могут при- меняться рекурсивно. 13. Объекты и L-значения Объект является доступным обработке участком памяти; L-значение - это выражение, ссылающееся на объект. Очевидным примером выражения L-значения является идентификатор. Сущес- твуют операции, результатом которых являются L-значения; ес- ли, например, E - выражение указанного типа, то *E является выражением L-значения, ссылающимся на объект E. Название "L-значение" происходит от выражения присваивания E1=E2, в котором левая часть должна быть выражением L-значения. При последующем обсуждении каждой операции будет указываться, ожидает ли она операндов L-значения и выдает ли она L-значе- ние. 14. Преобразования Ряд операций может в зависимости от своих операндов вы- зывать преобразование значение операнда из одного типа в другой. В этом разделе объясняются результаты, которые сле- дует ожидать от таких преобразований. В п. 14.6 Подводятся итоги преобразований, требуемые большинством обычных опера- ций; эти сведения дополняются необходимым образом при обсуж- дении каждой операции. 14.1. Символы и целые Символ или короткое целое можно использовать всюду, где можно использовать целое. Во всех случаях значение преобра- зуется к целому. Преобразование более короткого целого к бо- лее длинному всегда сопровождается знаковым расширением; це- лые являются величинами со знаком. Осуществляется или нет знаковое расширение для символов, зависит от используемой машины, но гарантируется, что член стандартного набора сим- волов неотрицателен. из всех машин, рассматриваемых в этом руководстве, только PDP-11 осуществляет знаковое расширение. область значений символьных переменных на PDP-11 меняется от -128 до 127; символы из набора ASC11 имеют положительные значения. Символьная константа, заданная с помощью восьме- ричной условной последовательности, подвергается знаковому расширению и может оказаться отрицательной; например, '\377' имеет значение -1. Когда более длинное целое преобразуется в более короткое или в CHAR, оно обрезается слева; лишние биты просто отбра- сываются. 14.2. Типы FLOAT и DOUBLE Вся плавающая арифметика в "C" выполняется с двойной точностью каждый раз, когда объект типа FLOAT появляется в выражении, он удлиняется до DOUBLE посредством добавления нулей в его дробную часть. когда объект типа DOUBLE должен быть преобразован к типу FLOAT, например, при присваивании, перед усечением DOUBLE округляется до длины FLOAT. 14.3. Плавающие и целочисленные величины Преобразование плавающих значений к целочисленному типу имеет тенденцию быть до некоторой степени машинно-зависимым; в частности направление усечения отрицательных чисел меняет- ся от машине к машине. Результат не определен, если значение не помещается в предоставляемое пространство. Преобразование целочисленных значений в плавающие выпол- няется без осложнений. Может произойти некоторая потеря точ- ности, если для результата не содержится достаточного коли- чества битов. 14.4. Указатели и целые Целое или длинное целое может быть прибавлено к указате- лю или вычтено из него; в этом случае первая величина преоб- разуется так, как указывается в разделе описания операции сложения. Два указателя на объекты одинакового типа могут быть вычтены; в этом случае результат преобразуется к целому, как указывается в разделе описания операции вычитания. 14.5. Целое без знака Всякий раз, когда целое без знака объединяется с простым целым, простое целое преобразуется в целое без знака и ре- зультат оказывается целым без знака. Значением является наи- меньшее целое без знака, соответствующее целому со знаком (по модулю 2**размер слова). В двоичном дополнительном пред- ставлении это преобразование является чисто умозрительным и не изменяет фактическую комбинацию битов. Когда целое без знака преобразуется к типу LONG, значе- ние результата совпадает со значением целого без знака. Та- ким образом, это преобразование сводится к добавлению нулей слева. 14.6. Арифметические преобразования Подавляющее большинство операций вызывает преобразование и определяет типы результата аналогичным образом. Приводимая ниже схема в дальнейшем будет называться "обычными арифмети- ческими преобразованиями". Сначала любые операнды типа CHAR или SHORT преобразуются в INT, а любые операнды типа FLOAT преобразуются в DOUBLE. Затем, если какой-либо операнд имеет тип DOUBLE, то другой преобразуется к типу DOUBLE, и это будет типом результата. В противном случае, если какой-либо операнд имеет тип LONG, то другой операнд преобразуется к типу LONG, и это и будет типом результата. В противном случае, если какой-либо операнд имеет тип UNSIGNED, то другой операнд преобразуется к типу UNSIGNED, и это будет типом результата. В противном случае оба операнда будут иметь тип INT, и это будет типом результата. 15. Выражения Старшинство операций в выражениях совпадает с порядком следования основных подразделов настоящего раздела, начиная с самого высокого уровня старшинства. Так, например, выраже- ниями, указываемыми в качестве операндов операции + (п.15.4), Являются выражения, определенные в п.п.15.1-15.3. Внутри каждого подраздела операции имеет одинаковое старшин- ство. В каждом подразделе для описываемых там операций ука- зывается их ассоциативность слева или справа. Старшинство и ассоциативность всех операций в выражениях резюмируются в грамматической сводке в п.18. В противном случае порядок вычислений выражений не опре- делен. В частности, компилятор считает себя в праве вычис- лять подвыражения в том порядке, который он находит наиболее эффективным, даже если эти подвыражения приводят к побочным эффектам. Порядок, в котором происходят побочные эффекты, не специфицируется. Выражения, включающие коммутативные и ассо- циативные операции ( *,+,&,!,^ ), могут быть переупорядочены произвольным образом даже при наличии круглых скобок; чтобы вынудить определенный порядок вычислений, в этом случае не- обходимо использовать явные промежуточные переменные. При вычислении выражений обработка переполнения и про- верка при делении являются машинно-зависимыми. Все существу- ющие реализации языка "C" игнорируют переполнение целых; об- работка ситуаций при делении на 0 и при всех особых случаях с плавающими числами меняется от машины к машине и обычно выполняется с помощью библиотечной функции. 15.1. Первичные выражения Первичные выражения, включающие ., ->, индексацию и об- ращения к функциям, группируются слева направо. Первичное выражение: идентификатор константа строка (выражение) первичное-выражение [выражение] первичное-выражение (список-выражений нео первичное-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. 15.2. Унарные операции Выражение с унарными операциями группируется справо на- лево. Унарное-выражение: * выражение & L-значение - выражение ! Выражение \^ выражение ++ L-значение -- L-значение L-значение ++ L-значение -- (имя-типа) выражение SIZEOF выражение SIZEOF имя-типа Унарная операция * означает косвенную адресацию: выраже- ние должно быть указателем, а результатом является L-значе- ние, ссылающееся на тот объект, на который указывает выраже- ние. Если типом выражения является "указатель на...", то ти- пом результата будет "...". Результатом унарной операции & является указатель на объект, к которому ссылается L-значение. Если L-значение имеет тип "...", то типом результата будет "указатель на ...". Результатом унарной операции - (минус) является ее опе- ранд, взятый с противоположным знаком. Для величины типа UNSIGNED результат получается вычитанием ее значения из 2**N (два в степени N), где N-число битов в INT. Унарной операции + (плюс) не существует. Результатом операции логического отрицания ! Является 1, если значение ее операнда равно 0, и 0, если значение ее операнда отлично от нуля. Результат имеет тип INT. Эта опе- рация применима к любому арифметическому типу или указате- лям. Операция \^ дает обратный код, или дополнение до едини- цы, своего операнда. Выполняются обычные арифметические пре- образования. Операнд должен быть целочисленного типа. Объект, на который ссылается операнд L-значения префикс- ной операции ++, увеличивается. значением является новое значение операнда, но это не L-значение. Выражение ++х экви- валентно х+=1. Информацию о преобразованиях смотри в разборе операции сложения (п. 15.4) и операции присваивания (п. 15.14). Префиксная операция -- аналогична префиксной операции ++, но приводит к уменьшению своего операнда L-значения. При применении постфиксной операции ++ к L-значению ре- зультатом является значение объекта, на который ссылается L-значение. После того, как результат принят к сведению, объект увеличивается точно таким же образом, как и в случае префиксной операции ++. Результат имеет тот же тип, что и выражение L-значения. При применении постфиксной операции -- к L-значению ре- зультатом является значение объекта, на который ссылается L-значение. После того, как результат принят к сведению, объект уменьшается точно таким же образом, как и в случае префиксной операции --. Результат имеет тот же тип, что и выражение L-значения. Заключенное в круглые скобки имя типа данных,стоящее пе- ред выражением , вызывает преобразование значения этого вы- ражения к указанному типу. Эта конструкция называется пере- вод (CAST). Имена типов описываются в п. 16.7. Операция SIZEOF выдает размер своего операнда в байтах. (Понятие байт в языке не определено, разве только как значе- ние операции SIZEOF. Однако во всех существующих реализациях байтом является пространство, необходимое для хранения объекта типа CHAR). При применении к массиву результатом яв- ляется полное число байтов в массиве. Размер определяется из описаний объектов в выражении. Это выражение семантически является целой константой и может быть использовано в любом месте, где требуется константа. Основное применение эта опе- рация находит при связях с процедурами, подобным распредели- телям памяти, и в системах ввода- вывода. Операция SIZEOF может быть также применена и к заключен- ному в круглые скобки имени типа. В этом случае она выдает размер в байтах объекта указанного типа. Конструкция SIZEOF (тип) рассматривается как целое, так что выражение SIZEOF (тип) - 2 эквивалентно выражению (SIZEOF (тип)9 - 2. 15.3. Мультипликативные операции Мультипликативные операции *, /, и % группируются слева направо. Выполняются обычные арифметические преобразования. Мультипликативное-выражение: выражение * выражение выражение / выражение выражение % выражение Бинарная операция * означает умножение. Операция * ассо- циативна, и выражения с несколькими умножениями на одном и том же уровне могут быть перегруппированы компилятором. Бинарная операция / означает деление. При делении поло- жительных целых осуществляется усечение по направлению к ну- лю, но если один из операндов отрицателен, то форма усечения зависит от используемой машины. На всех машинах, охватывае- мых настоящим руководством, остаток имеет тот же знак , что и делимое. Всегда справедливо, что (A/B)*B+A%B равно A (если B не равно 0). Бинарная операция % выдает остаток от деления первого выражения на второе. Выполняются обычные арифметические пре- образования. Операнды не должны быть типа FLOAT. 15.4. Аддитивные операции Аддитивные операции + и - группируются слева направо. выполняются обычные арифметические преобразования. Для каж- дой операции имеются некоторые дополнительные возможности, связанные с типами операндов. Аддитивное-выражение: выражение + выражение выражение - выражение Результатом операции + является сумма операндов. Можно скла- дывать указатель на объект в массиве и значение любого цело- численного типа. во всех случаях последнее преобразуется в адресное смещение посредством умножения его на длину объек- та, на который указывает этот указатель. Результатом являет- ся указатель того же самого типа, что и исходный указатель, который указывает на другой объект в том же массиве, смещен- ный соответствующим образом относительно первоначального объекта. Таким образом, если P является указателем объекта в массиве, то выражение P+1 является указателем на следующий объект в этом массиве. Никакие другие комбинации типов для указателей не разре- шаются. Операция + ассоциативна, и выражение с несколькими сло- жениями на том же самом уровне могут быть переупорядочены компилятором. Результатом операции - является разность операндов. Вы- полняются обычные арифметические преобразования. Кроме того, из указателя может быть вычтено значение любого целочислен- ного типа, причем, проводятся те же самые преобразования, что и при операции сложения. Если вычитаются два указателя на объекты одинакового ти- па, то результат преобразуется (делением на длину объекта) к типу INT, представляя собой число объектов, разделяющих ука- зываемые объекты. Если эти указатели не на объекты из одного и того же массива, то такое преобразование, вообще говоря, даст неожиданные результаты, потому что даже указатели на объекты одинакового типа не обязаны отличаться на величину, кратную длине объекта. 15.5. Операции сдвига Операции сдвига << и >> группируются слева направо. Для обеих операций проводятся обычные арифметические преобразо- вания их операндов, каждый из которых должен быть целочис- ленного типа. Затем правый операнд преобразуется к типу INT; результат имеет тип левого операнда. Результат не определен, если правый операнд отрицателен или больше или равен, чем длина объекта в битах. Выражение-сдвига: выражение << выражение выражение >> выражение Значением выражения E1<<E2 является E1 (интерпретируемое как комбинация битов), сдвинутое влево на E2 битов; освобождаю- щиеся биты заполняются нулем. значением выражения E1>>E2 яв- ляется E1, сдвинутое вправо на E2 битовых позиций. Если E1 имеет тип UNSIGNE, то сдвиг вправо гарантированно будет ло- гическим (заполнение нулем); в противном случае сдвиг может быть (и так и есть на PDP-11) арифметическим (освобождающие- ся биты заполняются копией знакового бита). 15.6. Операции отношения Операции отношения группируются слева направо, но этот факт не очень полезен; выражение A<B<C не означает того, что оно казалось бы должно означать. Выражение-отношения: выражение < выражение выражение > выражение выражение <= выражение выражение >= выражение Операции < (меньше), > (больше), <= (меньше или равно) и >= (больше или равно) все дают 0, если указанное отношение лож- но, и 1, если оно истинно. Результат имеет тип ITN. Выполня- ются обычные арифметические преобразования. Могут сравни- ваться два указателя; результат зависит от относительного расположения указываемых объектов в адресном пространстве. Сравнение указателей переносимо только в том случае, если указатели указывают на объекты из одного и того же массива. 15.7. Операции равенства Выражение-равенства: выражение == выражение выражение != выражение Операции == (равно) и != (не равно) в точности аналогичны операциям отношения, за исключением того, что они имеют бо- лее низкий уровень старшинства. (Поэтому значение выражения A<B==C<D равно 1 всякий раз, когда выражение A<B и C<D имеют одинаковое значение истинности). Указатель можно сравнивать с целым, но результат будет машинно- независимым только в том случае, если целым являет- ся константа 0. Гарантируется, что указатель, которому прис- воено значение 0, не указывает ни на какой объект и на самом деле оказывается равным 0; общепринято считать такой указа- тель нулем. 15.8. Побитовая операция 'и' Выражение-и: выражение & выражение Операция & является ассоциативной, и включающие & выражения могут быть переупорядочены. Выполняются обычные арифметичес- кие преобразования; результатом является побитовая функция 'и' операндов. Эта операция применима только к операндам це- лочисленного типа. 15.9. Побитовая операция исключающего 'или' Выражение-исключающего-или: выражение ^ выражение Операция ^ является ассоциативной, и включающие ^ выражения могут быть переупорядочены. выполняются обычные арифметичес- кие преобразования; результатом является побитовая функция исключающего 'или' операндов. Операция применима только к операндам целочисленного типа. 15.10. Побитовая операция включающего 'или' Выражение-включающего-или: выражение \! Выражение Операция \! Является ассоциативной, и содержащие \! Выраже- ния могут быть переупорядочены. выполняются обычные арифме- тические преобразования; результатом является побитовая фун- кция включающего 'или' операндов. Операция применима только к операндам целочисленного типа. 15.11. Логическая операция 'и' Выражение-логического-и: выражение && выражение Операция && группируется слева направо. Она возвращает 1, если оба ее операнда отличны от нуля, и 0 в противном слу- чае. В отличие от & операция && гарантирует вычисление слева направо; более того, если первый операнд равен 0, то значе- ние второго операнда вообще не вычисляется. Операнды не обязаны быть одинакового типа, но каждый из них должен быть либо одного из основных типов, либо указате- лем. результат всегда имеет тип ITN. 15.12. Операция логического 'или' Выражение-логического-или: выражение \!\! выражение Операция \!\! Группируется слева направо. Она возвращает 1, если один из операндов отличен от нуля, и 0 в противном слу- чае. В отличие от операции \! Операция \!\! Гарантирует вы- числение слева направо; более того, если первый операнд от- личен от нуля, то значение второго операнда вообще не вычис- ляется. Операнды не обязаны быть одинакового типа, но каждый из них должен быть либо одного из основных типов, либо указате- лем. Результат всегда имеет тип INT. 15.13. Условная операция Условное-выражение: выражение ? выражение : выражение Условные выражения группируются слево направо. Вычисляется значение первого выражения, и если оно отлично от нуля, то результатом будет значение второго выражения; в противном случае результатом будет значение третьего выражения. Если это возможно, проводятся обычные арифметические преобразова- ния, с тем, чтобы привести второе и третье выражения к обще- му типу; в противном случае, если оба выражения являются указателями одинакового типа, то результат имеет тот же тип; в противном случае одно выражение должно быть указателем, а другое - константой 0, и результат будет иметь тип указате- ля. Вычисляется только одно из второго и третьего выражений. 15.14. Операция присваивания Имеется ряд операций присваивания, каждая из которых группируется слева направо. Все операции требуют в качестве своего левого операнда 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.15. Операция запятая Выражение-с-запятой: выражение , выражение Пара выражений, разделенных запятой, вычисляется слева нап- раво и значение левого выражения отбрасывается. Типом и зна- чением результата является тип и значение правого операнда. Эта операция группируется слева направо. В контексте, где запятая имеет специальное значение, как, например, в списке фактических аргументов функций (п. 15.1) Или в списках ини- циализаторов (п. 16.6), Операция запятая, описываемая в этом разделе, может появляться только в круглых скобках; напри- мер, функция F(A,(T=3,T+2),C) имеет три аргумента, второй из которых имеет значение 5. 16. Описания Описания используются для указания интерпретации, кото- рую язык "C" будет давать каждому идентификатору; они не обязательно резервируют память, соответствующую идентифика- тору. Описания имеют форму Описание: спецификаторы-описания список-описателей необ; Описатели в списке описателей содержат описываемые идентифи- каторы. Спецификаторы описания представляют собой последова- тельность спецификаторов типа и спецификаторов класса памя- ти. Спецификаторы-описания: спецификатор-типа спецификаторы-описания необ спецификатор-класса-памяти спецификатор-описания необ список должен быть самосогласованным в смысле, описываемом ниже. 16.1. Спецификаторы класса памяти Ниже перечисляются спецификаторы класса памяти: Спецификатор-класса-памяти: AUTO STATIC EXTERN REGISTER TYPEDEF Спецификатор TYPEDEF не реализует памяти и называется "спецификатором класса памяти" только по синтаксическим со- ображениям; это обсуждается в п. 16.8. Смысл различных клас- сов памяти был обсужден в п. 12. Описания AUTO, STATIC и REGISTER служат также в качестве определений в том смысле, что они вызывают резервирование нужного количества памяти. В случае EXTERN должно присутст- вовать внешнее определение (п. 18) Указываемых идентификато- ров где-то вне функции, в которой они описаны. Описание REGISTER лучше всего представлять себе как опи- сание AUTO вместе с намеком компилятору, что описанные таким образом переменные будут часто использоваться. Эффективны только несколько первых таких описаний. Кроме того, в регис- трах могут храниться только переменные определенных типов; на PDP-11 это INT, CHAR или указатель. Существует и другое ограничение на использование регистровых переменных: к ним нельзя применять операцию взятия адреса &. При разумном ис- пользовании регистровых описаний можно ожидать получения меньших по размеру и более быстрых программ, но улучшение в будущем генерирования кодов может сделать их ненужными. Описание может содержать не более одного спецификатора класса памяти. Если описание не содержит спецификатора клас- са памяти, то считается, что он имеет значение AUTO, если описание находится внутри некоторой функции, и EXTERN в про- тивном случае. исключение: функции никогда не бывает автома- тическими. 16.2. Спецификаторы типа Ниже перечисляются спецификаторы типа. Спецификатор-типа: CHAR SHORT INT LONG UNSIGNED FLOAT DOUBLE спецификатор-структуры-или-объединения определяющее-тип-имя Слова LONG, SHORT и USIGNED можно рассматривать как при- лагательные; допустимы следующие комбинации: SHORT INT LONG INT USIGNED INT LONG FLOAT Последняя комбинация означает то же, что и DOUBLE. В осталь- ном описание может содержать не более одного спецификатора типа. Если описание не содержит спецификатора типа, то счи- тается, что он имеет значение INT. Спецификаторы структур и объединений обсуждаются в п. 16.5; Описания с определяющими тип именами TYPEDEF обсужда- ются в п. 16.8. 16.3. Описатели Входящий в описание список описателей представляет собой последовательность разделенных запятыми описателей, каждый из которых может иметь инициализатор. Список-описателей: инициализируемый-описатель инициализируемый-описатель, список-описателей инициализируемый-описатель: описатель-инициализатор необ Инициализаторы описываются в п. 16.6. Спецификаторы и описа- ния указывают тип и класс памяти объектов, на которые ссыла- ются описатели. Описатели имеют следующий синтаксис: описатель: идентификатор ( описатель ) * описатель описатель () описатель [константное-выражение необ] Группирование такое же как и в выражениях. 16.4. Смысл описателей Каждый описатель рассматривается как утверждение того, что когда конструкция той же самой формы, что и описатель, появляется в выражении, то она выдает объект указанного типа и указанного класса памяти. Каждый описатель содержит ровно один идентификатор; это именно тот идентификатор, который и описывается. Если в качестве описателя появляется просто идентифика- тор, то он имеет тип, указываемый в специфицирующем заголов- ке описания. Описатель в круглых скобках идентичен описателю без круглых скобок, но круглые скобки могут изменять связи в составных описателях. Примеры смотри ниже. Представим себе описание T DI где T - спецификатор типа (подобный INT и т.д.), а DI - опи- сатель. Предположим, что это описание приводит к тому, что соответствующий идентификатор имеет тип "...T", где "..." пусто, если DI просто отдельный идентификатор (так что тип X в "INT X" просто INT). Тогда , если DI имеет форму *D то содержащийся идентификатор будет иметь тип "... Указатель на T". Если DI имеет форму D() то содержащийся идентификатор имеет тип "... Функция, возв- ращающая T". Если DI имеет форму D[константное-выражение] или D[ ] то содержащийся идентификатор имеет тип "...массив T". В первом случае константным выражением является выражение, значение которого можно определить во время компиляции и ко- торое имеет тип INT. (Точное определение константного выра- жения дано в п. 23). Когда несколько спецификаций вида "мас- сив из" оказываются примыкающими, то создается многомерный массив; константное выражение, задающее границы массивов, может отсутствовать только у первого члена этой последова- тельности. Такое опускание полезно, когда массив является внешним и его фактическое определение, которое выделяет па- мять, приводится в другом месте. Первое константное выраже- ние может быть опущено также тогда, когда за описателем сле- дует инициализация. В этом случае размер определяется по числу приведенных инициализируемых элементов. Массив может быть образован из элементов одного из ос- новных типов, из указателей, из структур или объединений или из других массивов (чтобы образовать многомерный массив). Не все возможности, которые разрешены с точки зрения указанного выше синтаксиса, фактически допустимы. Имеются следующие ограничения: функции не могут возвращать массивы, структуры, объединения или функции, хотя они могут возвра- щать указатели на такие вещи; не существует массивов функ- ций, хотя могут быть массивы указателей на функции. Анало- гично, структуры или объединения не могут содержать функцию, но они могут содержать указатель на функцию. В качестве примера рассмотрим описание INT I, *IP, F(), *FIP(), (*PFI)(); в котором описывается целое I, указатель IP на целое, функ- ция F, возвращающая целое, функция FIP, возвращающая указа- тель на целое, и указатель PFI на функцию, которая возвраща- ет целое. Особенно полезно сравнить два последних описателя. Связь в *FIP() можно представить в виде *(FIP()), так что описанием предполагается, а такой же конструкцией в выраже- нии требуется обращение к функции FIP и последующее исполь- зование косвенной адресации для выдачи с помощью полученного результата (указателя) целого. В описателе (*PFI)() дополни- тельные скобки необходимы, поскольку они точно так же, как и в выражении, указывают, что косвенная адресация через указа- тель на функцию выдает функцию, которая затем вызывается; эта вызванная функция возвращает целое. В качестве другого примера приведем описание FLOAT FA[17], *AFP[17]; в котором описывается массив чисел типа FLOAT и массив ука- зателей на числа типа FLOAT. Наконец, STATIC INT X3D[3][5][7]; описывает статический трехмерный массив целых размером 3*5*7. более подробно, X3D является массивом из трех элемен- тов; каждый элемент является массивом пяти массивов; каждый последний массив является массив