Жанр: Учеба

Язык Си

сторінка №11

; /* NUMBER OF OCCURRENCES */
STRUCT TNODE *LEFT; /* LEFT CHILD */
STRUCT TNODE *RIGHT; /* RIGHT CHILD */
\);

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

STRUCT TNODE *LEFT;

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

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

#DEFINE MAXWORD 20
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 1

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

INWORD = YES;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

6.7. Поля

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

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

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

FLAGS \!= EXTERNAL \! STATIC;

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

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

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

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

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

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

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

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 1;

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

FLAGS.IS_EXTERN = FLAGS.IS_STATIC = 0;

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

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

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

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

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 использует PUTCHAR,
то командная строка

PROG"OUTFILE

приведет к записи стандартного вывода в файл OUTFILE, а не
на терминал. На системе UNIX можно также использовать поточный
механизм. Строка

PROG \! ANOTHERPROG

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

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

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.