4.5. Последовательные текстовые файлы. Формат
Файлы – последовательные потоки символов
По умолчанию, файлы, с которыми работают стандартные функции ввода-вывода, являются последовательными текстовыми, т.е. представляют собой потоки символов в сложившейся стандартной системе представления текста «символ-байт» (см. 4.4). Другие формы представления данных в файле и способы работы с ними рассмотрены в 9.4,9.5.
Работа с файлами производится в сеансовом режиме. Это значит, что функция открытия файла устанавливает связь между открытым файлом и идентифицирующим его элементом в программе. Идентификатором файла (в некоторых библиотеках) может быть целое число (handle). В стандартной библиотеке используется указателе на структурированную переменную – описатель (дескриптор) файла в библиотеке типа FILE*. При выполнении операций чтения-записи и при закрытии файла нужно использовать этот идентификатор как параметр функций ввода-вывода.
Функция | Параметры и результат | |
---|---|---|
FILE *fopen(char *name, char *mode) | Открыть файл | Результат FILE* - указатель на описатель файла или NULL fd -- идентификатор файла (указатель на описатель) name - строка с именем файла mode - строка режима работы с файлом |
int fclose(FILE *fd) | Закрыть файл | |
FILE *freopen(char *name, char *mode, FILE *fd) | Закрыть и открыть повторно | |
FILE *tmpfile(void) | Создать и открыть временный с уникальным именем |
Работа с текстовыми файлами имеет существенные ограничения. Файл можно открыть для использования только одного вида из перечисленных операций:
чтение текста из существующего файла;
запись текста во вновь создаваемый файл;
добавление текста в существующий файл.
Для указания режима используется параметр - текстовая строка, которая имеет вид «r», «w» или «a» соответственно. Поэтому при изменении содержимого текстового файла его можно либо полностью переписать из входного в выходной, либо читать в память полностью и редактировать его там.
Стандартная библиотека ввода-вывода ориентирована на работу в режиме командной строки (консольного приложения). Стандартные потоки ввода-вывода (клавиатура, экран) представляют собой «файлы», открытые перед выполнением main. Они так и объявлены в библиотеке, а функции, работающие со стандартным вводом, представляют собой частный случай работы с файлами.
extern FILE *stdin, *stdout, *stderr, *stdaux, *stdprn; // Имеется в stdio.h
#define getchar() getc(stdin)
При запуске программы в режиме командной строки stdin и stdout могут быть перенаправлены на любые текстовые файлы (например, ввод из a.txt, вывод в c:\xx\b.txt).
test.exe <\a.txt>c:\xx\b.txt
Посимвольный ввод-вывод
Первая группа функций ввода-вывода передает «за один присест» по одному символу текста из потока или в поток.
Посимвольный ввод | Параметры и результат | |
---|---|---|
int getc(FILE *fd) | Явно указанный файл | Код символа или EOF |
inc getchar(void) | Стандартный ввод | Код символа или EOF |
int ungetc(int ch, FILE *fd) | Возвратить символ в файл (повторно читается) | |
Посимвольный вывод | ||
int putc(int ch, FILE *fd) | Явно указанный файл | Код символа или EOF |
inc putchar(int ch) | Стандартный вывод | Код символа или EOF |
Примечание: хотя функции и выполняют ввод отдельного символа, обычно он осуществляется в стандартном режиме построчного ввода, поддерживаемого операционной системой в режиме командной строки («эхо»-печать вводимых символов, редактирование строки). Поэтому в библиотеку строка попадает только полностью, после ввода символа «конец строки», а уже затем выдается в программу посимвольно. Для немедленного реагирования программы на введенный символ или отказ от эхо-печати необходимо пользоваться нестандартными библиотеками (например, conio.h).
В следующем примере производится посимвольное копирование входного файла в выходной. При обнаружении символа – латинской буквы цепочка букв копируется в буфер, а затем записывается в выходной файл в обратном порядке.
#include <stdio.h>
//---------------------------------------------------------------------------45-01.cpp
// "Перевертывание" идентификаторов
int isalpha(char cc) {
return cc >= 'a' && cc <= 'z' || cc >= 'A' && cc <= 'Z';
}
void main() {
char c[100];
int i, j;
FILE * fd1 = fopen("45-01.cpp", "r"); // Чтение файла
FILE * fd2 = fopen("45-01.txt", "w"); // Создание и запись файла
if (fd1 == NULL || fd2 == NULL) return;
while ((c[0] = getc(fd1)) != EOF) { // Посимвольное чтение
if (isalpha(c[0])) { // Первая буква слова
i = 1; // Читать слово в массив
char ss;
while (isalpha(ss = getc(fd1))) c[i++] = ss;
for (i--; i >= 0; i--) putc(c[i], fd2);
putc(ss, fd2); // Вывести в обратном порядке
} // а также символ – после слова
else putc(c[0], fd2);
}
fclose(fd1);
fclose(fd2);
}
Построчный ввод-вывод
Форматированный ввод | Параметры и результат | |
---|---|---|
int fprintf(FILE *fd, char format[],...) | Явно указанный файл | Число фактических параметров, для которых введены значения, или EOF |
int printf(char format[],...) | Стандартный ввод | Число фактических параметров, для которых введены значения, или EOF |
int sprintf(char str[], char format[],...) | Строка в памяти | Число фактических параметров, для которых введены значения, или EOF |
Форматированный вывод | ||
int fprintf(FILE *fd, char format[],...) | Явно указанный файл | Число выведенных байтов или EOF |
int printf(char format[],...) | Стандартный вывод | Число выведенных байтов или EOF |
int sprintf(char str[], char format[],...) | Строка в памяти | Число выведенных байтов или EOF |
При построчном вводе-выводе нужно учитывать, что строка в файле (потоке) и в памяти – не совсем одно и то же, в первом случае она является последовательностью символов произвольной длины, ограниченной символом '\n', а во втором случае она размещена в массиве заданной размерности и ограничена символом '\0'. Отсюда нюансы:
при вводе-выводе все строки функции используют в качестве стандартного ограничителя строки в памяти символ '\0'. Символ конца строки '\n' уничтожается при стандартном вводе-выводе (gets - не записывает в строку, а puts автоматически добавляет при выводе) и сохраняется в строке при вводе-выводе из явно указанного файла (fgets - записывает в строку, fputs - выводит имеющийся в строке (сам не добавляет));
при построчном вводе необходимо обеспечить соответствие между длиной строки в файле (которая, в принципе, может быть любой) и размерностью массива символом. Контроль за этим осуществляется так: функция задает размерность массива символов. Если строка короче, то она будет иметь в массиве два ограничителя – символы ‘\n’ и ‘\0’ (конец строки в потоке и в памяти), если же нет, то только символ ‘\0’. Если этот факт игнорировать, то длинные строки при чтении из файла будут «порезаны» на части.
Форматированный ввод-вывод Форматированный ввод-вывод позволяет программе обмениваться данными не только символьного типа (символы, фрагменты строки), но и числовыми данными базовых типов (целые, вещественные). При этом, естественно, происходит преобразование данных из внешней формы во внутреннюю – при вводе (и наоборот – при выводе) (см. 4.4). Кроме того, можно управлять размещением данных в выходном потоке (выравнивание, число позиций и т.п.).
Форматированный ввод
Форматированный ввод | Параметры и результат | |
---|---|---|
int fprintf(FILE *fd, char format[],...) | Явно указанный файл | Число фактических параметров, для которых введены значения, или EOF |
int printf(char format[],...) | Стандартный ввод | |
int sprintf(char str[], char format[],...) | Строка в памяти | |
Форматированный вывод | ||
int fprintf(FILE *fd, char format[],...) | Явно указанный файл | Число выведенных байтов или EOF |
int printf(char format[],...) | Стандартный вывод | |
int sprintf(char str[], char format[],...) | Строка в памяти |
Форматная строка представляет собой шаблон, задающий выводимый в поток текст. В определенные места текста подставляются значения переменных из списка, следующего за форматной строкой. Место подстановки значения очередной переменной определяется символом «%» (спецификация формата). Спецификация имеет ряд необязательных параметров и один обязательный – тип подставляемой переменной. Ее вид - % [флаги][ширина][.точность][модификатор] тип.
Вид параметра | Значение | Действие |
---|---|---|
Флаги | - | выравнивание по левому краю поля |
+ | выводится знак числа ("+" или "-") | |
(пробел) | выводится пробел перед положительным числом | |
# | выводится идентификатор системы счисления (0- восьмеричная, 0x-шестнадцатеричная) | |
Ширина | n | минимальная ширина поля n (незаполненные позиции - пробелы) |
0n | то же, незаполненные позиции – нули | |
* | задается следующим фактическим параметром функции | |
Точность | .n | количество цифр дробной части |
Модификатор | h | Short |
l | long ( l – long int, lf – double) | |
Тип | с | Char |
d | Int | |
i | Int | |
u | unsigned десятичное | |
x | unsigned шестнадцатеричное | |
X | unsigned шестнадцатеричное (цифры A..F) | |
e | float в формате xxx.xxxexxx | |
E | float в формате xxx.xxxExxx | |
g | e или f | |
G | e или F | |
f | float в формате xxx.xxx | |
s | char* или char[] – массив символов (строка) |
Форматный ввод-вывод имеет ряд нюансов, понять которые можно, исходя из основного его назначения: сохранение данных различного вида в текстовых файлах, где формат определяет их размещение и, отчасти, внешний вид. Интерактивный ввод-вывод в режиме командной строки является для массового пользователя анахронизмом и используется «для внутреннего употребления» в системном программировании:
в форматном вводе-выводе источником данных может быть, кроме файла, строка символов (функции sscanf,sprintf), они имеют дополнительный параметр – массив символов – источник данных;
при форматном вводе функция читает ровно столько символов, сколько определяется спецификаторами ввода, а разделители возвращает во входной поток. Поэтому следует аккуратно «мешать» форматированный ввод с посимвольным и построчным. Например, если в строке содержится два целых числа, которые читаются функцией fscanf, то следующая функция fgets прочитает пустую строку, т.к. символ конца строки, ограничивающий второе число, будет возвращен во входной поток;
тип s используется для ввода-вывода массивов символов (строк), но при этом ввод осуществляется либо по заданному в формате количеству символов, либо до первого разделителя (в качестве которых используются пробел, конец строки, табуляция (‘\t’), запятая, точка с запятой);
функции форматного ввода требуют передачи указателей на переменные в списке (см.5.2), поэтому имена переменных должны предваряться операцией &;
типы переменных в списке и обозначенные типы в форматной строке должны совпадать, иначе происходят ошибки во время выполнения программы, не обнаруживаемые транслятором.
Последовательность, заданная форматом
Посимвольная и построчная структура текста – это первый уровень организации последовательного файла. Следующий этап состоит в разбиении его на структурные единицы с более сложным порядком следования, называемом форматом. Формат - это описание варьируемой последовательности элементов, в которой значение текущего элемента может определять порядок следования идущих за ним. Таким образом, формат всегда является саморазворачивающимся. Способы организации порядка следования элементов в формате аналогичны способам организации порядка выполнения действий в управляющих структурах программ:
последовательное размещение разных форматных единиц друг за другом;
повторение нескольких форматных единиц в цикле с использованием счетчика повторений или элемента-ограничителя;
выбор одной из нескольких форматных единиц в зависимости от значения элемента-селектора;
вложенность: последовательность форматных единиц является составной частью формата верхнего уровня.
Элементы в форматной последовательности несут различную функциональную нагрузку:
данные;
ограничители;
счетчики повторений;
селекторы (идентификаторы типа), определяющие вид последующего формата или значения.
Определение последовательностей посредством формата применяется, если данные имеют варьируемую структуру или переменные размерности, а также для более эффективного (сжатого) представления данных. Рассмотрим несколько примеров простых форматов:
Вещественные значения в последовательности целых чисел. В последовательности, содержащей целые положительные числа и ограниченной значением 0, изредка встречаются вещественные числа. Обозначить их появление можно с помощью значения «-1», которое играет роль индикатора (идентификатор типа):
4 5 6 -1 2.55 3 3 -1 4.75 3 0
Последовательность с повторяющимися значениями. В последовательности, содержащей целые положительные числа и ограниченной значением 0, встречаются группы подряд идущих одинаковых значений. При ее сжатии в качестве идентификатора группы используется отрицательное значение, обозначающее число повторений следующего за ним положительного значения:
4 5 6 6 6 6 6 7 7 2 3 6 6 6 6 0 // до сжатия
4 6 -5 6 -2 7 2 3 -4 6 0 // после сжатия
Заметим сразу, что в языках программирования отсутствуют средства для описания форматных последовательностей в разделе описания данных, вместо этого она «зашивается» в алгоритмическую часть. Это следует из соответствия форматных единиц управляющим конструкциям языка: для каждой форматной единицы пишется соответствующий ее структуре оператор языка (последовательность, ветвление, цикл). Таким образом, структура управляющих конструкций программы (операторов) однозначно соответствует структуре читаемого ею формата, формат как бы «зашивается» в нее. Для иллюстрации используем приведенные выше примеры.
//---------------------------------------------------------------------------------------------45-04.cpp
// Чтение последовательностей различных форматов
double F1(char c[]) {
FILE * fd = fopen(c, "r"); // Открыть файл
double s = 0;
while (1) { // Цикл чтения последовательности
int v;
double dd;
fscanf(fd, "%d", & v); // Читать очередное значение
if (v == 0) break; // Ограничитель - 0
if (v > 0) s += v; // Если >0 - добавить к сумме
else { // Если <0 - читать следующее
fscanf(fd, "%lf", & dd);
s += dd; // за ним - вещественное
}
}
fclose(fd);
return s;
}
//------------------------------------------------------------------------------------------------------
int F2(char c[], int d[]) {
FILE * fd = fopen(c, "r"); // Открыть файл
double s = 0;
int n = 0;
while (1) { // Цикл чтения последовательности
int v;
fscanf(fd, "%d", & v); // Читать очередное значение
if (v == 0) break; // Ограничитель - 0
if (v > 0) d[n++] = v; // Если >0 - добавить в массив
else {
int k = -v; // Если <0 - счетчик повторений
fscanf(fd, "%d", & v); // Следующее значение - повторяющееся
while (k-- != 0) // Цикл копирования повторяющегося
d[n++] = v; // значения
}
}
fclose(fd);
return n;
}
Между конкретной последовательностью значений в формате и структурой программы, ее распознающей, имеется та же самая взаимосвязь, как между разверткой выполняемых программой действий во времени и ее программным кодом.