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. Они так и объявлены в библиотеке, а функции, работающие со стандартным вводом, представляют собой частный случай работы с файлами.

    C
    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).

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

    C
    #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количество цифр дробной части
    МодификаторhShort
    llong ( l – long int, lf – double)
    ТипсChar
    dInt
    iInt
    uunsigned десятичное
    xunsigned шестнадцатеричное
    Xunsigned шестнадцатеричное (цифры A..F)
    efloat в формате xxx.xxxexxx
    Efloat в формате xxx.xxxExxx
    ge или f
    Ge или F
    ffloat в формате xxx.xxx
    schar* или 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 // после сжатия

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

    C
    //---------------------------------------------------------------------------------------------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;
    }

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