ую ширину 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> в файле STDIO.H определяются некоторые макросы и переменные, используемые библиотекой ввода/вывода. Использование угловых скобок вместо обычных двойных кавычек - указание компилятору искать этот файл в справочнике, содержащем заголовки стан- дартной информации (на системе UNIX обычно LUSRLINELUDE). Кроме того, при загрузке программы может оказаться необ- ходимым указать библиотеку явно; на системе PDP-11 UNIX, например, команда компиляции программы имела бы вид: CC исходные файлы и т.д. -LS где -LS указывает на загрузку из стандартной библиотеки. 7.2. Стандартный ввод и вывод - функции GETCHAR и PUTCHAR Самый простой механизм ввода заключается в чтении по од- ному символу за раз из "стандартного ввода", обычно с терми- нала пользователя, с помощью функции GETCHAR. Функция GETCHAR() при каждом к ней обращении возвращает следующий вводимый символ. В большинстве сред, которые поддерживают язык "с", терминал может быть заменен некоторым файлом с по- мощью обозначения < : если некоторая программа PROG исполь- зует функцию GETCHAR то командная строка PROG<INFILE приведет к тому, что PROG будет читать из файла INFILE, а не с терминала. Переключение ввода делается таким образом, что сама программа PROG не замечает изменения; в частности стро- ка"<INFILE" не включается в командную строку аргументов в ARGV. Переключение ввода оказывается незаметным и в том слу- чае, когда вывод поступает из другой программы посредством поточного (PIPE) механизма; командная строка 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 <STDIO.H> 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 45A72 присвоит I значение 56, X - 789.0, пропустит 0123 и поместит в NAME строку "45". при следующем обращении к любой процеду- ре ввода рассмотрение начнется с буквы A. В этих двух приме- рах NAME является указателем и, следовательно, перед ним не нужно помещать знак &. В качестве другого примера перепишем теперь элементарный калькулятор из главы 4, используя для преобразования ввода функцию SCANF: #INCLUDE <STDIO.H> MAIN() /* RUDIMENTARY DESK CALCULATOR */ \( DOUBLE SUM, V; SUM =0; WHILE (SCANF("%LF", &V) !=EOF) PRINTF("\T%.2F\N", SUM += V); \) выполнение функции SCANF заканчивается либо тогда, когда она исчерпывает свою управляющую строку, либо когда некоторый элемент ввода не совпадает с управляющей спецификацией. В качестве своего значения она возвращает число правильно сов- падающих и присвоенных элементов ввода. Это число может быть использовано для определения количества найденных элементов ввода. при выходе на конец файла возвращается EOF; подчерк- нем, что это значение отлично от 0, что следующий вводимый символ не удовлетворяет первой спецификации в управляющей строке. При следующем обращении к SCANF поиск возобновляется непосредственно за последним введенным символом. Заключительное предостережение: аргументы функции SCANF должны быть указателями. Несомненно наиболее распространен- ная ошибка состоит в написании SCANF("%D", N); вместо SCANF("%D", &N); 7.5. Форматное преобразование в памяти От функции SCANF и PRINTF происходят функции SSCANF и SPRINTF, которые осуществляют аналогичные преобразования, но оперируют со строкой, а не с файлом. Обращения к этим функ- циям имеют вид: SPRINTF(STRING, CONTROL, ARG1, ARG2, ...) SSCANF(STRING, CONTROL, ARG1, ARG2, ...) Как и раньше , функция SPRINTF преобразует свои аргументы ARG1, ARG2 и т.д. В соответствии с форматом, указанным в CONTROL, но помещает результаты в STRING, а не в стандартный вывод. KОнечно, строка STRING должна быть достаточно велика, чтобы принять результат. Например, если NAME - это символь- ный массив, а N - целое, то SPRINTF(NAME, "TEMP%D", N); создает в NAME строку вида TEMPNNN, где NNN - значение N. Функция SSCANF выполняет обратные преобразования - она просматривает строку STRING в соответствии с форматом в ар- гументе CONTROL и помещает результирующие значения в аргу- менты ARG1, ARG2 и т.д.эти аргументы должны быть указателя- ми. В результате обращения SSCANF(NAME, "TEMP%D", &N); переменная N получает значение строки цифр, следующих за TEMP в NAME. Упражнение 7-2 -------------- Перепишите настольный калькулятор из главы 4, используя для ввода и преобразования чисел SCANF и/или SSCANF. 7.6. Доступ к файлам Все до сих пор написанные программы читали из стандарт- ного ввода и писали в стандартный вывод, относительно кото- рых мы предполагали, что они магическим образом предоставле- ны программе местной операционной системой. Следующим шагом в вопросе ввода-вывода является написа- ние программы, работающей с файлом, который не связан зара- нее с программой. одной из программ, которая явно демонстри- рует потребность в таких операциях, является CAT, которая объединяет набор из нескольких именованных файлов в стандар- тный вывод. Программа CAT используется для вывода файлов на терминал и в качестве универсального сборщика ввода для программ, которые не имеют возможности обращаться к файлам по имени. Например, команда CAT X.C.Y.C печатает содержимое файлов X.C и Y.C в стандартный вывод. Вопрос состоит в том, как организовать чтение из имено- ванных файлов, т.е., как связать внешние имена, которыми мыслит пользователь, с фактически читающими данные операто- рами. Эти правила просты. Прежде чем можно считывать из неко- торого файла или записывать в него, этот файл должен быть открыт с помощью функции FOPEN из стандартной библиотеки. функция FOPEN берет внешнее имя (подобное X.C или Y.C), про- водит некоторые обслуживающие действия и переговоры с опера- ционной системой (детали которых не должны нас касаться) и возвращает внутреннее имя, которое должно использоваться при последующих чтениях из файла или записях в него. Это внутреннее имя, называемое "указателем файла", фак- тически является указателем структуры, которая содержит ин- формацию о файле, такую как место размещения буфера, текущая позиция символа в буфере, происходит ли чтение из файла или запись в него и тому подобное. Пользователи не обязаны знать эти детали, потому что среди определений для стандартного ввода-вывода, получаемых из файла STDIO.H, содержится опре- деление структуры с именем FILE. Единственное необходимое для указателя файла описание демонстрируется примером: FILE *FOPEN(), *FP; Здесь говорится, что FP является указателем на FILE и FOPEN возвращает указатель на FILE. Oбратите внимание, что FILE является именем типа, подобным INT, а не ярлыку струк- туры; это реализовано как TYPEDEF. (Подробности того, как все это работает на системе UNIX, приведены в главе 8). Фактическое обращение к функции FOPEN в программе имеет вид: FP=FOPEN(NAME,MODE); Первым аргументом функции FOPEN является "имя" файла, кото- рое задается в виде символьной строки. Второй аргумент MODE ("режим") также является символьной строкой, которая указы- вает, как этот файл будет использоваться. Допустимыми режи- мами являются: чтение ("R"), запись ("W") и добавление ("A"). Если вы откроете файл, который еще не сущетвует, для за- писи или добавления, то такой файл будет создан (если это возможно). Открытие существующего файла на запись приводит к отбрасыванию его старого содержимого. Попытка чтения несу- ществующего файла является ощибкой. Ошибки могут быть обус- ловлены и другими причинами (например, попыткой чтения из файла, не имея на то разрешения). При наличии какой-либо ошибки функция возвращает нулевое значение указателя NULL (которое для удобства также определяется в файле STDIO.H). Другой необходимой вещью является способ чтения или за- писи, если файл уже открыт. Здесь имеется несколько возмож- ностей, из которых GETC и PUTC являются простейшими.функция GETC возвращает следующий символ из файла; ей необходим ука- затель файла, чтобы знать, из какого файла читать. Таким об- разом, C=GETC(FP) помещает в "C" следующий символ из файла, указанного посред- ством FP, и EOF, если достигнут конец файла. Функция PUTC, являющаяся обращением к функции GETC, PUTC(C,FP) помещает символ "C" в файл FP и возвращает "C". Подобно фун- кциям GETCHAR и PUTCHAR, GETC и PUTC могут быть макросами, а не функциями. При запуске программы автоматически открываются три фай- ла, которые снабжены определенными указателями файлов. Этими файлами являются стандартный ввод, стандартный вывод и стан- дартный вывод ошибок; соответствующие указатели файлов назы- ваются STDIN, STDOUT и STDERR. Обычно все эти указатели свя- заны с терминалом, но STDIN и STDOUT могут быть перенаправ- лены на файлы или в поток (PIPE), как описывалось в разделе 7.2. Функции GETCHAR и PUTCHAR могут быть определены в терми- налах GETC, PUTC, STDIN и STDOUT следующим образом: #DEFINE GETCHAR() GETC(STDIN) #DEFINE PUTCHAR(C) PUTC(C, STDOUT) При работе с файлами для форматного ввода и вывода можно ис- пользовать функции FSCANF и FPRINTF. Они идентичны функциям SCANF и PRINTF, за исключением того, что первым аргументом является указатель файла, определяющий тот файл, который бу- дет читаться или куда будет вестись запись; управляющая строка будет вторым аргументом. Покончив с предварительными замечаниями, мы теперь в состоянии написать программу CAT для конкатенации файлов. Используемая здесь основная схема оказывается удобной во многих программах: если имеются аргументы в командной стро- ке, то они обрабатываются последовательно. Если такие аргу- менты отсутствуют, то обрабатывается стандартный ввод. Это позволяет использовать программу как самостоятельно, так и как часть большей задачи. #INCLUDE <STDIO.H> MAIN(ARGC, ARGV) /*CAT: CONCATENATE FILES*/ INT ARGC; CHAR *ARGV[]; \( FILE *FP, *FOPEN(); IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/ FILECOPY(STDIN); ELSE WHILE (--ARGC > 0) IF ((FP=FOPEN(*++ARGV,"R"))==NULL) \( PRINTF("CAT:CAN'T OPEN %\N",*ARGV); BREAK; \) ELSE \( FILECOPY(FP); FCLOSE(FP); \) \) FILECOPY(FP) /*COPY FILE FP TO STANDARD OUTPUT*/ FILE *FP; \( INT C; WHILE ((C=GETC(FP)) !=EOF) PUTC(C, STDOUT); \) Указатели файлов STDIN и STDOUT заранее определены в библио- теке ввода-вывода как стандартный ввод и стандартный вывод; они могут быть использованы в любом месте, где можно исполь- зовать объект типа FILE*.они однако являются константами, а не переменными, так что не пытайтесь им что-либо присваи- вать. Функция FCLOSE является обратной по отношению к FOPEN; она разрывает связь между указателем файла и внешним именем, установленную функцией FOPEN, и высвобождает указатель файла для другого файла.большинство операционных систем имеют не- которые ограничения на число одновременно открытых файлов, которыми может распоряжаться программа. Поэтому, то как мы поступили в CAT, освободив не нужные нам более объекты, яв- ляется хорошей идеей. Имеется и другая причина для примене- ния функции FCLOSE к выходному файлу - она вызывает выдачу информации из буфера, в котором PUTC собирает вывод. (При нормальном завершении работы программы функция FCLOSE вызы- вается автоматически для каждого открытого файла). 7.7. Обработка ошибок - STDERR и EXIT Обработка ошибок в CAT неидеальна. Неудобство заключает- ся в том, что если один из файлов по некоторой причине ока- зывается недоступным, диагностическое сообщение об этом пе- чатается в конце объединенного вывода. Это приемлемо, если вывод поступает на терминал, но не годится, если вывод пос- тупает в некоторый файл или через поточный (PIPELINE) меха- низм в другую программу. Чтобы лучше обрабатывать такую ситуацию, к программе точно таким же образом, как STDIN и STDOUT, присоединяется второй выходной файл, называемый STDERR. Если это вообще возможно, вывод, записанный в файле STDERR, появляется на терминале пользователя, даже если стандартный вывод направ- ляется в другое место. Давайте переделаем программу CAT таким образом, чтобы сообщения об ошибках писались в стандартный файл ошибок. "INCLUDE <STDIO.H> MAIN(ARGC,ARGV) /*CAT: CONCATENATE FILES*/ INT ARGC; CHAR *ARGV[]; \( FILE *FP, *FOPEN(); IF(ARGC==1) /*NO ARGS; COPY STANDARD INPUT*/ FILECOPY(STDIN); ELSE WHILE (--ARGC > 0) IF((FP=FOPEN(*++ARGV,"R#))==NULL) \( PRINTF(STDERR, "CAT: CAN'T OPEN,%S\N", ARGV); EXIT(1); \) ELSE \( FILECOPY(FP); \) EXIT(0); \) Программа сообщает об ошибках двумя способами. Диагностичес- кое сообщение, выдаваемое функцией FPRINTF, поступает в STDERR и, таким образом, оказывается на терминале пользова- теля, а не исчезает в потоке (PIPELINE) или в выходном фай- ле. Программа также использует функцию EXIT из стандартной библиотеки, обращение к которой вызывает завершение выполне- ния программы. Аргумент функции EXIT доступен любой програм- ме, обращающейся к данной функции, так что успешное или неу- дачное завершение данной программы может быть проверено дру- гой программой, использующей эту в качестве подзадачи. По соглашению величина 0 в качетсве возвращаемого значения сви- детельствует о том, что все в порядке, а различные ненулевые значения являются признаками нормальных ситуаций. Функция EXIT вызывает функцию FCLOSE для каждого откры- того выходного файла, с тем чтобы вывести всю помещенную в буферы выходную информацию, а затем вызывает функцию _EXIT. Функция _EXIT приводит к немедленному завершению без очистки каких-либо буферов; конечно, при желании к этой функции мож- но обратиться непосредственно. 7.8. Ввод и вывод строк Стандартная библиотека содержит функцию FGETS, совершен- но аналогичную функции GETLINE, которую мы использовали на всем протяжении книги. В результате обращения FGETS(LINE, MAXLINE, FP) следующая строка ввода (включая символ новой строки) считы- вается из файла FP в символьный массив LINE; самое большое MAXLINE_1 символ будет прочитан. Результирующая строка за- канчивается символом \ 0. Нормально функция FGETS возвращает LINE; в конце файла она возвращает NULL. (Наша функция GETLINE возвращает длину строки, а при выходе на конец файла - нуль). Предназначенная для вывода функция FPUTS записывает строку (которая не обязана содержать символ новой строки) в файл: FPUTS(LINE, FP) Чтобы показать, что в функциях типа FGETS и FPUTS нет ничего таинственного, мы приводим их ниже, скопированными непосредственно из стандартной библиотеки ввода-вывода: #INCLUDE <STDIO.H> CHAR *FGETS(S,N,IOP) /*GET AT MOST N CHARS FROM IOP*/ CHAR *S; INT N; REGISTER FILE *IOP; \( REGISTER INT C; REGISTER CHAR *CS; CS = S; WHILE(--N>0&&(C=GETC(IOP)) !=EOF) IF ((*CS++ = C)=='\N') BREAK; *CS = '\0'; RETURN((C==EOF && CS==S) 7 NULL : S); \) FPUTS(S,IOP) /*PUT STRING S ON FILS IOP*/ REGISTER CHAR *S; REGISTER FILE *IOP; \( REGISTER INT C; WHILE (C = *S++) PUTC(C,IOP); \) Упражнение 7-3 --------------- Напишите программу сравнения двух файлов, которая будет печатать первую строку и позицию символа, где они различают- ся. Упражнение 7-4 --------------- Переделайте программу поиска заданной комбинации симво- лов из главы 5 таким образом, чтобы в качестве ввода исполь- зовался набор именованных файлов или, если никакие файлы не указаны как аргументы, стандартный ввод. Следует ли печатать имя файла при нахождении подходящей строки? Упражнение 7-5 -------------- Напишите программу печати набора файлов, которая начина- ет каждый новый файл с новой страницы и печатает для каждого файла заголовок и счетчик текущих страниц. 7.9. Несколько разнообразных функций Стандартная библиотека предоставляет множество разнооб- разных функций, некоторые из которых оказываются особенно полезными. Мы уже упоминали функции для работы со строками: STRLEN, STRCPY, STRCAT и STRCMP. Вот некоторые другие. 7.9.1. Проверка вида символов и преобразования Некоторые макросы выполняют проверку символов и преобра- зования: SALPHA(C) не 0, если "C" алфавитный символ, 0 - если нет. SUPPER(C) Не 0, если "C" буква верхнего регистра, 0 - если нет. SLOWER(C) Не 0, если "C" буква нижнего регистра, 0 - если нет. SDIGIT(C) Не 0, если "C" цифра, 0 - если нет. SSPACL(C) Не 0, если "C" пробел, табуляция или новая строка, 0 - если нет. OUPPER(C) Преобразует "C" в букву верхнего регистра. OLOWER(C) Преобразует "C" в букву нижнего регистра. 7.9.2. Функция UNGETC Стандартная библиотека содержит довольно ограниченную версию функции UNGETCH, написанной нами в главе 4; она назы- вается UNGETC. В результате обращения UNGETC(C,FP) символ "C" возвращается в файл FP. Позволяется возвращать в каждый файл только один символ. Функция UNGETC может быть использована в любой из функций ввода и с макросами типа SCANF, GETC или GETCHAR. 7.9.3. Обращение к системе Функция SYSTEM(S) выполняет команду, содержащуюся в сим- вольной строке S, и затем возобновляет выполнение текущей программы. Содержимое S сильно зависит от используемой опе- рационной системы. В качестве тривиального примера, укажем, что на системе UNIX строка SYSTEM("DATE"); приводит к выполнению программы DATE, которая печатает дату и время дня. 7.9.4. Управление памятью Функция CALLOC весьма сходна с функцией ALLOC, использо- ванной нами в предыдущих главах. В результате обращения CALLOC(N, SIZEOF(OBJCCT)) возвращается либо указатель пространства, достаточного для размещения N объектов указанного размера, либо NULL, если запрос не может быть удволетворен. Отводимая память инициа- лизируется нулевыми значениями. Указатель обладает нужным для рассматриваемых объектов выравниванием, но ему следует приписывать соответствующий тип, как в CHAR *CALLOC(); INT *IP; IP=(INT*) CALLOC(N,SIZEOF(INT)); Функция CFREE(P) освобождает пространство, на которое указывает "P", причем указатель "P" певоначально должен быть получен в результате обращения к CALLOC. Здесь нет никаких ограничений на порядок освобождения пространства, но будет неприятнейшей ошибкой освободить что-нибудь, что не было по- лучено обращением к CALLOC. Реализация программы распределения памяти, подобной CALLOC, в которой размещенные блоки могут освобождаться в произвольном порядке, продемонстрирована в главе 8.  * 8. Интерфейс системы UNIX *  Материал этой главы относится к интерфейсу между с-прог- раммами и операционной системой UNIX. Так как большинство пользователей языка "C" работают на системе UNIX, эта глава окажется полезной для большинства читателей. даже если вы используете с-компилятор на другой машине, изучение приводи- мых здесь примеров должно помочь вам глубже проникнуть в ме- тоды программирования на языке "C". Эта глава делится на три основные части: ввод/вывод, система файлов и распределение памяти. Первые две части предполагают небольшое знакомство с внешними характеристика- ми системы UNIX. В главе 7 мы имели дело с системным интерфейсом, который одинаков для всего многообразия операционных систем. На каж- дой конкретной системе функции стандартной библиотеки должны быть написаны в терминах ввода-вывода, доступных на данной машине. В следующих нескольких разделах мы опишем основную систему связанных с вводом и выводом точек входа операцион- ной системы UNIX и проиллюстрируем, как с их помощью могут быть реализованы различные части стандартной библиотеки. 8.1. Дескрипторы файлов В операционной системе UNIX весь ввод и вывод осуществ- ляется посредством чтения файлов или их записи, потому что все периферийные устройства, включая даже терминал пользова- теля, являются файлами