WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 |

«Б.В. Керниган, Д.М. Ричи. ...»

-- [ Страница 3 ] --

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

такая программа будет работать вечно. (Более точно, ожидаемое время работы растет как квадрат числа вводимых слов). Как же нам организовать программу, чтобы справиться со списком произвольных слов?

Одно из решений состоит в том, чтобы все время хранить массив поступающих до сих пор слов в упорядоченном виде, помещая каждое слово в нужное место по мере их поступления. OДнако это не следует делать, перемещая слова в линейном массиве, — это также потребует слишком много времени. Вместо этого мы используем структуру данных, называемую доичным деревом.

Каждому новому слову соответствует один "узел" дерева;

каждый узел содержит:

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

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

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

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

Возвращаясь назад к описанию узла, ясно, что это будет структура с четырьмя компонентами:

STRUCT TNODE \( /* THE BASIC NODE */ CHAR *WORD;

/* POINTS TO THE TEXT */ INT COUNT;

/* NUMBER OF OCCURRENCES */ STRUCT TNODE *LEFT;

/* LEFT CHILD */ STRUCT TNODE *RIGHT;

/* RIGHT CHILD */ \);

Это "рекурсивное" описание узла может показаться рискованным, но на самом деле оно вполне корректно. Структура не имеет права содержать ссылку на саму себя, но Б.В. Керниган, Д.М. Ричи. Язык С.

STRUCT TNODE *LEFT;

описывает LEFT как указатель на узел, а не каксамузел.

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

Ведущая программа просто считывает слова с помощью функции GETWORD и помещает их в дерево, используя функцию TREE.

#DEFINE MAXWORD MAIN() /* WORD FREGUENCY COUNT */ \( STRUCT TNODE *ROOT, *TREE();

CHAR WORD[MAXWORD];

INT T;

ROOT = NULL;

WHILE ((T = GETWORD(WORD, MAXWORD)) \! = EOF) IF (T == LETTER) ROOT = TREE(ROOT, WORD);

TREEPRINT(ROOT);

\) Функция TREE сама по себе проста. Слово передается функцией MAIN к верхнему уровню (корню) дерева. На каждом этапе это слово сравнивается со словом, уже хранящимся в этом узле, и с помощью рекурсивного обращения к TREE просачивается вниз либо к левому, либо к правому поддереву. В конце концов это слово либо совпадает с каким-то словом, уже находящимся в дереве (в этом случае счетчик увеличивается на единицу), либо программа натолкнется на нулевой указатель, свидетельствующий о необходимости создания и добавления к дереву но вого узла. В случае создания нового узла функция TREE возвращает указатель этого узла, который помещается в родительский узел.

STRUCT TNODE *TREE(P, W) /* INSTALL W AT OR BELOW P */ STRUCT TNODE *P;

CHAR *W;

\( STRUCT TNODE *TALLOC();

CHAR *STRSAVE();

INT COND;

IF (P == NULL) \( /* A NEW WORD Б.В. Керниган, Д.М. Ричи. Язык С.

HAS ARRIVED */ P == TALLOC();

/* MAKE A NEW NODE */ P->WORD = STRSAVE(W);

P->COUNT = 1;

P->LEFT = P->RIGHT = NULL;

\) ELSE IF ((COND = STRCMP(W, P->WORD)) == 0) P->COUNT++;

/* REPEATED WORD */ ELSE IF (COND < 0)/* LOWER GOES INTO LEFT SUBTREE */ P->LEFT = TREE(P->LEFT, W);

ELSE /* GREATER INTO RIGHT SUBTREE */ P->RIGHT = TREE(P->RIGHT, W);

RETURN(P);

\) Память для нового узла выделяется функцией TALLOC, являющейся адаптацией для данного случая функции ALLOC, написанной нами ранее. Она возвращает указатель свободного пространства, пригодного для хранения нового узла дерева. (Мы вскоре обсудим это подробнее). Новое слово копируется функцией STRSAVE в скрытое место, счетчик инициализируется единицей, и указатели обоих потомков полагаются равными нулю. Эта часть программы выполняется только при добавлении нового узла к ребру дерева. Мы здесь опустили проверку на ошибки возвращаемых функций STRSAVE и TALLOC значений (что неразумно для практически работающей программы).

Функция TREEPRINT печатает дерево, начиная с левого поддерева;

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

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

TREEPRINT (P) /* PRINT TREE P RECURSIVELY */ STRUCT TNODE *P;

\( IF (P != NULL) \( TREEPRINT (P->LEFT);

PRINTF("%4D %S\N", P->COUNT, P->WORD);

TREEPRINT (P->RIGHT);

\) \) Практическое замечание: если дерево становится "несбалансированным" из-за того, что слова поступают не в случайном порядке, то время работы программы может расти слишком быстро. Вхудшемслучае, когда поступающие слова уже упоря дочены, настоящая программа осуществляет дорогостоящую имитацию линейного Б.В. Керниган, Д.М. Ричи. Язык С.

поиска. Существуют различные обобщения двоичного дерева, особенно 2-3 деревья и AVL деревья, которые не ведут себя так "в худших случаях", но мы не будем здесь на них останавливаться.

Прежде чем расстаться с этим примером, уместно сделать небольшое отступление в связи с вопросом о распределении памяти. Ясно, что в программе желательно иметь только один распределитель памяти, даже если ему приходится размещать различные виды объектов. Но если мы хотим использовать один распределитель памяти для обработки запросов на выделение памяти для указателей на переменные типа CHAR и для указателей на STRUCT TNODE, то при этом возникают два вопроса. Первый: как выполнить то существующее на большинстве реальных машин ограничение, что объекты определенных типов должны удовлетворять требованиям выравнивания (например, часто целые должны размещаться в четных адресах)? Второй: как организовать описания, чтобы справиться с тем, что функция ALLOC должна возвращать различные виды указателей?

Вообще говоря, требования выравнивания легко выполнить за счет выделения некоторого лишнего пространства, просто обеспечив то, чтобы распределитель памяти всегда возвращал указатель, удовлетворяющий всем ограничениям выравнивания. Например, на PDP-11 достаточно, чтобы функция ALLOC всегда возвращала четный указатель, поскольку в четный адрес можно поместить любой тип объекта. единственный расход при этом — лишний символ при запросе на нечетную длину. Аналогичные действия предпринимаются на других машинах. Таким образом, реализация ALLOC может не оказаться переносимой, но ее использование будет переносимым. Функция ALLOC из главы 5 не предусматривает никакого определенного выравнивания;

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

Вопрос описания типа функции ALLOC является мучительным для любого языка, который серьезно относится к проверке типов. Лучший способ в языке "C" — объявить, что ALLOC возвращает указатель на переменную типа CHAR, а затем явно преобразовать этот указатель к желаемому типу с помощью операции перевода типов.

Таким образом, если описать P в виде CHAR *P;

то (STRUCT TNODE *) P преобразует его в выражениях в указатель на структуру типа TNODE. Следовательно, функцию TALLOC можно записать в виде:

STRUCT TNODE *TALLOC() \( CHAR *ALLOC();

RETURN ((STRUCT TNODE *) ALLOC(SIZEOF(STRUCT TNODE)));

\) это более чем достаточно для работающих в настоящее время Б.В. Керниган, Д.М. Ричи. Язык С.

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

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

Напишите программу, которая читает "C"-программу и печатает в алфавитном порядке каждую группу имен переменных, которые совпадают в первых семи символах, но отличаются где-то дальше. (Сделайте так, чтобы 7 было параметром).

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

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

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

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

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

Oно должно быть замещено на 1.

Имеются две основные процедуры, которые управляют именами и заменяющими их текстами. Функция INSTALL(S,T) записывает имя S и заменяющий текст T в таблицу;

здесь S и T просто символьные строки. Функция LOOKUP(S) ищет имя S в таблице и возвращает либо указатель того места, где это имя найдено, либо NULL, если этого имени в таблице не оказалось.

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

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

STRUCT NLIST \( /* BASIC TABLE ENTRY */ CHAR *NAME;

Б.В. Керниган, Д.М. Ричи. Язык С.

CHAR *DEF;

STRUCT NLIST *NEXT;

/* NEXT ENTRY IN CHAIN */ \);

Массив указателей это просто DEFINE HASHSIZE TATIC STRUCT NLIST *HASHTAB[HASHSIZE] /* POINTER TABLE */ Значение функции хеширования, используемой обеими функциями LOOKUP и INSTALL, получается просто как остаток от деления суммы символьных значений строки на размер массива. (Это не самый лучший возможный алгоритм, но его достоинство состоит в исключительной простоте).

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

\( INT HASHVAL;

FOR (HASHVAL = 0;

*S != '\0';

) HASHVAL += *S++;

RETURN(HASHVAL % HASHSIZE);

\) В результате процесса хеширования выдается начальный индекс в массиве HASHTAB ;

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

если нет, то она возвращает NULL.

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

\( STRUCT NLIST *NP;

FOR (NP = HASHTAB[HASH(S)];

NP != NULL;

NP=NP->NEXT) IF (STRCMP(S, NP->NAME) == 0) RETURN(NP);

/* FOUND IT */ RETURN(NULL);

/* NOT FOUND */ Функция INSTALL использует функцию LOOKUP для определения, не присутствует ли уже вводимое в данный момент имя;

если это так, то новое определение должно вытеснить старое. В противном случае создается совершенно Б.В. Керниган, Д.М. Ричи. Язык С.

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

STRUCT NLIST *INSTALL(NAME, DEF) /* PUT (NAME, DEF) */ CHAR *NAME, *DEF;

\( STRUCT NLIST *NP, *LOOKUP();

CHAR *STRSAVE(), *ALLOC();

INT HASHVAL;

IF((NP = LOOKUP(NAME)) == NULL) \( /* NOT FOUND */ NP = (STRUCT NLIST *) ALLOC(SIZEOF(*NP));

IF (NP == NULL) RETURN(NULL);

IF ((NP->NAME = STRSAVE(NAME)) == NULL) RETURN(NULL);

HASHVAL = HASH(NP->NAME);

NP->NEXT = HASHTAB[HASHVAL];

HASHTAB[HASHVAL] = NP;

\) ELSE /* ALREADY THERE */ FREE((NP->DEF);

/* FREE PREVIOUS DEFINITION */ IF ((NP->DEF = STRSAVE(DEF)) == NULL) RETURN (NULL);

RETURN(NP);

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

смотрите главы 7 и 8.

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

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

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

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

Б.В. Керниган, Д.М. Ричи. Язык С.

6.7. Поля Когда вопрос экономии памяти становится очень существенным, то может оказаться необходимым помещать в одно машинное слово несколько различных объектов;

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

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

Обычный способ, которым это делается, состоит в определении набора "масок", отвечающих соответствущим битовым позициям, как в #DEFINE KEYWORD #DEFINE EXTERNAL #DEFINE STATIC (числа должны быть степенями двойки). Тогда обработка битов сведется к "жонглированию битами" с помощью операций сдвига, маскирования и дополнения, описанных нами в главе 2.

Некоторые часто встречающиеся идиомы:

FLAGS \!= EXTERNAL \! STATIC;

включает биты EXTERNAL и STATIC в FLAGS, вто время как FLAGS &= \^(еXTERNAL \! STATIC);

их выключает, а IF((FLAGS&(EXTERNAL \! STATIC)) == 0)...

истинно, еслиоба бита выключены.

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

STRUCT \( UNSIGNED IS_KEYWORD : 1;

UNSIGNED IS_EXTERN : 1;

Б.В. Керниган, Д.М. Ричи. Язык С.

UNSIGNED IS_STATIC : 1;

\) FLAGS;

Здесь определяется переменная с именем FLAGS, которая содержит три 1-битовых поля. Следующее за двоеточием число задает ширину поля в битах. Поля описаны как UNSIGNED, чтобы подчеркнуть, что онидействительно будут величинамибез знака.

На отдельные поля можно ссылаться, как FLAGS.IS_STATIE, FLAGS. IS_EXTERN, FLAGS.IS_KEYWORD И т.д., то есть точно так же, как на другие члены структуры.

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

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 1;

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

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 0;

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

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

для их проверки.

Поле не может перекрывать границу INT;

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

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

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

Другие ограничения, которые следует иметь в виду: поля не имеют знака;

они могут храниться только в переменных типа INT (или, что эквивалентно, типа UNSIGNED);

они не являются массивами;

они не имеют адресов, так что к ним не применима операция &.

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

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

Б.В. Керниган, Д.М. Ричи. Язык С.

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

UNION U_TAG \( INT IVAL;

FLOAT FVAL;

CHAR *PVAL;

\) UVAL;

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

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

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

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

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

IF (UTYPE == INT) PRINTF("%D\N", UVAL.IVAL);

ELSE IF (UTYPE == FLOAT) PRINTF("%F\N", UVAL.FVAL);

ELSE IF (UTYPE == STRING) PRINTF("%S\N", UVAL.PVAL);

ELSE PRINTF("BAD TYPE %D IN UTYPE\N", UTYPE);

Б.В. Керниган, Д.М. Ричи. Язык С.

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

INT FLAGS;

INT UTYPE;

UNION \( INT IVAL;

FLOAT FVAL;

CHAR *PVAL;

\) UVAL;

\) SYMTAB[NSYM];

на переменную IVAL можно сослаться как SYMTAB[I].UVAL.IVAL а на первыйсимвол строкиPVAL как *SYMTAB[I].UVAL.PVAL В сущности объединение является структурой, в которой все члены имеют нулевое смещение. Сама структура достаточно велика, чтобы хранить "самый широкий" член, и выравнивание пригодно для всех типов, входящих в объединение. Как и в случае структур, единственными операциями, которые в настоящее время можно проводить с объединениями, являются доступ к члену и извлечение адреса;

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

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

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

делает имя LENGTH синонимом для INT. "Тип" LENGTHможет быть использован в описаниях, переводов типов и т.д. Точно таким Б.В. Керниган, Д.М. Ричи. Язык С.

же образом, как и тип INT:

LENGTH LEN, MAXLEN;

LENGTH *LENGTHS[];

Аналогично описанию TYPEDEF CHAR *STRING;

делает STRING синонимом для CHAR*, то есть для указателя на символы, что затем можно использовать в описаниях вида STRING P, LINEPTR[LINES], ALLOC();

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

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

TYPEDEF STRUCT TNODE \( /* THE BASIC NODE */ CHAR *WORD;

/* POINTS TO THE TEXT */ INT COUNT;

/* NUMBER OF OCCURRENCES */ STRUCT TNODE *LEFT;

/* LEFT CHILD */ STRUCT TNODE *RIGHT;

/* RIGHT CHILD */ \) TREENODE, *TREEPTR;

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

RETURN((TREEPTR) ALLOC(SIZEOF(TREENODE)));

\) Необходимо подчеркнуть, что описание TYPEDEF не приводит к созданию нового в каком-либо смысле типа;

оно только добавляет новое имя для некоторого существующего типа. при этом не возникает и никакой новой семантики: описанные таким способом переменные обладают точно теми же свойствами, что и переменные, описанные явным образом. По существу конструкция TYPEDEF сходна с #DEFINE за исключением того, что она интерпретируется компилятором и потому может осуществлять подстановки текста, которые выходят за пределы возможностей мак ропроцессора языка "C". Например, Б.В. Керниган, Д.М. Ричи. Язык С.

TYPEDEF INT (*PFI) ();

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

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

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

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

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

Мы здесь не будем пытаться описать всю библиотеку ввода/вывода;

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

7.1. Обращение к стандартной библиотеке Каждый исходный файл, который обращается к функции из стандартной библиотеки, должен вблизи начала содержать строку Б.В. Керниган, Д.М. Ричи. Язык С.

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

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

на системе PDP-11 UNIX, например, команда компиляции программы имела бы вид:

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

7.2. Стандартный ввод и вывод — функции GETCHAR и PUTCHAR Самый простой механизм ввода заключается в чтении по одному символу за раз из "стандартного ввода", обычно с терминала пользователя, с помощью функции GETCHAR. Функция GETCHAR() при каждом к ней обращении возвращает следующий вводимый символ. В большинстве сред, которые поддерживают язык "с", терминал может быть заменен некоторым файлом с помощью обозначения < : если некоторая программа PROG использует функцию GETCHAR то командная строка PROG

Переключение ввода делается таким образом, что сама программа PROG не замечает изменения;

в частности строка"

командная строка OTHERPROG \! PROG прогоняет две программы, OTHERPROG и PROG, и организует так, что стандартным вводом для PROG служит стандартный вывод OTHERPROG.

Функция GETCHAR возвращает значение EOF, когда она попадает на конец файла, какой бы ввод она при этом не считывала. Стандартная библиотека полагает символическую константу EOF равной -1 (посредством #DEFINE в файле STDIO.H), но проверки следует писать в терминах EOF, а не -1, чтобы избежать зависимости от конкретного значения.

Вывод можно осуществлять с помощью функции PUTCHAR(C), помещающей символ 'с' в "стандартный ввод", который по умолчанию является терминалом. Вывод Б.В. Керниган, Д.М. Ричи. Язык С.

можно направить в некоторый файл с помощью обозначения > : если PROG использует PUTCHAR, то командная строка PROG>OUTFILE приведет к записи стандартного вывода в файл OUTFILE, а не на терминал. На системе UNIX можно также использовать поточный механизм.

Строка PROG \! ANOTHERPROG помещает стандартный вывод PROG в стандартный ввод ANOTHERPROG. И опять PROG не будет осведомлена об изменении направления.

Вывод, осуществляемый функцией PRINTF, также поступает в стандартный вывод, и обращения к PUTCHAR и PRINTF могут перемежаться.

Поразительное количество программ читает только из одного входного потока и пишет только в один выходной поток;

для таких программ ввод и вывод с помощью функций GETCHAR, PUTCHAR и PRINTF может оказаться вполне адекватным и для начала определенно достаточным. Это особенно справедливо тог да, когда имеется возможность указания файлов для ввода и вывода и поточный механизм для связи вывода одной программы с вводом другой. Рассмотрим, например, программу LOWER, которая преобразует прописные буквы из своего ввода в строчные:

#INCLUDE MAIN() /* CONVERT INPUT TO LOWER CASE */ \( INT C;

WHILE((C= GETCHAR()) != EOF) PUTCHAR(ISUPPER(C)? TOLOWER(C) : C);

\) "Функции" ISUPPER и TOLOWER на самом деле являются макросами, определенными в STDIO.H. Макрос ISUPPER проверяет, является ли его аргумент буквой из верхнего регистра, и возвращает ненулевое значение, если это так, и нуль в противном случае.

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

Если требуется преобразовать несколько файлов, то можно собрать эти файлы с помощью программы, подобной утилите CAT системы UNIX, CAT FILE1 FILE2... \! LOWER>OUTPUT Б.В. Керниган, Д.М. Ричи. Язык С.

и избежать тем самым вопроса о том, как обратиться к этим файлам из программы. (Программа CAT приводится позже в этой главе).

Кроме того отметим, что в стандартной библиотеке ввода/вывода "функции" GETCHAR и PUTCHAR на самом деле могут быть макросами. Это позволяет избежать накладных расходов на обращение к функции для обработки каждого символа. В главе 8 мы продемонстрируем, как это делается.

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

Мы уже всюду в предыдущих главах неформально использовали функцию PRINTF;

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

Каждая спецификация преобразования начинается с символа % и заканчивается символом преобразования. Между % и символом преобразования могут находиться:

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

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

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

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

- Строка цифр (точность), которая указывает максимальное число символов строки, которые должны быть напечатаны, или число печатаемых справа от десятичной точки цифр для переменных типа FLOAT или DOUBLE.

- Модификатор длины L, который указывает, что соответствующий элемент данных имеет тип LONG, а не INT.

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

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

O — Аргумент преобразуется в беззнаковую восьмеричную форму (без лидирующего нуля).

Б.В. Керниган, Д.М. Ричи. Язык С.

X — Аргумент преобразуется в беззнаковую шестнадцатеричную форму (без лидирующих 0X).

U — Аргумент преобразуется в беззнаковую десятичную форму.

C — Аргумент рассматривается как отдельный символ.

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

E — Аргумент, рассматриваемый как переменная типа FLOAT или DOUBLE, преобразуется в десятичную форму в виде [-]M.NNNNNNE[+-]XX, где длина строки из N определяется указанной точностью. Точность по умолчанию равна 6.

F — Аргумент, рассматриваемый как переменная типа FLOAT или DOUBLE, преобразуется в десятичную форму в виде [-]MMM.NNNNN, где длина строки из N определяется указанной точностью. Точность по умолчанию равна 6. отметим, что эта точность не определяет количество печатаемых в формате F значащих цифр.

G — Используется или формат %E или %F, какой короче;

незначащие нули не печатаются.

Если идущий за % символ не является символом преобразования, то печатается сам этот символ;

следовательно,символ % можно напечатать, указав %%.

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

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

:%10S: :HELLO, WORLD:

:%10-S: :HELLO, WORLD:

:%20S: : HELLO, WORLD:

:%-20S: :HELLO, WORLD :

:%20.10S: : HELLO, WOR:

:%-20.10S: :HELLO, WOR :

:%.10S: :HELLO, WOR:

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

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

Б.В. Керниган, Д.М. Ричи. Язык С.

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

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

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

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

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

Управляющая строка может содержать:

- пробелы, табуляции или символы новой строки ("символы пустых промежутков"), которые игнорируются.

- Обычные символы (не %), которые предполагаются совпадающими со следующими отличными от символов пустых промежутков символами входного потока.

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

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

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

Символ преобразования определяет интерпретацию поля ввода;

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

соответствующий ар гумент должен быть указателем на целое.

O — На вводе ожидается восьмеричное целое (с лидирующим нулем или без него);

соответствующий аргумент должен быть указателем на целое.

Б.В. Керниган, Д.М. Ричи. Язык С.

X — На вводе ожидается шестнадцатеричное целое (с лидирующими 0X или без них);

соответствующий аргумент должен быть указателем на целое.

H — На вводе ожидается целое типа SHORT;

соответсвующий аргумент должен быть указателем на целое типа SHORT.

C — Ожидается отдельный символ;

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

следующий вводимый символ помещается в указанное место. Обычный пропуск символов пустых промежутков в этом случае подавляется;

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

S — Ожидается символьная строка;

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

F — Ожидается число с плавающей точкой;

соответствующий аргумент должен быть указателем на переменную типа FLOAT.

Е — символ преобразования E является синонимом для F. Формат ввода переменной типа FLOAT включает необязательный знак, строку цифр, возможно содержащую десятичную точку и необязательное поле экспоненты, состоящее из буквы E, за которой следует целое, возможно имеющее знак.

Перед символами преобразования D, O и X может стоять L, которая означает, что в списке аргументов должен находиться указатель на переменную типа LONG, а не типа INT. Аналогично, буква L может стоять перед символами преобразования E или F, говоря о том, что в списке аргументов должен находиться указатель на переменную типа DOUBLE, а не типа FLOAT.

Например, обращение INT I;

FLOAT X;

CHAR NAME[50];

SCANF("&D %F %S", &I, &X, NAME);

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

FLOAT X;

CHAR NAME[50];

SCANF("%2D %F %*D %2S", &I, &X, NAME);

с вводом Б.В. Керниган, Д.М. Ричи. Язык С.

56789 0123 45A присвоит I значение 56, X — 789.0, пропустит 0123 и поместит в NAME строку "45".

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

В качестве другого примера перепишем теперь элементарный калькулятор из главы 4, используя для преобразования ввода функцию SCANF:

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

Функция 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 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.

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

Упражнение 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 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 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 #DEFINE BUFSIZE #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 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 /* ERROR HAS OCCURRED ON THIS FILE */ DEFINE NULL 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 #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 _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))\!= 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 Б.В. Керниган, Д.М. Ричи. Язык С.

#INCLUDE /*TYPEDEFS*/ #INCLUDE /*DIRECTORY ENTRY STRUCTURE*/ #INCLUDE /*STRUCTURE RETURNED BY STAT*/ #DEFINE BUFSIZE 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,".")== \!\! STRCMP(DIRBUF.D_NAME,"..")== CONTINUE;

/*SKIP SELF AND PARENT*/ FOR (I=0,NEP=NBP;

I

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 \!\! PS.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. резюмируются характеристики аппаратных средств, которые влияют на размеры.

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

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

Синтаксис суммируется в пункте 18.

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

Язык "C" основывает интерпретацию идентификатора на двух признаках идентификатора: его классе памяти и его типе. Класс памяти определяет место и время хранения памяти, связанной с идентификатором;

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

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

быстрых регистрах машины;

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

Вязыке "C" предусмотрено несколько основных типов объектов:

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

Можно использовать до трех размеров целых, описываемых как SHORT INT, INT и LONG INT. Длинные целые занимают не меньше памяти, чем короткие, но в конкретной реализации может оказаться, что либо короткие целые, либо длинные целые, либо те и другие будут эквивалентны простым целым. "Простые" целые имеют естественный размер, предусматриваемый архиитектурой используемой машины;

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

Целые без знака, описываемые как UNSIGNED, подчиняются законам арифметики по модулю 2**N, где N — число битов в их представлении. (На PDP- длинные величины без знака не предусмотрены).

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

выводы суммируются ниже в п. 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<

освобождающиеся биты заполняются нулем.

значением выражения E1>>E2 является E1, сдвинутое вправо на E2 битовых позиций. Если E1 имеет тип UNSIGNE, то сдвиг вправо гарантированно будет ло гическим (заполнение нулем);

в противном случае сдвиг может быть (и так и есть на PDP-11) арифметическим (освобождающиеся биты заполняются копией знакового бита).

15.6. Операции отношения Операции отношения группируются слева направо, но этот факт не очень полезен;

выражение A

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

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

15.7. Операции равенства Выражение-равенства: выражение == выражение выражение != выражение Операции == (равно) и != (не равно) в точности аналогичны операциям отношения, за исключением того, что они имеют более низкий уровень старшинства. (Поэтому Б.В. Керниган, Д.М. Ричи. Язык С.

значение выражения A

Указатель можно сравнивать с целым, но результат будет машинно независимым только в том случае, если целым является константа 0. Гарантируется, что указатель, которому присвоено значение 0, не указывает ни на какой объект и на самом деле оказывается равным 0;

общепринято считать такой указатель нулем.

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

результатом является побитовая функция 'и' операндов. Эта операция применима только к операндам целочисленного типа.

15.9. Побитовая операция исключающего 'или' Выражение-исключающего-или:

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

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

15.10. Побитовая операция включающего 'или' Выражение-включающего-или:

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

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

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

более того, если первый операнд равен 0, то значение второго операнда вообще не вычисляется.

Pages:     | 1 | 2 || 4 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

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