Возможности ввода-вывода не являются частью самого языка Си, поэтому мы подробно и не рассматривали их до сих пор. Между тем реальные программы взаимодействуют со своим окружением гораздо более сложным способом, чем те, которые были затронуты ранее. В этой главе мы опишем стандартную библиотеку, набор функций, обеспечивающих ввод-вывод, работу со стрингами, управление памятью, стандартные математические функции и разного рода сервисные Си-программы. Но особое внимание уделим вводу-выводу.
Библиотечные функции ввода–вывода точно определяются стандартом ANSI, так что они совместимы на любых установках, где поддерживается Си. Программы, которые в своем взаимодействии с системным окружением не выходят за рамки возможностей стандартной библиотеки, можно без изменений переносить с одной машины на другую.
Свойства библиотечных функций специфицированы в более чем дюжине головных
файлов; вам уже встречались некоторые из них, в том числе <stdio.h>
,
<string.h>
и <ctype.h>
. Мы не рассматриваем здесь всю библиотеку, так как нас
больше интересует написание Си-программ, чем использование библиотечных
функций. Стандартная библиотека подробно описана в приложении B.
Как уже говорилось в гл. 1, библиотечные функции реализуют простую модель текстового ввода-вывода. Текстовый поток состоит из последовательности строк; каждая строка заканчивается литерой новая_строка. Если система в чем-то не следует принятой модели, библиотека сделает так, чтобы казалось, что эта модель удовлетворяется полностью. Например, пара литер — возврат_каретки и перевод_строки — при вводе могла бы быть преобразована в одну литеру новая_строка, а при выводе выполнялось бы обратное преобразование.
Простейший механизм ввода — это чтение одной литеры из стандартного ввела
(обычно с клавиатуры) функцией getchar
:
int getchar(void);
В качестве результата каждого своего вызова функция getchar
возвращает
следующую литеру ввода или, если обнаружен конец файла, EOF
. Именованная
константа EOF
(аббревиатура от end of file — конец файла) определена в
<stdio.h>
. Обычно значение EOF
равно -1, но, чтобы не зависеть от конкретного
значения этой константы, ссылаться на нее следует по имени (EOF
).
Во многих системах клавиатуру можно заменить файлом, перенаправив ввод при
помощи значка <
. Так, если программа prog
использует getchar
, то командная
строка
prog <infile
предпишет программе prog
читать литеры из infile
, а не с клавиатуры.
Переключение ввода делается так, что сама программа prog
не замечает подмены;
в частности, стринг «<infile
» не будет включен в аргументы командной строки
argv
. Переключение ввода будет также незаметным, если ввод исходит от другой
программы и передается через «трубопроводный» механизм. В некоторых системах
командная строка
otherprog | prog
приведет к тому, что две программы, otherprog
и prog
, соединятся напрямую,
т.е. стандартный вывод otherprog
станет стандартным вводом для prog
.
Функция
int putchar(int)
используется для вывода: putchar(c)
отправляет литеру c
в стандартный вывод,
под которым по умолчанию подразумевается экран. Функция putchar
в качестве
результата возвращает посланную литеру или, в случае ошибки, EOF
. То же и в
отношении вывода: при помощи записи вида >имя_файла
вывод можно перенаправить
в файл. Например, если prog
использует для вывода функцию putchar
, то
prog >outfile
будет направлять стандартный вывод не на экран, а в outfile
. А командная
строка
prog | anotherprog
соединит стандартный вывод программы prog
со стандартным вводом программы
anotherprog
.
Вывод, осуществляемый функцией printf
, также отправляется в стандартный
выходной поток. Вызовы putchar
и printf
могут как угодно чередоваться, при
этом вывод будет формироваться в той последовательности, в которой
происходили вызовы этих функций.
Любой исходный Си-файл, использующий хотя бы одну функцию библиотеки ввода-вывода, должен содержать в себе строку
#include <stdio.h>
причем она должна быть расположена до первого обращения к вводу-выводу. Если
имя головного файла заключено в угловые скобки <
и >
, это значит, что поиск
головного файла ведется в стандартном месте (например, в системе UNIX это
обычно директория /usr/include
).
Многие программы читают только из одного входного потока и пишут только в
один выходной поток. Для организации ввода-вывода таким программам вполне
хватит функций getchar
, putchar
и printf
, а для начального обучения
ознакомления с этими функциями уж точно достаточно. В частности,
перечисленных функций достаточно, когда требуется вывод одной программы
соединить с вводом следующей. В качестве примера рассмотрим программу lower
,
переводящую свой ввод на нижний регистр:
#include <stdio.h> #include <ctype.h> /* lower: переводит ввод на нижний регистр */ main() { int c; while ((c = getchar()) != EOF) putchar (tolower (c)); return 0; }
Функция tolower
определена в <ctype.h>
. Она переводит буквы верхнего регистра
в буквы нижнего регистра, а остальные литеры возвращает без изменений. Как мы
уже упоминали, «функции» типа getchar
и putchar
из библиотеки <stdio.h>
и
функция tolower
из библиотеки <ctype.h>
часто реализуют в виде макросов,
чтобы исключить накладные расходы от вызова функции на каждую отдельную
литеру. В разд. 8.5 мы покажем, как это делается. Независимо от того, как на
той или иной машине реализованы функции библиотеки <ctype.h>
, использующие их
программы могут ничего не знать о кодировке литер.
Напишите программу, осуществляющую перевод ввода с верхнего
регистра на нижний или с нижнего на верхний в зависимости от имени, по
которому она вызывается и текст которого находится в arg[0]
.
printf
)
Функция printf
переводит внутренние значения в текст.
int printf(char *format, arg1, arg2, ...)
В предыдущих главах мы использовали printf
неформально. Здесь мы покажем
наиболее типичные случаи применения этой функции; полное ее описание дано в
приложении B.
Функция printf
преобразует, форматирует и печатает свои аргументы в
стандартном выводе под управлением формата. Возвращает она количество
напечатанных литер.
Форматный стринг содержит два вида объектов: обычные литеры, которые впрямую
копируются в выходной поток, и спецификации преобразования, каждая из которых
вызывает преобразование и печать очередного аргумента printf
. Любая
спецификация преобразования начинается знаком %
и заканчивается
литерой–спецификатором. Между %
и литерой–спецификатором могут быть расположены (в
указанном ниже порядке) следующие элементы:
h
, если печатаемое целое должно рассматриваться как short
, или l
(латинская буква эль), если целое должно рассматриваться как long
.
Литеры–спецификаторы перечислены в табл. 7.1. Если за %
не помещена
литера–спецификатор, поведение функции printf
будет не определено.
printf
Литера | Тип аргумента; вид печати |
---|---|
d, i |
int ; десятичное целое. |
o |
int ; беззнаковое восьмеричное (octal) целое (без ведущего нуля). |
x, X |
int ; беззнаковое шестнадцатиричное целое (без ведущих 0x и 0X ),
для 10...15 используются abcdef или ABCDEF .
|
u |
int ; беззнаковое десятичное целое. |
c |
int ; одиночная литера. |
s |
char * ; печатает литеры, расположенные до знака \0 ,
или в количестве, заданном точностью.
|
f |
double ; [-]m.dddddd , где количество цифр d
задается точностью (по умолчанию равно 6).
|
e, E |
double ; [-]m.dddddde+-xx или [-]m.ddddddE+-xx ,
где количество цифр d задается точностью (по умолчанию равно 6).
|
g, G |
double ; использует %e или %E , если экспонента меньше,
чем -4, или больше или равна точности; в противном случае использует %f . «Хвостовые»
нули и «хвостовая» десятичная точка не печатаются.
|
p |
void * ; указатель (представление зависит от реализации). |
% |
аргумент не преобразуется; печатается знак % . |
Ширину и точность можно специфицировать при помощи *
; значение ширины (или
точности) в этом случае берется из следующего аргумента (который должен быть
типа int
). Например, чтобы напечатать не более max
литер из стринга s
.
годится следующая запись:
printf("%.*s", max, s);
Большая часть форматных преобразований была продемонстрирована в предыдущих
главах. Исключение составляет задание точности для стрингов. Далее приводится
перечень спецификаций и показывается их влияние на печать стринга
"hello, world"
, состоящего из 12 литер. Поле специально обрамлено двоеточиями, чтобы
была видна его протяженность.
:%s: |
:hello, world: |
:%10s: |
:hello, world: |
:%.10s: |
:hello, wor: |
:%-10s: |
:hello, world: |
:%.15s: |
:hello, world: |
:%-15s: |
:hello, world : |
:%15.10s: |
: hello, wor: |
:%-15.10s: |
:hello, wor : |
Предостережение: функция printf
использует свой первый аргумент, чтобы
определить, сколько еще ожидается аргументов и какого они будут типа. Вы не
получите правильного результата, если аргументов будет не хватать или они
будут принадлежать не тому типу. Вы должны также понимать разницу в следующих
двух обращениях:
printf(s); /* НЕВЕРНО, если в s есть % */ printf("%s", s); /* ВЕРНО всегда */
Функция sprintf
выполняет те же преобразования, что и printf
, но вывод
запоминает в стринге
int sprintf(char *string, char *format, arg1, arg2, ...)
Эта функция форматирует arg1
, arg2
и т.д. в соответствии с информацией,
заданной аргументом format
так, как мы ранее описывали, но результат помещает
не в стандартный вывод, а в string
. Заметим, что string
должен быть
достаточно большим, чтобы в нем поместился результат.
Напишите программу, которая будет печатать разумным способом любой ввод. Как минимум она должна уметь печатать неграфические литеры в восьмеричном или шестнадцатиричном виде (в форме, принятой на вашей машине), обрывая длинные текстовые строки.
Этот раздел содержит реализацию минимальной версии printf
. Приводится она для
того, чтобы показать, как надо писать функции со списками аргументов
переменной длины, причем такие, которые были бы переносимы. Поскольку нас
главным образом интересует обработка аргументов, функцию minprintf
напишем
таким образом, что она в основном будет работать со стрингом, задающим
формат, и аргументами; что же касается форматных преобразований, то они будут
осуществляться при помощи стандартного printf
.
Декларация стандартной функции printf
выглядит так:
int printf(char *fmt, ...)
Многоточие в декларации означает, что число и типы аргументов могут
изменяться. Знак многоточие может стоять только в конце списка аргументов.
Наша функция minprintf
декларируется как
void minprintf(char *fmt, ...)
поскольку она не будет выдавать число литер, как это делает printf
.
Вся сложность в том, каким образом minprintf
будет продвигаться вдоль списка
аргументов, — ведь у этого списка нет даже имени. Стандартный головной файл
<stdarg.h>
содержит набор макроопределений, которые определяют, как шагать по
списку аргументов. Наполнение этого головного файла может изменяться от
машины к машине, но представленный им интерфейс везде одинаков.
Тип va_list
служит для описания переменной, которая будет по очереди
ссылаться на каждый из аргументов; в minprintf
эта переменная имеет имя ap
(от «argument pointer» — указатель на аргумент). Макрос va_start
инициализирует переменную ap
, чтобы она указывала на первый безымянный
аргумент. К va_start
нужно обратиться до первого использования ap
. Среди
аргументов по крайней мере один должен быть именованным; от последнего
именованного аргумента этот макрос «отталкивается» при начальной установке.
Макрос va_arg
на каждом своем вызове выдает очередной аргумент, а ap
передвигает на следующий; по имени типа он определяет тип возвращаемого
значения и размер шага для выхода на следующий аргумент. Наконец, макрос
va_end
делает очистку всего, что необходимо. К va_end
следует обратиться
перед самым выходом из функции.
Перечисленные средства образуют основу нашей упрощенной версии printf
.
#include <stdarg.h> /* minprintf: минимальный printf с переменным числом арг. */ void minprintf(char *fmt, ...) { va_list ap; /* указывает на очереди. безымянный арг. */ char *p, *sval; int ival; double dval; va_start(ap, fmt); /* ap указ-ет на 1-й безымянный арг. */ for (p = fmt; *p; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); printf("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) putchar(*sval); break; default: putchar(*p); break; } } va_end(ap); /* очистка, когда все сделано */ }
Дополните minprintf
другими возможностями printf
.
scanf
)
Функция scanf
обеспечивающая ввод, является аналогом printf
; она выполняет
многие из упоминавшихся преобразований, но в противоположном направлении. Ее
декларация имеет следующий вид:
int scanf(char *format, ...)
Функция scanf
читает литеры из стандартною входного потока, интерпретирует их
согласно спецификациям стринга format
и рассылает результаты в свои остальные
аргументы. Аргумент — формат мы опишем позже; другие аргументы, каждый из
которых должен быть указателем, определяют, где будут запоминаться должным
образом преобразованные данные. Как и для printf
, в этом разделе дается
сводка наиболее полезных, но отнюдь не всех возможностей данной функции.
Функция scanf
прекращает работу, когда оказывается, что исчерпался формат или
вводимая величина не соответствует управляющей спецификации. В качестве
результата scanf
возвращает количество успешно введенных элементов данных. По
исчерпании файла она выдает EOF
. Существенно то, что значение EOF
не равно
нулю, поскольку нуль scanf
выдает, когда вводимая литера не соответствует
первой спецификации форматного стринга. Каждое очередное обращение к scanf
продолжает ввод с литеры, следующей сразу за последней обработанной.
Существует также функция sscanf
, которая читает из стринга (а не из
стандартного ввода).
int sscanf(char *string, char *format, arg1, arg2, ...)
Функция sscanf
просматривает string
согласно формату format
и рассылает
полученные значения в arg1
, arg2
и т.д. Последние должны быть указателями.
Формат обычно содержит спецификации, которые используются для управления преобразованиями ввода. В него могут входить следующие элементы:
%
), которые, как ожидается, совпадут с
очередными непробельными литерами входного потока.
%
и
завершается литерой–спецификатором типа преобразования. В промежутке
между этими двумя литерами в любой спецификации могут располагаться,
причем в том порядке, как они здесь указаны: знак *
(признак подавления
присваивания); число, определяющее ширину поля; буква h
, l
или L
,
указывающая на размер получаемого значения.
Спецификация преобразования управляет преобразованием следующего вводимого
поля. Обычно результат помещается в переменную, на которую указывает
соответствующий аргумент. Однако если в спецификации преобразования
присутствует *
, то поле ввода пропускается и никакое присваивание не
выполняется. Поле ввода определяется как стринг без пробельных литер; оно
простирается до следующей пробельной литеры или же ограничено шириной поля,
если она задана. Поскольку литера новая_строка относится к пробельным
литерам, это значит, что scanf
при чтении будет переходить с одной строки на
другую. (Пробельными литерами являются литеры пробела, табуляции,
новой_строки, возврата_каретки, вертикальной_табуляции и перевода_страницы.)
Литера–спецификатор указывает, каким образом следует интерпретировать очередное поле ввода. Соответствующий аргумент должен быть указателем, как того требует механизм передачи параметров по значению, принятый в Си. Литеры–спецификаторы приведены в табл. 7.2.
scanf
Литера | Вводимые данные; тип аргумента |
---|---|
d |
десятичное целое; int * . |
i |
целое; int * . Целое может быть восьмеричными (с ведущим 0) или шестнадцатиричным (с ведущими 0x или 0X ). |
o |
восьмеричное целое (с ведущим нулем или без него); int * . |
u |
беззнаковое десятичное целое; unsigned int * . |
x |
шестнадцатиричное целое (с ведущими 0x или 0X или без них); |
c |
литеры; char * . Следующие литеры ввода (по умолчанию одна) размещаются в указанном месте.
Обычный пропуск пробельных литер подавляется; чтобы прочесть очередную литеру, отличную от пробельной,
используйте %1s .
|
s |
стринг литер (без обрамляющих кавычек); char * , указывающий на массив литер,
достаточный для стринга и завершающей литеры '\0' , которая будет добавлена.
|
e, f, g |
число с плавающей точкой, возможно, со знаком; обязательно присутствие либо десятичной точки,
либо экспоненциальной части, а возможно, и обеих вместе; float * .
|
% |
сам знак % , никакое присваивание не выполняется. |
Перед литерами–спецификаторами d
, i
, o
, u
и x
может стоять буква h
,
указывающая на то, что соответствующий аргумент должен иметь тип short *
(а
не int *
), или l
(латинская эль), указывающая на тип long *
. Аналогично,
перед литерами–спецификаторами e
, f
и g
может стоять l
, указывающая, что
тип аргумента — double *
(а не float *
).
Чтобы построить первый пример, обратимся к программе калькулятора из гл. 4, в
которой организуем ввод с помощью функции scanf
:
#include <stdio.h> /* программа-калькулятор */ main() { double sum, v; sum = 0; while (scanf("%lf", &v) == 1) printf("\t%.2f\n", sum += v); return 0; }
Предположим, что нам нужно прочитать строки ввода, содержащие данные вида
25 дек 1988
Обращение к scanf
выглядит следующим образом:
int day, year; /* день, год */ char monthname[20]; /* название месяца */ scanf("%d %s %d", &day, monthname, &year);
Знак &
перед monthday
не нужен, так как имя массива есть указатель.
В стринге формата могут присутствовать литеры, не участвующие ни в одной из
спецификаций; это значит, что эти литеры должны появиться на вводе. Так, мы
могли бы читать даты вида mm/dd/yy при помощи следующего обращения к scanf
:
int day, month, year; /* день, месяц, год */ scanf("%d/%d/%d", &day, &month, &year);
В своем формате функция scanf
игнорирует пробелы и табуляции. Кроме того,
при поиске следующей порции ввода она пропускает во входном потоке все
пробельные литеры (пробелы, табуляции, новые_строки и т.д.). Воспринимать
входной поток, не имеющий фиксированного формата, часто оказывается удобнее,
если вводить всю строку целиком и для каждого отдельного случая подбирать
подходящий вариант sscanf
. Предположим, например, что нам нужно читать строки
с датами, записанными в любой из приведенных выше форм. Тогда мы могли бы
написать:
while (getline(line, sizeof(line)) > 0) { if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3) printf("верно: %s\n", line); /* типа 25 дек 1988 */ else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3) printf("верно: %s\n", line); /* типа mm/dd/yy */ else printf("неверно: %s\n", line); /* неверн. форма даты */ }
Обращения к scanf
могут перемежаться с вызовами других функций ввода. Любая
функция ввода, вызванная после scanf
, продолжит чтение с первой еще
непрочитанной литеры.
В завершение еще раз напомним, что аргументы scanf
и sscanf
должны быть
указателями.
Одна из самых распространенных ошибок состоит в том, что вместо того, чтобы написать
scanf("%d", &n);
пишут
scanf("%d", n);
Компилятор о подобной ошибке ничего не сообщает.
Напишите свою версию scanf
по аналогии с minprintf
из
предыдущего раздела.
Перепишите основанную на постфиксной записи программу
калькулятора из гл. 4 таким образом, чтобы для ввода и преобразования чисел
она использовала scanf
и/или sscanf
.
Во всех предыдущих примерах мы имели дело со стандартным вводом и стандартным выводом, которые для программы автоматически предопределены операционной системой конкретной машины.
Следующий шаг — научиться писать программы, которые имели бы доступ к файлам,
заранее не подсоединенным к программам. Одна из программ, в которой возникает
такая необходимость — это программа cat
, объединяющая несколько именованных
файлов и направляющая результат в стандартный вывод. Функция cat
часто
применяется для выдачи файлов на экран, а также как универсальный «коллектор»
файловой информации тех программ, которые не имеют возможности обратиться к
файлу по имени. Например, команда
cat x.c y.c
направит в стандартный вывод содержимое файлов x.c
и y.c
(и ничего более).
Возникает вопрос: что надо сделать, чтобы именованные файлы можно было читать; иначе говоря, как связать внешние имена, придуманные пользователем, с инструкциями чтения данных?
На этот счет имеются простые правила. Для того чтобы можно было читать из
файла или писать в файл, он должен быть предварительно открыт при помощи
библиотечной функции fopen
. Функция fopen
получает внешнее имя типа x.c
или
y.c
, после чего осуществляет некоторые организационные действия и
«переговоры» с операционной системой (технические детали которых здесь не
рассматриваются) и возвращает указатель, используемый в дальнейшем для
доступа к файлу.
Этот указатель, называемый указателем файла, ссылается на структуру,
содержащую информацию о файле (адрес буфера, положение текущей литеры в
буфере, открыт файл на чтение или на запись, были ли ошибки при работе с
файлом и встретился ли конец файла). Пользователю не нужно знать подробности,
поскольку определения, полученные из <stdio.h>
, включают описание такой
структуры, называемой FILE
. Единственное, что требуется для определения
указателя файла, — это задать декларации такого, например, вида:
FILE *fp; FILE *fopen(char *name, char *mode);
Из этой записи следует, что fp
есть указатель на FILE
, а fopen
возвращает
указатель на FILE
. Заметим, что FILE
есть имя типа, наподобие int
, а не тег
структуры. Оно определено при помощи typedef
. (Детали того, как можно
реализовать fopen
в системе UNIX, приводятся в разд. 8.5.)
Обращение к fopen
в программе может выглядеть следующим образом:
fp = fopen(name, mode);
Первый аргумент — стринг, содержащий имя файла. Второй аргумент несет
информацию о режиме. Это тоже стринг: в нем указывается, каким образом
пользователь намерен использовать файл. Возможны следующие режимы: чтение
(read
— "r"
), запись (write
—
"w"
) и добавление (append
— "a"
), т.е. запись
информации в конец уже существующего файла. В некоторых системах различаются
текстовые и бинарные файлы; в случае последних в стринг режима необходимо
добавить букву "b"
(binary — бинарный).
Тот факт, что некий файл, которого раньше не было, открывается на запись или
добавление, означает, что он создается (если такая процедура физически
возможна). Открытие уже существующего файла на запись приводит к выбрасыванию
его старого содержимого, в то время как при открытии файла на добавление его
старое содержимое сохраняется. Попытка читать несуществующий файл является
ошибкой. Могут иметь место и другие ошибки; например, ошибкой считается
попытка чтения файла, который по статусу запрещено читать. При наличии любой
ошибки fopen
возвращает NULL
. (Возможна более точная идентификация ошибки;
детальная информация по этому поводу приводится в конце
разд. 1 приложения B.)
Следующее, что нам необходимо знать, — это как читать из файла или писать в
файл, коль скоро он открыт. Существует несколько способов сделать это, из
которых самый простой состоит в том, чтобы воспользоваться функциями getc
и
putc
. Функция getc
возвращает следующую литеру из файла; ей необходимо
сообщить указатель файла, чтобы она знала откуда брать литеру.
int getc(FILE *fp)
Функция getc
возвращает следующую литеру из потока, на который ссылаются при
помощи *fp
; в случае исчерпания файла или ошибки она возвращает EOF
.
int putc(int c, FILE *fp)
Функция putc
пишет литеру c
в файл fp
и возвращает записанную литеру или EOF
,
в случае ошибки. Аналогично getchar
и putchar
, программы getc
и putc
могут
быть реализованы в виде макросов, а не функций.
При запуске Си-программы операционная система всегда открывает три файла и
обеспечивает три файловые ссылки на них. Этими файлами являются: стандартный
ввод, стандартный вывод и стандартный файл ошибок; соответствующие им
указатели называются stdin
, stdout
и stderr
; они описаны в <stdio.h>
. Обычно
stdin
соотнесен с клавиатурой, а stdout
и stderr
— с экраном. Однако stdin
и
stdout
можно связать с файлами или, используя механизм «трубопровода»,
соединить напрямую с другими программами, как это описывалось в разд. 7.1.
При помощи getc
, putc
, stdin
и stdout
функции getchar
и putchar
теперь можно
определить следующим образом:
#define getchar() getc(stdin) #define putchar (c) putc((c), stdout)
Форматный ввод-вывод файлов можно построить на функциях fscanf
и fprintf
. Они
идентичны scanf
и printf
с той лишь разницей, что первым их аргументом
является указатель, ссылающийся на файл, для которого осуществляется
ввод-вывод, формат же указывается вторым аргументом.
int fscanf(FILE *fp, char *format, ...) int fprintf(FILE *fp, char *format, ...)
Вот теперь мы располагаем теми сведениями, которые достаточны для написания
программы cat
, предназначенной для конкатенации (последовательного
соединения) файлов. Предлагаемая версия функции cat
, как оказалось, удобна
для многих программ. Если в командной строке присутствуют аргументы, они
рассматриваются как имена последовательно обрабатываемых файлов. Если
аргументов нет, то обработке подвергается стандартный ввод.
#include <stdio.h> /* cat: конкатенация файлов, версия 1 */ main(int argc, char *argv[]) { FILE *fp; void filecopy(FILE *, FILE *); if (argc == 1) /* нет арг-тов; копир-ся станд. ввод */ filecopy(stdin, stdout); else while (--argc > 0) if ((fp = fopen(*++argv, "r")) == NULL) { printf("cat: не могу открыть файл %s\n", *argv); return 1; } else { filecopy(fp, stdout); fclose(fp); } return 0; } /* filecopy: копирует файл ifp в файл ofp */ void filecopy(FILE *ifp, FILE *ofp) { int c; while ((c = getc(ifp)) != EOF) putc(c, ofp); }
Файловые указатели stdin
и stdout
представляют собой объекты типа FILE *
. Это
константы, а не переменные, следовательно, им нельзя ничего присваивать.
Функция
int fclose(FILE *fp)
— обратная по отношению к fopen
; она разрывает связь между файловым
указателем и внешним именем (которая раньше была установлена при помощи
fopen
), освобождая тем самым этот указатель для других файлов. Так как в
большинстве операционных систем количество одновременно открытых одной
программой файлов ограничено, то файловые указатели, если они больше не
нужны, лучше освобождать, как это и делается в программе cat
. Есть еще одна
причина применить fclose
к файлу вывода, — это необходимость «опорожнить»
буфер, в котором putc
накопила, предназначенные для вывода данные. При
нормальном завершении работы программы для каждого открытого файла fclose
вызывается автоматически. (Вы можете закрыть stdin
и stdout
, если они вам не
нужны. Воспользовавшись библиотечной функцией freopen
их можно создать
заново.)
stderr
и exit
)
Обработка ошибок в cat
нельзя признать идеальной. Беда в том, что если файл
по какой-либо причине недоступен, сообщение об этом мы получим по окончании
конкатенируемого вывода. Это нас устроило бы, если бы вывод отправлялся
только на экран, а не в файл или другой программе, напрямую по
«трубопроводу».
Чтобы лучше справиться с этой проблемой, программе помимо стандартного вывода
stdout
придается еще один выходной поток, называемый stderr
. Вывод в stderr
обычно отправляется на экран, даже если вывод stdout
перенаправлен в другое
место.
Перепишем cat
так, чтобы сообщения об ошибках отправлялись в stderr
.
#include <stdio.h> /* cat: конкатенация файлов, версия 2 */ main(int argc, char *argv[]) { FILE *fp; void filecopy(FILE *, FILE *); char *prog = argv[0]; /* имя программы */ if (argc == 1) /* нет арг-тов: копир-ся станд. ввод */ filecopy(stdin, stdout); else while (--argc > 0) if ((fp = fopen(*++argv, "r")) == NULL) { fprintf(stderr, "%s: не могу откр. файл %s\n", prog, *argv); exit(1); } else { filecopy(fp, stdout); fclose(fp); } if (ferror(stdout)) { fprintf(stderr, "%s: ошибка записи в stdout\n", prog); exit(2); } exit(0); }
Программа сигнализирует об ошибках двумя способами. Первый — сообщение об
ошибке при помощи fprintf
посылается в stderr
с тем, чтобы оно попало на
экран, а не оказалось в «трубопроводе» или в другом файле вывода. Имя
программы, хранящееся в argv[0]
, мы включили в сообщение, чтобы в случаях,
когда данная программа работает совместно с другими, был ясен источник
ошибки.
Второй способ указать на ошибку — обратиться к библиотечной функции exit
,
завершающей работу программы. Аргумент функции exit
доступен некоторому
процессу, вызвавшему данный процесс. А следовательно, успешное или ошибочное
завершение программы можно проконтролировать с помощью некоей программы,
которая рассматривает эту программу в качестве подчиненного процесса. По
общей договоренности возврат нуля сигнализирует о том, что работа прошла
нормально, в то время как ненулевые значения обычно говорят об ошибках. Чтобы
опустошить буфера, накопившие информацию для всех открытых файлов вывода,
функция exit
вызывает fclose
.
Инструкция главной программы return выр
эквивалентна обращению к функции
exit(выр)
. Последняя запись (при помощи exit
) имеет то преимущество, что она
пригодна для выхода и из других функций, и, кроме того, ее легко обнаружить
при помощи программы контекстного поиска, похожей на ту, которую мы
рассматривали в гл. 5.
Функция ferror
выдает ненулевое значение, если в файле fp
была обнаружена
ошибка.
int ferror(FILE *fp);
Хотя при выводе редко возникают ошибки, все же они встречаются (например, оказался переполненным диск); поэтому в программах широкого пользования они должны тщательно контролироваться.
Функция feof(FILE *)
аналогична функции ferror
; она возвращает ненулевое
значение, если встретился конец указанного в аргументе файла.
int feof(FILE *fp)
В наших небольших иллюстративных программах мы не заботились о выдаче статуса выхода, т.е. выдаче некоторого числа, характеризующего состояние программы в момент завершения: работа закончилась нормально или прервана из-за ошибки? Если работа прервана в результате ошибки, то какой? Любая серьезная программа должна выдавать статус выхода.
В стандартной библиотеке имеется программа ввода fgets
, аналогичная программе
getline
, которой мы пользовались в предыдущих главах.
char *fgets(char *line, int maxline, FILE *fp)
Функция fgets
читает следующую строку ввода (включая и литеру новая_строка)
из файла fp
в массив литер line
, причем она может прочитать не более
maxline-1
литер. Переписанная строка дополняется литерой '\0'
. Обычно fgets
возвращает line
, а по исчерпании файла или в случае ошибки — NULL
. (Наша
getline
возвращала длину строки, которой мы потом пользовались, и нуль по
концу файла.)
Функция вывода fputs
пишет стринг (который может и не заканчиваться литерой
новая_строка) в файл.
int fputs(char *line, FILE *fp)
Эта функция возвращает EOF
, если возникла ошибка, и нуль в противном случае.
Библиотечные функции gets
и puts
подобны функциям fgets
и fputs
. Отличаются
они тем, что оперируют только стандартными файлами stdin
и stdout
, и, кроме
того, gets
выбрасывает последнюю литеру '\n'
, а puts
ее добавляет.
Чтобы показать, что ничего особенного в функциях типа fgets
и fputs
нет, мы
приводим их здесь в том виде, в каком они существуют в стандартной библиотеке
на нашей системе.
/*fgets: получает не более n литер из iop */ char *fgets(char *s, int n, 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) ? NULL : s; } /* fputs: посылает стринг s в файл iop */ int fputs(char *s, FILE *iop) { int c; while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0; }
Напишите программу, сравнивающую два файла и печатающую первую строку, в которой они различаются.
Модифицируйте программу поиска по образцу из гл. 5 таким образом, чтобы она брала текст из множества именованных файлов, а если имен файлов в аргументах нет, то — из стандартного ввода. Следует ли печатать имя файла, в котором найдена подходящая строка?
Напишите программу, печатающую несколько файлов. Каждый файл должен начинаться с новой страницы, предваряться заголовком и иметь свою нумерацию страниц.
В стандартной библиотеке представлен широкий спектр различных функций. Настоящий раздел содержит краткий обзор наиболее полезных из них. Более подробно эти и другие функции описаны в приложении B.
Мы уже упоминали функции strlen
, strcpy
, strcat
и strcmp
, описание которых
даны в <string.h>
. Далее, до конца подраздела, предполагается, что s
и t
имеют тип char *
, c
и n
— тип int
.
strcat(s, t) |
конкатенирует t в конец s |
strncat(s, t, n) |
конкатенирует n литер t в конец s |
strcmp(s, t) |
возвращает отрицательное число, нуль или положительное число для s < t ,
s == t или s > t соответственно
|
strncmp(s, t, n) |
делает то же, что и strcmp , но количество сравниваемых литер
не может превышать n
|
strcpy(s, t) |
копирует t в s |
strncpy(s, t, n) |
копирует не более n литер t в s |
strlen(s) |
возвращает длину s |
strchr(s, c) |
возвращает указатель на первое появление литеры c в s или,
если c нет в s , NULL
|
strrchr(s, c) |
возвращает указатель на последнее появление литеры c в s или,
если c нет в s , NULL
|
Несколько функций из библиотеки <ctype.h>
выполняют проверки и преобразование
литер. Далее, до конца подраздела, переменная c
— это переменная типа int
,
которая может быть представлена значением unsigned char
или EOF
. Функции
возвращают значения типа int
.
isalpha(c) |
не нуль, если c — буква; 0 в противном случае |
isupper(c) |
не нуль, если c — буква верхнего регистра; 0 в противном случае |
islower(c) |
не нуль, если c — буква нижнего регистра; 0 в противном случае |
isdigit(c) |
не нуль, если c — цифра; 0 в противном случае |
isalnum(c) |
не нуль, если или isalpha(c) , или isdigit(c) истинны;
0 в противном случае
|
isspace(c) |
не нуль, если c — литера пробела, табуляции, новой_строки,
возврата_каретки, перевода_страницы, вертикальной_табуляции
|
toupper(c) |
возвращает c , приведенную к верхнему регистру |
tolower(c) |
возвращает c , приведенную к нижнему регистру |
В стандартной библиотеке содержится более ограниченная версия функции ugetch
по сравнению с той, которую мы написали в гл. 4. Называется она ungetc
. Эта
функция, имеющая прототип
int ungetc (int c, FILE *fp);отправляет литеру
c
назад в файл fp
и возвращает c
или EOF
, в случае ошибки.
Для каждого файла гарантирован возврат не более одной литеры. Функцию ungetc
можно использовать совместно с любой из функций ввода типа scanf
, getc
,
getchar
и т.д.
Функция system(char *s)
выполняет команду системы, содержащуюся в стринге s
,
и затем возвращается к выполнению текущей программы. Содержимое s
, строго
говоря, зависит от конкретной операционной системы. Рассмотрим простой
пример: в системе UNIX инструкция
system("date");
вызовет программу date
, которая направит дату и время в стандартный вывод.
Функция возвращает зависящий от системы статус выполненной команды. В системе
UNIX возвращаемый статус — это значение, переданное функцией exit
.
Функции malloc
и calloc
получают динамически запрашиваемые ими области
памяти. Функция malloc
с прототипом
void *malloc(size_t n);
возвращает указатель на n
байт неинициализированной памяти или NULL
, если
запрос удовлетворить нельзя. Функция calloc
с прототипом
void *calloc(size_t n, size_t size);
возвращает указатель на область, достаточную для хранения массива из n
объектов указанного размера (size
), или NULL
, если запрос не удается
удовлетворить. Выделенная память обнуляется.
Указатель, возвращаемый функциями malloc
и calloc
, будет выдан с учетом
выравнивания, выполненного согласно указанному типу объекта. Тем не менее к
нему должна быть применена операция приведения к соответствующему типу, как
это сделано в следующем фрагменте программы:
int *ip; ip = (int *) calloc(n, siseof(int));
Функция free(p)
освобождает область памяти, на которую указывает p
, —
указатель, первоначально полученный с помощью malloc
или calloc
. Никаких
ограничений на порядок, в котором будет освобождаться память, нет, но ужасной
ошибкой считается освобождение тех областей, которые не были получены при
помощи calloc
или malloc
.
Нельзя также использовать те области памяти, которые уже освобождены. Следующий пример демонстрирует типичную ошибку в цикле, освобождающем элементы списка.
for (p = head; p != NULL; p = p->next) /* НЕВЕРНО */ free(p);
Правильным будет, если вы до освобождения сохраните то, что вам потребуется, как в следующем цикле:
for (p = head; p != NULL; p = q) { q = p->next; free(p); }
В разд. 8.7 мы рассмотрим реализацию программы управления памятью типа
malloc
, позволяющую освобождать выделенные блоки памяти в любой
последовательности.
В <math.h>
описано более двадцати математических функций. Здесь же приведены
наиболее употребительные. Каждая из них имеет один или два аргумента типа
double
и возвращает результат также типа double
.
sin(x) |
синус x , x в радианах |
cos(x) |
косинус x , x в радианах |
atan2(y, x) |
арктангенс y/x , y/x в радианах |
exp(x) |
экспоненциальная функция ex |
log(x) |
натуральный (по основанию e) логарифм x (x > 0 ) |
log10(x) |
обычный (по основанию 10) логарифм x (x > 0 ) |
pow(x, y) |
xy |
sqrt(x) |
корень квадратный x (x >= 0) |
fabs(x) |
абсолютное значение x |
Функция rand()
вычисляет последовательность псевдослучайных целых в диапазоне
от нуля до значения, заданного именованной константой RAMD_MAX
, которая
определена в <stdlib.h>
. Привести случайные числа к значениям с плавающей
точкой, большим или равным 0
и меньшим 1
, можно по формуле
#define frand() ((double) rand() / (RAND_MAX + 1.0))
(Если в вашей библиотеке уже есть функция для получения случайных чисел с плавающей точкой, вполне возможно, что ее статистические характеристики лучше указанной.)
Функция srand(unsigned)
осуществляет «начальную затравку» для rand
.
Реализации rand
и srand
, предлагаемые стандартом и, следовательно,
переносимые на различные машины, рассмотрены в разд. 2.7.
Реализуя функции типа isupper
, можно либо экономить память,
либо время. Напишите оба варианта функции.