Глава 10. Ввод-вывод в языке С

Большинство популярных языков программирования высокого уровня имеет довольно ограниченные средства ввода-вывода. В результате программистам часто приходится разрабатывать изощренные алгоритмы, чтобы добиться необходимых результатов при вводе или выводе данных. К счастью, это не относится к языку С, который снабжен мощной подсистемой ввода-вывода, представленной обширной библиотекой соответствующих функций, хотя исторически эта подсистема не была частью языка. Как бы то ни было, те программисты, которые привыкли в языке Pascalиспользовать только два оператора — readln и writeln, — будут поражены обилием функций ввода-вывода в языке С. В настоящей главе мы рассмотрим более 20-ти различных способов ввода и вывода информации, существующих в С.

Стандартные библиотечные функции ввода-вывода языка С позволяют считывать и записывать данные, связанные с файлами и различными устройствами. В то же время в самом языке С не предусмотрены какие-то предопределенные структуры файлов. Любые данные рассматриваются как цепочка байтов. В целом же все функции ввода-вывода можно разделить на три основные категории: потоковые, консольные и низкоуровневые.

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

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

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

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

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

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

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

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

Потоковые функции

Чтобы получить доступ к потоковым функциям, приложение должно подключить файл STDIO.H. Этот файл содержит объявления констант, типов данных и структур, используемых этими функциями, а также прототипы самих функций и описания служебных макросов.

Многие константы, содержащиеся в файле STDIO.H, находят достаточно широкое применение в приложениях. Например, константа eof возвращается функциями ввода при достижении конца файла, а константа null служит нулевым ("пустым") указателем. Другие примеры: тип данных file представляет собой структуру, содержащую информацию о потоке, а константа BUFSIZ задает стандартный размер в байтах буфера, используемого потоком.

Открытие файлов и потоков

Чтобы открыть файл для ввода или вывода данных, воспользуйтесь одной из следующих функций: fopen(),fdopen() или freopen() Файл открывается для чтения, записи или для выполнения обеих операций. Кроме того, файл может быть открыт в текстовом или в двоичном режиме.
Все три функции возвращают указатель файла, который используется для обращения к потоку. Например:

pfinfile =  fopen("input.dat", "r");

При запуске приложения автоматически открываются сразу пять стандартных потоков: ввода (stdin), вывода (stdout), ошибок (stderr), печати (stdprn) и внешнего устройства (stdaux). По умолчанию стандартные потоки ввода, вывода и ошибок связаны с консолью. Например, данные, записываемые в стандартный вывод, выводятся на терминал. Любое сообщение об ошибке, сгенерированное библиотечной функцией, также выводится на терминал. Потоки stdprnи stdaux направляют данные соответственно в порт принтера и порт внешнего устройства.

Во всех функциях, где в качестве аргумента требуется указатель на поток, можно использовать любой из перечисленных выше указателей. Некоторые функции, такие как getchar() и putchar(), работают только с потоками stdinи stdout. Поскольку указатели stdin, stdout, stderr, stdprn и stdaux являются константами, а не переменными, не пытайтесь переадресовать их на другие потоки.

Переадресация ввода-вывода

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

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

Например, в командной строке MS-DOS переадресация выполняется очень просто. Вы используете оператор < для переадресации ввода и > для переадресации вывода. Допустим, исполняемый файл вашего приложения называется REDIRECT. Тогда следующая команда сначала запустит программу REDIRECT, а затем инициирует ввод данных из файла SAMPLE.DAT вместо стандартного ввода с клавиатуры:

redirect< sample.dat

Следующая строка одновременно осуществит переадресацию ввода из файла SAMPLE.DATи переадресацию вывода в файл SAMPLE.BAK:

redirect < sample.dat > sample.bak

И наконец, последняя строка переадресует только вывод данных:

redirect > sample.bak

Стандартный поток ошибок stderr не может быть переадресован.

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

Для того чтобы связать каналом стандартный ввод одной программы и стандартный вывод другой, следует поставить символ вертикальной черты (|):

process1 | process2

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

Изменение буфера потока

Потоки stdin, stdout и stdprn буферизуются по умолчанию: данные из них извлекаются, как только буфер переполняется. Потоки stderr и stdaux не буферизуются, если только они не используются в функциях семейств printf() и scanf(): в этих случаях им назначается временный буфер. Буферизацию потоков stderr и stdaux также можно осуществлять с помощью функций setbuf() и setvbuf().

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

Размер создаваемого буфера может быть произвольным. При использовании функции setbuf() размер буфера неявно задается константой BUFSIZ, объявленной в файле STDIO.H. Синтаксис функции выглядит следующим образом:

void setbuf(FILE *имя потока, char *имя_буфера) ;

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

/* 
*   setbuf.с 
Эта программа на языке С демонстрирует, как назначить
*  буфер небуферизованному потоку stderr.
*/

#include <stdio.h>

char cmyoutputbuffer[BUFSIZ];

void main(void)

{

/* Назначение буфера небуферизованному потоку stderr*/
setbuf(stderr, cmyoutputbuffer); /* попробуйте превратить эту строку в комментарий*/

/* Вставка данных в выходной поток */

fputs("Строка данных\n", stderr);

fputs("вставляется в выходной буфер.\n",stderr);

/* "Выталкивание" буфера потока на экран */
fflush(stderr); }

Запустите программу в режиме отладки, и вы увидите, что выводимые строки появятся на экране только после выполнения последней строки, когда содержимое буфера принудительно "выталкивается". Если заключить строку с вызовом функции setbuf() в символы комментария и снова запустить программу в отладчике, то данные, записываемые в поток stderr, не будут буферизоваться, и тогда результат выполнения каждой функции fputs() будет появляться на экране немедленно.

В приводимой ниже программе используется функция setvbuf(),синтаксис которой таков:

int setvbuf(FILE *имя_потока,   char *имя_буфера,   int  тип_буфера, size_t размер_буфера) ;

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

/*
*  setvbuf.с
*  Эта программа на языке С демонстрирует использование функции Setvbuf() .
*/

#include <stdio.h>

#define MYBUFSIZE 512

void main(void) {

char ichar, cmybuffer[MYBUFSIZE]; FILE *pfinfile, <<pfoutfile;

pfinfile = fopen("sample.in","r");

pfoutfile = fopen("sample.out", "w");

if(setvbuf(pfinfile, cmybuffer, _IOFBF, MYBUFSIZE) != 0)
printf("Ошибка выделения буфера для потока pfinfile.\n");

else 

printf("Буфер потока pfinfile создан.\n");   

if(setvbuf(pfoutfile,   NULL,   _IONBF,   0)    !=  0)

printf("Ошибка выделения буфера для потока pfoutfile.\n") ;

else

printf("Поток pfoutfileне имеет буфера.\n");

while (fscanf (pfinfile, "%c", &ichar,) != EOF)

fprintf(pfoutfile, "%c",ichar);    

fclose(pfinfile);

f close (pfoutfile);        

}               

Программа создает именованный буфер для потока pfinfile и запрещает буферизацию потока pfoutfile. В первом случае указан тип буфера _IOFBF(полная буферизация). Если при этом не задать имя буфера, он будет создан автоматически в динамической памяти и так же автоматически удален по завершении работы программы. Во втором случае указан тип _ionbf(нет буфера). В такой ситуации имя буфера и его размер, даже если они заданы, игнорируются.

Закрытие файлов и потоков

Функция fclose () закрывает указанный файл, тогда как функция _fcloseall() закрывает сразу все открытые потоки, кроме стандартных: stdin, stdout, stderr, stdprn и stdaux. Если в программе явно не закрыть поток, он все равно будет автоматически закрыт по завершении программы. Поскольку одновременно можно открыть только ограниченное число потоков, следует своевременно закрывать те из них, которые больше не нужны.

Низкоуровневый ввод-вывод

При низкоуровневом вводе-выводе не происходит ни буферизации, ни форматирования данных. Файлы, открытые на низком уровне, представляются в программе дескрипторами — целочисленными значениями, которые операционная система использует в качестве ссылок на файлы. Для открытия файла предназначена функция _ореn (). Чтобы открыть файл в режиме совместного доступа, следует воспользоваться функцией _sopen().

В табл. 10.1 перечислены наиболее часто употребляемые в приложениях функции ввода-вывода низкого уровня. Все они объявлены в файле IO.Н.

Данная подсистема ввода-вывода, ориентированная на работу с дисковыми файлами, изначально была создана для операционной системы UNIX. Поскольку комитет ANSI С отказался стандартизировать ее, мы не рекомендуем применять эти функции. В новых проектах предпочтительнее работать со стандартными потоковыми функциями, которые детально рассматриваются в этой главе.

Таблица 10.1. Наиболее часто используемые низкоуровненвые функции ввода-вывода
Функция Описание
_close() 

Закрывает файл на диске

_lseek() 

Перемещает указатель файла на заданный байт

_ореn ()  Открывает файл на диске
_read()   

Считывает блок данных из файла

_unlike()    Удаляет файл из каталога
_write() Записывает блок данных в файл

Ввод-вывод символов

В стандарте ANSI С описан ряд функций, предназначенных для ввода-вывода символов и входящих в стандартный комплект поставки всех компиляторов языка С. Таков общий принцип С: реализовывать ввод-вывод посредством внешних библиотечных функций, а не ключевых слов, являющихся частью языка.

Функции getc( ), putc( ), fgetc( ) и fputc( )

Функция getc()  читает один символ из указанного файлового потока:

int ic;
ic = getc(stdin);

Вас может удивить, почему переменная ic не объявлена как char. Дело в том, что в прототипе функции getc() указано, что она возвращает значения типа int. Это связано с необходимостью обрабатывать также признак конца файла, который не может быть сохранен в обычной переменной типа char.

Функция getc() преобразовывает читаемый символ из типа char в тип unsigned char и только затем — в int. Такой способ обработки данных гарантирует, что символы с ASCII-кодами больше 127 не будут представлены отрицательными числами. Это позволяет зарезервировать отрицательные значения для нестандартных ситуаций — признаков ошибок или конца файла. Так, например, признаком конца файла традиционно служит значение -1. Правда, стандарт ANSI С гарантирует лишь то, что константа EOF содержит некое отрицательное значение.

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

Функция getc() читает данные из буфера. Это означает, что управление не будет обратно передано программе до тех пор, пока в указанном потоке не встретится символ новой строки. Функция возвращает только самый первый из обнаруженных ею в буфере символов, другие остаются невостребованными. Таким образом, функцию getc() нельзя использовать для последовательного ввода символов с клавиатур ры, не нажимая при этом каждый раз клавишу [Enter].

Функция putc(} записывает символ в файловый поток, представленный указателем файла. Например, чтобы отобразить тот символ, который был введен в предыдущем примере, задайте такую строку:

putc(ic, stdout);

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

if(putc (ic,stdout) == EOF) 
printf("Обнаружена ошибка записи в stdout");

И последнее замечание: функции getc() и putc() реализованы и как макросы, и как функции. Макроверсии имеют более высокий приоритет и выполняются в первую очередь. Чтобы изменить такой порядок, следует с помощью директивы препроцессора #undef отменить определение макроса:

#undef getc

Существуют "чистые" функции fgetc () и fputc (), которые выполняют аналогичные действия, но не имеют макросов-"двойников".

Функции getchar( ), putchar( ), _fgetchar( ) n_fputchar()

Функции getchar() и putchar() являются модификациями рассмотренных выше функций getc() и putc(), работающими только cо стандартными потоками ввода (stdin) и вывода (stdout). Рассмотренные в предыдущем параграфе примеры могут быть переписаны с использованием функций getchar() и putchar() следующим образом:

int  ic; 
ic  =  getchar();

И

putchar(ic) ;                                .

Эти функции тоже реализованы в виде макросов и в виде функций, а аналогичные им функции, fgetchar() и fputchar(),не имеют макродубликатов.

Функции_getch( ), _getche( ) и_putch( )

Функции _getch(), _getche() и _putch(), объявленные в файле CONIO.H, не соответствуют стандарту ANSI С, поскольку взаимодействуют напрямую с консолью или портом ввода-вывода. Они не работают с буферами, т.е., например, все символы, вводимые с клавиатуры, немедленно возвращаются функцией _getch() в программу. Вывод функции _putch() всегда направляется на консоль.

Функция _getoh() воспринимает нажатия тех клавиш, которые игнорируются функцией getchar(}, например [PageUp], [PageDown], [Home] и [End], и при этом не требует последующего нажатия клавиши [Enter]. Она работает в режиме без эха, а ее аналог _getche() — с эхом. В случае функциональных и управляющих клавиш эти функции следует вызывать дважды: сначала возвращается 0, а затем — непосредственно код клавиши.

Ввод-вывод строк

Для большинства приложений более важным является ввод-вывод целых строк, а не отдельных символов. Ниже мы познакомимся с основными потоковыми функциями, предназначенными для этих целей. Все они объявлены в файле STDIO.H

Функции gets( ), puts( ), fgets( ) иfputs( )

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

Функция fgets () принимает три аргумента: адрес массива, в котором будет сохранена строка, максимальное число символов в строке и указатель потока, из которого выполняется чтение данных. Функция будет считывать символы до тех пор, пока их количество не станет на единицу меньшим, чем указанный предел. Последним всегда записывается символ \0.Если функция fgets() встретит символ новой строки (\n), дальнейшее чтение будет прекращено, а сам символ помещен в массив. Если обнаруживается конец потока, чтение также прекращается.

Предположим, у нас есть файл базы данных SALESMAN.DATсо следующими строками:

Иванов  32767 0.1530   
Сергеев 35000 0.1223
Кузьмин 40000 0.1540

Допустим также, что максимальная длина строки с учетом символа \n не должна превышать 40 символов. Приводимая ниже программа считывает записи из файла и направляет их на стандартное устройство вывода:

/*
*  fgets.с
*   Эта программа на языке С демонстрирует процесс считывания строк с
*   помощью функции fgets() и вывода их с помощью функции fputs()
*/

#include <stdio.h>

#define INULL_CHAR 1

#define IMAX_REC_SIZE 40

void main ()

{

FILE  *pfinfile;

char crecord[IMAX_REC_SIZE +  INULL_CHAR];

pfinfile =  fopen("salesman.dat",   "r") ;

while(fgets(crecord,   IMAX_REC_SIZE  +  INULL_CHAR,   pfinfile)    != NULL)
fputs(crecord,  stdout);

fclose(pfinfile);}

Поскольку максимальная длина строки равна 40 символам, следует создать для нее массив, содержащий 41 элемент. Дополнительный элемент необходим для хранения признака конца строки \0. Программа не генерирует самостоятельно никаких разрывов строк при выводе строк на терминал. Тем не менее, структура строк сохраняется такой же, что и в исходном файле, поскольку символы \n читаются и сохраняются в массиве функцией fgets(). Функция fputs() выводит содержимое массива crecord в поток stdout без каких-либо изменений.

Функция gets() отличается от функции fgets () тем, что читает данные только из потока stdin, причем до тех пор, пока не будет нажата клавиша [Enter], не проверяя, достаточно ли в указанном массиве места для размещения всех введенных символов. Символ новой строки \n, генерируемый клавишей [Enter], заменяется символом \0.

Функция puts() выводит данные в стандартный поток вывода stdout и добавляет в конец выводимой строки символ \n, чего не делает функция fputs().

Пример совместной работы всех перечисленных функций был дан в главе "Массивы".

Ввод-вывод целых чисел

В некоторых приложениях бывает необходимо считывать и записывать потоки (в том числе буферизованные) целых чисел. Для этих целей в языке С существуют две функции: getw() и putw().

Функции getw( ) и putw( )

Дополняющие друг друга функции getw()и putw() очень похожи по своему действию на функции getc( ) и putc( ) за тем исключением, что работают с целыми числами, а не символами, и могут использоваться только с файлами, открытыми в двоичном режиме. Следующая программа открывает двоичный файл, записывает в него ряд целых чисел, закрывает файл, а затем вновь открывает его для чтения данных с одновременным выводом их на экран:

/*
*       badfile.c
*   Эта программа на языке С демонстрирует использование функций getw() и
*  putw( ) для ввода-вывода данных из двоичного файла .
*/

#include <stdio.h>
#include <stdlib.h>
#define ISIZE 10

void main () {

FILE *pfi;

int ivalue, ivalues [ISIZE], i;

pfi = fopen ("integer.dat","wb") ;
if (pfi== NULL) {

printf("Heудалось открыть файл.");

exit(l);

}

for(i = 0; i < ISIZE; i++) {

ivalues[i] = i + 1;

putw(ivalues[i], pfi); } fclose(pfi);

pfi = fopen("integer.dat",   "wb");
if(pfi == NULL)   {

printf("Heудалось открыть файл.");

exit(l); } while(Ifeof(pfi)) {

ivalue = getw(pfi);

printf("%3d",ivalue); } }

Посмотрите, какие данные будут выведены на экран при выполнении этой программы, и попытайтесь определить, что было сделано неправильно:

1 2 3 4 5 6 7 8 9 10 -1

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

Чтобы исправить ошибку, допущенную в предыдущем примере, применим метод упреждающего чтения.

/*
*  getwputw.c
*  Это исправленная версия предыдущего примера .
*/

#include <stdio.h>
#include <stdlib.h>
#define ISIZE 10

void main () {

FILE *pfi;

int ivalue, ivalues [ISIZE], 1;

pfi = f open ("integer .dat","wb"); if (pfi == NULL) {

printf("Heудалось открыть файл.");

exit(l); } ford = 0; i < ISIZE; 1++) {

ivalues [i]= i + 1,-

putw (ivalues [i], pfi); ) f close (pfi);

pfi = fopen ("integer .dat","rb");
if (pfi== NULL) (

printf("Heудалось открыть файл.");

exit(l);

}

ivalue = getw(pfi); while(Ifeof(pfi)) {

printf("%3d",ivalue) ivalue = getw(pfi); }

Прежде чем приступить к выполнению цикла while, программа осуществляет упреждающее чтение файла, для того чтобы проверить, не является ли он пустым. Если файл не пуст, то в переменную ivalue будет записано целочисленное значение. Если же файл окажется пустым, то это будет обнаружено функцией feof( ) .
Также обратите внимание, что метод упреждающего чтения потребовал измене­ния последовательности инструкций в цикле while. Предположим, цикл выполнен уже девять раз. На девятой итерации переменная ivalueприняла значение 9. На следующей итерации на экран будет выведено 9, а переменной будет присвоено значение 10. Цикл выполнится еще раз, в результате чего на экране отобразится 10 и переменная ivalueпримет значение -1, соответствующее константе EOF. Это вызовет завершение цикла while, поскольку функция feof(} определит конец файла.

Форматный вывод данных

Для вывода форматированных данных, как правило, применяются функции printf() и fprintf(). Первая записывает данные в поток stdout, а вторая — в указанный файл или поток. Как уже говорилось ранее, аргументами функции printf() являются строка форматирования и список выводимых переменных. (В функции fprintf() первый аргумент — указатель файла.) Для каждой переменной из списка задание формата вывода осуществляется с помощью следующего синтаксиса:

%[флаги][ширина][.точность][{h   |  1}]спецификатор

В простейшем случае указывается только знак процента и спецификатор, например %f.Обязательное поле спецификатор указывает на способ интерпретации переменной: как символа, строки или числа (табл. 10.2). Необязательное поле флаги определяет дополнительные особенности вывода (табл. 10.3).

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

Необязательное поле точность интерпретируется следующим образом:

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

Необязательные модификаторы hи 1 определяют размерность целочисленной переменной: hсоответствует ключевому слову short, al — long.

Таблица 10.2. Спецификаторы типа переменной в функциях printf() и fprintf()
Спецификатор

Тип

Формат вывода
c int Символ (переменная приводится к типу unsignedchar)
d, i int Знаковое десятичное целое число
o int Беззнаковое восьмеричное целое число  
u int Беззнаковое десятичное целое число
x int Беззнаковое шестнадцатеричное целое число (в качестве цифр от 10 до 15 используются буквы "abcdef')
X int Беззнаковое шестнадцатеричное целое число (в качестве цифр от 10 до 15 используются буквы "ABCDEF")
e double Знаковое число с плавающей запятой в формате [ - ] т. ddde xxx, где т — одна десятичная цифра, ddd— ноль или более десятичных цифр (количество определяется значением поля точность, нулевая точность подавляет вывод десятичной точки, по умолчанию точность — 6), 'е' — символ экспоненты, ххх — ровно три десятичные цифры (показатель экспоненты)
E double То же, что и е, только вместо символа 'е' применяется 'Е'
f double Знаковое число с плавающей запятой в формате [ - ] mmm.ddd, где mmm — одна или более десятичных цифр, ddd — ноль или более десятичных цифр (количество определяется значением поля точность, нулевая точность подавляет вывод десятичной точки, по умолчанию точность — 6)
g double Знаковое число с плавающей запятой в формате f или е; формат е выбирается, если показатель экспоненты меньше -4 или больше либо равен значению поля точность; десятичная точка не ставится, если за ней не следуют значащие цифры; хвостовые нули не выводятся
G double То же, что и g, но при необходимости выбирается формат Е, а не е
int *

Ничего не выводится; количество символов, выведенных к данному моменту функцией, записывается в переменную

void * Адрес, содержащийся в указателе (отображается в формате х)
s    char * Строка символов; вывод осуществляется до тех пор, пока не будет обнаружен символ \0 или число символов не станет равным значению поля точность

Таблица 10.3. Флаги в фугкциях printf() и fprintf()
Флаг Назначение
- Если число выведенных символов оказывается меньше указанного, результат выравнивается по левому краю поля вывода (по умолчанию принято правостороннее выравнивание)
+ При выводе знаковых чисел знак отображается всегда (по умолчанию знак устанавливается только перед отрицательными числами)
0 Если значению поля ширина предшествует символ '0', выводимое число дополняется ведущими нулями до минимальной ширины поля вывода (по умолчанию в качестве заполнителей применяются пробелы); при левостороннем выравнивании игнорируется
пробел Если выводится положительное знаковое число, перед ним ставится пробел (по умолчанию пробел в таких случаях не ставится); игнорируется при наличии флага +
# Для чисел формата о, х и X означает добавление ведущих 0, 0х и 0Х соответственно (по умолчанию отсутствуют); для чисел формата е, Е, g, G и f задает присутствие десятичной точки, даже когда за ней не следуют значащие цифры (по умолчанию точка в таких случаях не ставится); для чисел формата g и G предотвращает отбрасывание хвостовых нулей (по умолчанию отбрасываются)

Функция printf()

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

/*
*       printf.c
*  Эта программа на языке С демонстрирует применение
*   спецификаторов форматирования функции printfO.
*/

#include <stdio.h>

void main () {

char  с     = 'А',

psz[]= "Строка для экспериментов";
int  iln  = 0,

ivalue = 1234;
double dPi   = 3.14159265;

/* 1 — вывод символа с */
printf("\n[%2d] %c",++iln, c) ;

/* 2 — вывод ASCII-кода символа с */
printf<"\n[%2d] %d",++iln, c);

/* 3 — вывод символа с ASCII-кодом 90 */
printf("\n[%2d] %c",++iln, 90);

/* 4 — вывод значения ivalue в восьмеричной системе */
printf("\n[%2d] %o",++iln, ivalue);

/* 5 — вывод значения ivalue в шестнадцатеричной */
/* системе с буквами в нижнем регистре */
printf("\n[%2d]%х",++iln, ivalue);

/* 6 — вывод значения ivalue в шестнадцатеричной */
/*     системе с буквами в верхнем регистре    */
printf("\n[%2d]%Х",++iln, ivalue); 

/* 7 — вывод одного символа, минимальная ширина поля равна 5, */
/* выравнивание вправо с дополнением пробелами         */
printf("\n[%2d]%5c",++iln, с); .

/* 8 -- вывод одного символа, минимальная ширина поля равна 5, */
/* выравнивание влево с дополнением пробелами         */
printf("\n[%2d]%-5c",++iln, с);

/* 9 — вывод строки, отображаются 24 символа */ 
printf("\n[%2d]%s",++iln, psz);

/* 10 — вывод минимум 5-ти символов строки, отображаются 24 символа */
printf ("\n[%d]%5s",-n-iln, psz);

/* 11 — вывод минимум 38-ми символов строки, */
/* выравнивание вправо с дополнением пробелами */
printf("\n[%d]%38s",++iln, psz);

/*12 — вывод минимум 38-ми символов строки, */
/* выравнивание влево с дополнением пробелами */
printf("\n[%d]%-38s",++iln, psz);

/* 13 — вывод значения ivalue, по умолчанию отображаются 4 цифры  */
printf("\n[%d]%d",++iln, ivalue);

/* 14 — вывод значения ivalueсо знаком */  
printf("\n[%d]%+d",++iln, ivalue);

/* 15 — вывод значения ivalueминимум из 3-х цифр, */
/* отображаются 4 цифры*/
printf("\n[%d]%3d",++iln, ivalue);

/* 16 — вывод значения ivalueминимум из 10-ти цифр, */
/* выравнивание вправо с дополнением пробелами */
printf("\n[%d]%10d",++iln, ivalue);

/* 17 — вывод значения ivalueминимум из 10-ти цифр, '*/
/*      выравнивание влево с дополнением пробелами  */ 

printf("\n[%d]%-10d",++iln, ivalue);

/* 18 — вывод значения ivalue минимум из 10-ти цифр, */

/*      выравнивание вправо с дополнением нулями    */
printf ("\n[%d]%010d",-n-iln, ivalue); 

/* 19 — вывод значения dPiс форматированием по умолчанию */
printf("\n[%d]%f",++iln, dPi); .

/* 20 — вывод значения dPi, минимальная ширина поля равна 20, */
/* выравнивание вправо с дополнением пробелами */
printf("\n[%d]%20f",++iln, dPi) ;

/* 21 — вывод значения dPi, минимальная ширина поля равна 20, */
/*'выравнивание вправо с дополнением нулями        */
printf("\n[%d]%020f",++iln, dPi) ;

/* 22 — вывод значения dPi, минимальная ширина поля равна 20, */
/* выравнивание влево с дополнением пробелами        */
printf("\n[%d]%-20f",++iln, dPi);

/* 23 — вывод 19-ти символов строки,     */
/*      минимальная ширина поля равна 19 */ 
printf("\n[%d]%19.19s",++iln, psz);

/* 24 — вывод первых двух символов строки */
printf("\n[%d]%,2s",++iln, psz);

/*'25 — вывод первых двух символов строки, минимальная ширина поля */
/*
равна 19, выравнивание вправо с дополнением пробелами '*/
printf("\n[%d]119.2s",++iln, psz);

/* 26 — вывод первых двух символов строки, минимальная ширина поля */
/* равна 19, выравнивание влево с дополнением пробелами */
printf("\n[%d]%-19.2s",++iln, psz);

/* 27 "- вывод первых шести символов строки, минимальная ширина поля */
/* равна 19, выравнивание вправо с дополнением пробелами */
printf ("\n[%d]%*.*s",++iln, 19,6, psz);

/* 28 — вывод значения dPi,минимальная ширина поля */
/*      равна  10, 8 цифр после десятичной точки    */
printf("\n[%d] %10.8f",++iln, dPi);

/* 29 — вывод значения dPi, минимальная ширина поля */
/* равна 20, 2 цифры после десятичной точки, */
/* выравнивание вправо с дополнением пробелами */
printf("\n[%d]%20.2f",++iln, dPi) ;

/*' 30 — вывод, значения dPi, минимальная ширина поля */              .
/*      равна 20, 4 цифры после десятичной точки,   */
/*    .  выравнивание влево с дополнением пробелами */
printf("\n[%d]%-20.4f",++iln, dPi);

/* 31 — вывод значения dPi, минимальная ширина поля */
/* равна 20, 4 цифры после десятичной точки, */
/* выравнивание вправо с дополнением пробелами */
printf("\n[%d]%20.4f",++iln, dPi);

/* 32 — вывод значения dPi, минимальная ширина поля */
/* равна 20, 2 цифры после десятичной точки, */
/* выравнивание вправо с дополнением пробелами, */
/* научный формат (с экспонентой)        */
printf("\n[%d]%20.2e",++iln, dPi);

Результат работы программы будет выглядеть следующим образом:

[ 1] А

[ 2] 65

[ 3] Z

[ 4] 2322   

[ 5] 4d2

[ 6] 4D2

[ 7]     А

[ 8] А

[ 9] Строка для экспериментов

[10] Строка для экспериментов

[11] Строка для экспериментов

[12]Строка для экспериментов

[13]1234                .

[14] +1234     

[15]1234

[16]      1234

[17]1234

[18] 0000001234

[19]3.141593

[20]              3.141593

[21]0000000000003.141593

[22] 3.141593

[23]Строка для эксперим

[24] Ст

[25]           Ст

[26] Ст

[27]           Строка

[28]3.14159265

[29]              3.14

[30]3.1416

[31]              3.1416

[32]            3.14е+000

Поиск в файлах с помощью функций fseek( ), ftell( ) и rewind()

Функции fseek(),ftell() и rewind() предназначены для определения и изменения положения маркера текущей позиции файла. Первая из них, fseek() , перемещает маркер в файле, заданном указателем pf, на требуемое число байтов, определенное аргументом ibytes. Перемещение осуществляется от начала файла (аргумент ifrom равен 0), от текущего положения маркера (аргумент ifrom равен 1) или от конца файла (аргумент ifrom равен 2). В языке С предусмотрены три константы, которые можно указывать в качестве аргумента ifrom: seek_set (сдвиг от начала файла), seek_cur(сдвиг от текущей позиции) и seek_end(сдвиг от конца файла). Функция fseek ( ) возвращает ноль при успешном завершении и EOF в противном случае. Синтаксис функции таков:

fseek(pf,  ibytes, ifrom) ;

Функция ftell() возвращает текущую позицию маркера в файле, которая определяется величиной смещения в байтах от начала файла. Возвращаемое значение имеет тип long.
Функция rewind( ) просто перемещает маркер в начало файла.

В следующей программе на языке С продемонстрировано использование всех трех функций: fseek(),ftell() и rewind().

/*
*   fseek.с
*  Эта программа на языке С демонстрирует использование
*  функций fseek(), ftell() и rewind() .
*/

#include <stdio.h>

void main ( ) {

FILE *pf;

char c;

long llocation;

pf = fopen("test.dat","r+t");

с = fgetc(pf ) ;   
putchar (c) ;  

с = fgetc(pf ) ;
putchar (c);

llocation = ftell (pf ) ;

с = fgetc (pf) ;

putchar (c); 

fseek(pf,llocation, 0) ;

с = fgetc(pf) ;

putchar(с);    

fseek(pf, llocation, 0); 
fputc.(E1, pf) ;

fseek(pf,llooation, 0) ;

с = fgetc(pf); putchar(c);

rewind (pf);   

с = fgetc (pf) ;   
putchar(c);

}   

Переменная llocation имеет тип long. Это связано с тем, что язык С поддерживает работу с файлами, размер которых превышает 64 Кб. Файл TEST.DAT содержит строку "ABCD". Первая из функций fgetc() возвращает букву 'А', после чего программа выводит ее на экран. В следующих двух строках выводится буква 'В'.

Далее функция ftell() записывает в переменную llocation значение маркера текущей позиции в файле. Поскольку буква 'В' уже прочитана, переменная llocationбудет содержать 2. Это означает, что в данный момент маркер указывает на третий символ, который смещен на 2 байта от первого символа 'А'.
В следующей паре строк считывается и отображается буква 'С'. После выполнения данной операции маркер сдвинется на четвертый символ — 'D'.
В этом месте программа вызывает функцию fseek (), которая перемещает маркер на 2 байта от начала файла (отсчет идет от начала файла, поскольку третий аргумент функции равен 0). После выполнения функции маркер вновь "указывает на третий символ в файле. Поэтому в результате работы следующих двух строк на экран будет выведена буква 'С'.

При втором вызове функции fseek() устанавливаются те же параметры, что и в первом случае. Но на этот раз с помощью функции fputc() в файл записывается буква 'Е' на место буквы 'С'. Чтобы убедиться, что новая буква действительно была вставлена в файл, программа в очередной раз обращается к функции fseek()для перемещения к третьей позиции, считывает символ в этой позиции и отображает его. Этим символом будет 'Е'.

3атем вызывается функция rewind(),которая перемещает маркер в начало файла. При следующем вызове функции fgetc() из файла считывается буква 'А', которая и выводится на экран. В результате работы программы на экране будет получена следующая последовательность букв:

АВССЕА 

Форматный ввод

Форматирование вводимых данных в языке С можно осуществлять с помощью достаточно мощных функций scanf() и fscanf (), Различие между ними заключается в том, что последняя требует указания файла, из которого читаются данные. Функция scanf() принимает данные из стандартного входного потока stdin.

Функции scanfC ), fscanf( ) и sscanf( )

Функция scanf (), как и ее "родственники" fscanf() и sscanf (), в качестве аргумента принимает такого же рода строку форматирования, что и функция printf(), осуществляющая вывод данных, хотя имеются и некоторые отличия. Для примера рассмотрим следующее выражение:

scanf("%2d%5s%4f",   sivalue,   psz,   &fvalue);

Данная функция считывает целое число, состоящее из двух цифр, строку из пяти символов и число с плавающей запятой, образованное не более чем четырьмя символами (2,97, 12,5 и т.п.). Все аргументы, перечисленные после строки форматирования, должны быть указателями, т.е. содержать адреса переменных. А теперь попробуем разобрать более сложное выражение:

scanf ("  \"%[^А-Zа-z-] %*[-] %[^\"]",   ps1,  ps2);

Первым в строке форматирования стоит пробел. Он служит указанием пропустить все ведущие пробелы, символы табуляции и новой строки, пока не встретится двойная кавычка (")В строке форматирования этот символ защищен обратной косой чертой (\"), так как в противном случае он означал бы завершение самой строки! Таким образом, обратная косая черта является своего рода командой отмены специального назначения следующего за ней символа.

Управляющая последовательность %[^A-Za-z-] говоритотом, что встроку рs1 нужно вводить все, кроме букв и символа дефиса (-). Квадратные скобки, в которых на первом месте стоит знак крышки (Л), определяют диапазон символов, из которых не должна состоять вводимая строка. В данном случае в диапазон входят буквы от 'А' до 'Z' (дефис между ними означает "от и до") и от 'а' до 'z', а также сам дефис. Если убрать знак ^, то, наоборот, будут ожидаться только буквы и символы дефиса. В конец прочитанной строки добавляется символ \0. Если во входном потоке вслед за открывающей кавычкой первыми встретятся буква или дефис, выполнение функции scanf() завершится ошибкой и в переменные ps1 и рs2 ничего не будет записано, а сама строка останется в буфере потока. Чтение строки ps1 продолжается до тех пор, пока не встретится один из символов, указанных после знака ^.

Вторая управляющая последовательность % * [ - ] говорит о том, что после чтения строки ps1 должна быть обнаружена группа из одного или нескольких дефисов, которые необходимо пропустить. Символ звездочки (*) указывает на то, что данные должны быть прочитаны, но не сохранены. Отметим два важных момента.

Последняя управляющая последовательность %[^\"] указывает на то, что в строку ps2 нужно считывать все символы, кроме двойных кавычек, которые являются признаком завершения ввода.

Для полной ясности приведем пример входной строки:

"65---ААА"

Вот что будет получено в результате: *psl= 65, *ps2 = ааа.

Все вышесказанное справедливо и в отношении функций fscanf () и sscanf (). Функция sscanf() работает точно так же, как и scanf(),но читает данные из указанного символьного массива, а не из потока stdin. В следующем примере показано, как с помощью функции sscanf() преобразовать строку цифр в целое число:

sscanf(psz,"%d",&ivalue);