9.4. Двоичные и текстовые файлы

    «Двоичный файл это:

    a) файл, в котором используется двоичный поиск,

    б) файл, в котором данные представлены в двоичной системе счисления»

    Несуществующий вопрос в системе тестирования по курсу «Информатика».

    Знакомство с вводом-выводом обычно начинается с текстовых файлов (см. 4.5). У многих оно этим и заканчивается. Однако существует еще одна общая форма представления данных в файлах, на самом деле более многообразная – двоичный файл. Сюда относятся не только файлы с «мультимедийным» содержанием: документы, изображение, звук. Форматы двоичных файлов используются в базах данных и в самой файловой системе. Ведь файловая система – это не что иное, как большой двоичный файл, в котором построена сложная многоуровневая динамическая структура данных, включающая в себя все файлы каталоги.

    Модель двоичного файла

    Двоичный файл отличается от текстового тем, что данные в нем представлены во внутренней форме (см.1.3). А поскольку при внутреннем представлении используется двоичная система счисления, то «в честь ее» файлы и называются двоичными. По существу, двоичный файл является аналогом внутренней (оперативной, физической) памяти – неограниченным массивом байтов с возможностью непосредственного обращения (произвольного доступа) к любой его части.

    Такая модель файла полностью совпадает с системой представлений, принятой в Си для работы с памятью на низком (физическом уровне).

    • физическая память имеет байтную структуру – единицей адресации является байт;

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

    • указатель на переменную интерпретируется как ее адрес в памяти. Преобразование типа указателя к void позволяет интерпретировать его как «чистый» адрес, а преобразование к char - как указатель на массив байтов (физическое представление памяти).

      Исходя из этих принципов, функции двоичного ввода-вывода fread и fwrite переносят содержимое памяти в двоичный файл «прозрачно», т.е. байт в байт без каких либо преобразований. Функции используются для перенесения данных из файла в память программы (чтение) и обратно (запись).

    C
    int         fread  (void *buf, int size, int nrec, FILE *fd);
    
    int         fwrite (void *buf, int size, int nrec, FILE *fd);

    Особенностью этих функций является то, что для них безразличен (неизвестен) характер структуры данных в той области памяти, в которую осуществляется ввод-вывод (указатель void* buf). Функци fread читает, а функция fwrite пишет в файл, начиная с текущей позиции, массив из nrec элементов размерностью size байтов каждый, возвращая количество успешно прочитанных (записанных) элементов.

    Чтобы воспользоваться этими функциями, необходимо обеспечить преобразования переменных к «массиву байтов», используя указатели для задания адресов и операцию sizeof для вычисления размерности:

    C
    // Прочитать  целую переменную и следующий за ней
    
    // динамический массив из n переменных типа double
    
    int n;                                         // в целой переменной – размерность массива
    
    fread(&n, sizeof(int),1,fd);            // указатель на переменную int
    
    double   *pd = new double[n];                 
    
    fread(pd, sizeof(double),n,fd);      // преобразование к void* - неявное

    Дальнейшее изложение приходится начинать с банальности: при использовании исключительно функций fread/fwrite данные, записанные в определенной последовательности в файл, хранятся в нем и читаются в том же самом порядке. Этот неизменный порядок извлечения данных называется последовательным доступом, а файл - последовательным двоичным файлом. Естественно, что нас при этом не интересуют адреса размещения данных в файле. Однако существует и другой способ, позволяющий извлекать данные в любом произвольном порядке – прямой (или произвольный) доступ.

    Произвольный доступ базируется на понятии адреса в двоичном файле. Поскольку на физическом уровне двоичный файл представляется как «неограниченно растущий» массив байтов, то под адресом понимается порядковый номер байта, начиная с 0.

    В Си для представления адресов используются переменные типа long, на которые можно распространить известное понятие указатель - указатель в файле. Такая образная аналогия вполне уместна и позволяет с общих позиций рассматривать структуры данных, размещенные в файлах. Указатель в файле не является типизированным, его тип никак не связан с типом адресуемых данных и он рассматривается как «чистый» физический адрес в файле.

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

    Замечание: текущая позиция в файле является адресом размещения переменной в нем, но получить этот адрес можно перед, и не после ее чтения оттуда.

    Текущую позицию можно читать и устанавливать с помощью функций позиционирования, которые превращают последовательный файл в файл произвольного доступа. Функция long ftell(FILE *fp) возвращает текущую позицию в файле. Если по каким-то причинам текущая позиция не определена, функция возвращает -1. Это же самое значение будем использовать в дальнейшем для представления недействительного значения файлового указателя (файловый NULL), самостоятельно определив его

    #define FNULL -1L

    Функция int fseek(FILE *fp, long pos, int mode) устанавливает текущую позицию в файле на байт с номером pos. Параметр mode определяет, относительно чего отсчитывается текущая позиция в файле, и имеет символические и числовые значения (установленные в stdio.h):

    C
    #define SEEK_SET 0    // Относительно начала файла
    
                                        // начало файла - позиция 0
    
    #define  SEEK_CUR 1    // Относительно текущей позиции,
    
                                        // >0 - вперед, <0 - назад
    
    #define SEEK_END 2    // Относительно конца файла    
    
    // (значение pos - отрицательное)

    Функция fseek возвращает значение 0 при успешном позиционировании и -1 (EOF) - при ошибке. Различные значения параметра mode определяют различные способы адресации данных в файле:

    • значение SEEK_SET определяет абсолютную адресацию данных в файле от его начала. Заметим, что функция ftell возвращает текущую позицию в абсолютном значении;

    • значение SEEK_END за начало координат берет конец файла (EOF). Адреса уже записанных данных имеют отрицательное значение. Например, если в конце файла находится целая переменная, то ее позиция при адресации от конца файла будет иметь значение 0–sizeof(int). В этом же режиме можно определить текущую длину файла можно простым позиционированием:

    C
    long      fsize;
    
    fseek(fl,0L,SEEK_END); // Установить позицию на конец файла
    
    fsize = ftell(fd);                           // Прочитать значение текущей позиции
    • значение SEEK_CUR дает способ относительной адресации от текущего положения указателя в файле. Таким образом, задается расстояние в байтах от текущей переменной до адресуемой. Если это расстояние само находится в файле, то оно обычно носит название смещения.
    C
    fseek(fd,100,SEEK_SET);           // По адресу 100 находится смещение
    
    fread(&P,sizeof(long),1,fd);          // Читается P=46, после чтения текущая позиция
    
    fseek(fd,i,SEEK_CUR);               // 100+sizeof(long)=104, позиционирование 104+46=150

    Замечание: введя понятие произвольного доступа по адресу в файле, мы не ответили на главные вопросы: а откуда взять эти адреса и как размещаются данные в файле (распределяется память). Эти вопросы – к технологии программирования.

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

    C
    // Открыть существующий  как двоичный для чтения и записи
    
    FILE *fd; fd = fopen("a.dat","rb+wb");
    
    // Создать новый как  двоичный для записи  и чтения
    
    fd = fopen("a.dat","wb+");                        

    Дамп двоичного файла

    При отладке программ, работающих с двоичными файлами, иногда сложно установить, какой фрагмент – запись или чтение – содержит ошибку. Аналогично, при чтении уже известного формата необходимо проверять, насколько правильно читаются данные. Здесь не обойтись без навыков чтения дампа – двоичного содержимого файла. Для этого нам придется вспомнить основы представления базовых типов данных в памяти (см. 1.3). Естественно, что все данные и адреса присутствуют в шестнадцатеричной системе счисления.

    Посмотрим, как выглядит двоичный дамп файла, записанного простой программой. При создании файла в него записывается переменная типа long – резервируется место под будущий указатель на второй массив. Затем пишется размерность первого массива – под него отводится один байт и сам массив элементов типа short (размерность – 2 байта). Далее функцией ftell читается текущая позиция в файле – адрес будущего второго массива и запоминается в переменной p. Записывается размерность второго массива (1 байт) и сам массив целых переменных (размерность int – 4 байта). И, наконец, текущая позиция устанавливается на начало файла и полученное значение адреса из p записывается в начало файла.

    C
    //-------------------------------------------------94-00.cpp
    
    // Формирование ДАМПА для чтения файла
    
    void main(){
    
    FILE *fd=fopen("94-00.dat","wb");
    
    char k=10,m=4;
    
    short A[10]={6,3,7,3,4,8,300,5,23,64};
    
    int   B[4]={6,3,7,3};
    
    long p=0,offset;
    
    fwrite(&p,sizeof(long),1,fd);          // Занять место под указатель
    
    fwrite(&k,1,1,fd);             // Записать один байт - счетчик
    
    fwrite(A,sizeof(short),k,fd);          // Записать массив коротких целых (2B)
    
    p=ftell(fd);                                  // Получить значение указателя
    
    fwrite(&m,1,1,fd);                        // Записать один байт - счетчик
    
    fwrite(B,sizeof(int),m,fd); // Записать массив целых
    
    fseek(fd,0,SEEK_SET);              // К началу файла
    
    fwrite(&p,sizeof(long),1,fd);          // Обновить указатель на второй массив
    
    fclose(fd);}

    рис. 94-1. Дамп двоичного файла

    Чтобы теперь «увидеть» в дампе то, что мы записали, нужно учесть следующее:

    • дамп выводится побайтно, один байт представлен двумя шестнадцатеричными цифрами;

    • в каждой строке дампа – 16 байтов. Слева находится адрес строки (естественно, шестнадцатеричный). На рисунке для удобства обозначена младшая цифра адреса каждого байта;

    • все данные пишутся «младшими байтами вперед». Т.е. для получения значения машинного слова байты надо переписать в обратном порядке;

    • абсолютный адрес второго массива, находящийся в начале файла имеет размерность long (4 байта в данной версии компилятора). Его шестнадцатеричное содержимое нужно непосредственно использовать для поиска адресуемой области в дампе;

    • для данных другого типа необходим перевод в десятичную систему счисления, а также учет их размерности в соответствии с их типом.

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

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

    C
    // Добавить в файл вещественную переменную 
    
    double b=5.6;                           
    
    fseek (fd,0L,SEEK_END);           // Позиционироваться на конец файла
    
    long pp=ftell(fd);                        // Адрес переменной в файле
    
    fwrite (&b, sizeof(double),1,fd);    // Записать переменную как массив байтов

    Последовательность операций записи в файл создает в нем необходимую структуру данных. Иногда эти операции записи являются фиктивными с целью «занять место» под переменные с еще не определенными значениями.

    Если содержимое двоичного файла при каждом запуске программы переписывается от начала до конца, то он всегда будет содержать только необходимые данные. Иначе обстоит дело, если двоичный файл при очередном запуске редактируется. Тогда при удалении данных файле остаются неиспользованные участки. То же самое происходит при изменении (обновлении, в терминологии баз данных - update) данных, если их размерность увеличивается: они переписываются в конец файла. Эта свободные участки называются «мусором» и со временем они приводят к значительному превышению объема файла относительно его полезного содержимого. Программа может придерживаться разных стратегий «утилизации мусора»:

    • свободные участки объединяются в отдельную структуру данных, например, список, и повторно используются. Недостатком является сложность поддержания в файле дополнительной структуры данных;

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

    Изменение данных в файле не может быть выполнено непосредственно. Необходимо создать в памяти переменную, прочитать туда значение из файла, а после изменения записать обратно (обновить).

    C
    // Обновить счетчик в двоичном файле
    
    int a; long pos;                          
    
    fseek(fd,pos,SEEK_SET);                       // Читать счетчик
    
    fwrite((void*)&a, sizeof(int),1,fd);
    
    a++;                                                     // Увеличить в памяти
    
    fseek(fd,pos,SEEK_SET);                       // Записать обратно по тому же адресу
    
    fwrite((void*)&a, sizeof(int),1,fd);

    В связи с этим возникает вопросы о соответствии структур данных в двоичном файле и в памяти, а также о способах поддержания соответствия между ними. Как говорится, «возможны варианты»:

    • структуры данных в памяти и в файле принципиально различаются. Например, дерево (данные в вершинах и связи) можно сохранить в последовательном потоке (файле) в виде рекурсивного саморазворачивающегося формата (см. 8.2). В этом случае используется полная загрузка/сохранение структуры данных в виде единой операции;

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

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

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

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

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

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

    Запись фиксированной длины - все записи файла представляют собой переменные одного типа и имеют фиксированную для данного файла размерность. Обычно файл записей фиксированной длины представляет собой массив переменных одного типа.

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

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

    Последовательный доступ - файл по своей физической организации (устройство ввода-вывода) или по характеру структуры данных допускает просмотр записей в последовательном порядке. При отсутствии операций позиционирования записи в файле извлекаются в режиме последовательного доступа.

    Способы организации данных в файлах

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

    Предлагаемая классификация базируется на различии способов доступа к данным. Фактически вопрос сводится к способу получения их адреса. Их известно несколько:

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

    • адрес вычисляется, исходя из количества и размерности переменных. Сюда относятся массивы и более сложные табличные структуры данных, размерности которых хранятся в них самих (параметризация).

    • адрес содержится в другой части структуры данных, то есть используется указатель в файле. Указатель может использоваться и эпизодически как адрес (смещение) до области данных, так и для поддержания регулярных структур данных, таких как массивы указателей, списки и деревья.

    рис. 94-2. Способы организации данных в файлах

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

    Повторяющиеся фрагменты в тексте. Исходный текстовый файл может содержать вложенные друг в друга фрагменты вида (12 …), ограниченные скобками, включающими в себя произвольный текст и счетчик его повторений в виде целой константы (строки цифр). Требуется сгенерировать выходной текст, «раскрыв» повторения.

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

    C
    //--------------------------------------------------------94-01.cpp
    
    void more(FILE *fd){
    
    long pp;                                                 // Текущая позиция фрагмента повторения
    
    char c; int n=0;                                      // Количество повторов
    
    while(1){
    
                pp=ftell(fd);                                // Запомнить текущую позицию
    
                char c=getc(fd);                         //
    
                if (!isdigit(c)) break;                     //
    
                n=n*10+c-'0';                             // Накопление константы
    
                }
    
    if (n==0) n=1;                                         // Отсутствие константы - повторить 1 раз
    
    while(n--!=0){                                          // Повторять фрагмент
    
                fseek(fd,pp,SEEK_SET);            // Вернуться на начало
    
                while     ((c=getc(fd)) !=EOF && c!=')'){
    
                            if (c=='(') more(fd);          // Вложенный фрагмент -
    
                            else                              // рекурсивный вызов после '('
    
                                        putchar(c);         // Перечитать фрагмент до ')'    
    
                            }}}
    
    void main(){ FILE *fd=fopen("d310-00.txt","r"); more(fd); fclose(fd); }

    Из main функция вызывается при установленной начальной позиции файла, что по умолчанию определяет однократный просмотр его содержимого. В этом случае признаком завершения фрагмента служит конец файла (EOF).

    Постраничный просмотр текста. Для просмотра текста в произвольном порядке необходимо предварительно последовательно прочитать файл, сделав «закладки» в нужных местах, в нашем случае – запомнить адреса в файле каждой страницы текста, вызвав функцию ftell перед чтением очередной двадцатки строк.

    C
    //------------------------------------------------------94-02.cpp
    
    //----- Вывод текста с заданной страницы
    
     void main() {
    
    FILE *fd; char name[30]="94.txt" , str[80];
    
    int i,n,NP;                                              // Количество страниц в файле
    
    long *POS;                                            // Массив адресов начала страниц в файле
    
    if ((fd=fopen(name,"r"))==NULL) return;
    
    for (n=0;fgets(str,80,fd)!=NULL;n++);
    
    NP=n/20; if (n%20!=0) NP++;                  // Кол-во строк - кол-во-страниц
    
    fseek(fd,0,SEEK_SET);                          // Вернуться в начало файла
    
    POS=new long[NP];                               // Динамический массив "закладок"
    
    for (n=0; n < NP; n++){                           // Просмотр страниц файла
    
                POS[n]=ftell(fd);                         // Запомнить начало страницы
    
                for (i=0; i<20; i++)                      // Чтение строк страницы
    
                            if (fgets(str,80,fd)==NULL)
    
                                        break;                // Конец файла - выход из цикла
    
                if (i < 20) break;                         // Неполная страница - выход
    
                }
    
    while(1){
    
                printf("page number(0..%d):",NP-1); scanf("%d",&n);
    
                if ((n >= NP) || (n <0)) break;
    
                fseek(fd,POS[n],SEEK_SET);    // Позиционироваться на страницу
    
                for (i=0; i<20; i++) {                    // Повторное чтение страницы
    
                            if (fgets(str,80,fd)==NULL) break;
    
                printf("%s",str);
    
            }}}

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

    • однократно просмотреть текст, выделить в нем заголовки функций и вызовом функции ftell запомнить их адреса в тексте. Создать таблицу имен, сохранив в ней пары «имя-адрес»;

    • создать в программе стек, содержащий «точки возврата», которые также являются адресами в тексте;

    • начать интерпретацию текста с того, что найти в таблице имя main и выполнить с помощью fseek позиционирование к ее телу в файле;

    • если при чтении текста встречается «вызов» функции, то после его прочтения сохранить в стеке текущий адрес в текстовом файле, получив его через ftell. Затем найти в таблице имя вызываемой функции и позиционироваться к ее началу с помощью fseek;

    • если при чтении обнаруживается завершение функции, то из стека извлекается адрес (точка возврата) и к ней производится позиционирование с помощью fseek.

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

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

    C
    //------------------------------------------------------94-03.cpp
    
    //----- "Микрохирургическое" исправление счетчика
    
    void main() {
    
    FILE *fd; char cc, name[30]="94-03.txt";
    
    long POS; int cnt;
    
    if ((fd=fopen(name,"r+w"))==NULL) return;
    
    while(1){
    
                POS=ftell(fd);                                         // Запомнить адрес символа
    
                if ((cc=getc(fd))==EOF) break;
    
                if (cc>='0' && cc<='9'){                            // Прочитана цифра
    
                            fseek(fd,POS,SEEK_SET);         // Вернуться на 1 символ
    
                            fscanf(fd,"%d",&cnt);                  // и прочитать счетчик - 6 символов
    
                            cnt++;                                       // Увеличить счетчик
    
                            fseek(fd,POS,SEEK_SET);         // Вернуться на начало счетчика
    
                            fprintf(fd,"%06d",cnt);                  // и записать счетчик - 6 символов
    
                            break; }
    
                }
    
    fclose(fd);}

    Лабораторный практикум

    Указанные варианты заданий реализовать с использованием позиционирования указателя в текстовом файле и массива указателей, без загрузки самого текстового файла в память.

    1. Сортировка строк файла по длине и по алфавиту и вывод результата в отдельный файл.

    2. Программа-интерпретатор текста. Текстовый файл разбит на именованные модули. Каждый модуль может иметь вызовы других текстовых модулей. Требуется вывести текст модуля main с включением текстов других модулей в порядке вызова:

    C
    #aaa{
    
    Произвольные строки модуля текста ааа
    
    }
    
    #ппп{
    
    Произвольные строки текста
    
    #aaa                   // Вызов модуля текста с именем aaa       
    
    Произвольные строки текста
    
    }
    
    #main{
    
    Основной текст с вызовами других модулей
    
    }
    1. Программа - редактор текста с командами удаления, копирования, и перестановки строк, с прокруткой текста в обоих направлениях (исходный файл при редактировании не меняется).

    2. Программа - интерпретатор текста, включающего фрагменты следующего вида:

    C
    #repeat 5
    
    Произвольный текст
    
    #end

    При просмотре файла программа выводит его текст, текст фрагментов "#repeat - #end" выводится указанное количество раз. Фрагменты могут быть вложенными.

    1. Программа просмотра блочной структуры Си-программы с командами вывода текущего блока, входа в n-ый по счету вложенный блок и выхода в блок верхнего уровня.

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

    3. Программа просмотра текстового файла по предложениям. Предложением считается любая последовательность слов, ограниченная точкой, после которой идет большая буква или конец строки. Программа выводит на экран любой блок с n-го по m-ое предложение.

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

    5. Программа составляет словарь терминов. Каждый термин - слово, записанное большими (строчными) буквами. Программа запоминает каждый термин и указатель на строку, в которой он встречается. Кроме того, программа позволяет просматривать текст в обоих направлениях построчно и при выборе текущей строки ищет в ней термин и позиционируется к нему.

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

    7. Программа составляет «оглавление» текстового файла путем поиска и запоминания позиций строк вида «5.7.6 Позиционирование в текстовом файле». Затем программа составляет меню, с помощью которого позиционируется в начало соответствующих разделов и пунктов с прокруткой текста в обоих направлениях.

    8. Программа составляет словарь функций Си-программы. Затем программа составляет меню, с помощью которого позиционируется в начало соответствующих функций. (Функцию достаточно идентифицировать по фрагменту вида «идентификатор(...» вне фигурных скобок).

    9. Программа - редактор текста с командами изменения (редактирования) строки и прокруткой текста в обоих направлениях (измененные строки добавляются в в новый файл, исходный файл не меняется).

    10. Программа ищет в тексте Си-программы самый внутренний блок (для простоты начало и конец блока располагаются в отдельных строчках), присваивает ему номер и «выкусывает» основного текста, заменяя его ссылкой на этот номер. Затем по заданному номеру блока производится его вывод на экран, в тексте блока при этом должна присутствовать строка вида «#БЛОК nnn» при наличии вложенного блока. (Процедуру «выкусывания» блоков рекомендуется реализовать при помощи «выкусывания» указателей на строки вложенного блока в файле и замене их на отрицательное число -n, где n-номер, присвоенный блоку).

    11. Программа сортировки файла по длине предложений и вывода результата в отдельный файл. При выводе каждое предложение следует переформатировать так, чтобы оно начиналось с отдельной строки и располагалось в строках размером не более 60 символов.