Структуры и алгоритмы компьютерной обработки данных

Работа с элементами файлов

Показывать лекцию целиком

Цель лекции: изучить алгоритмы и приемы чтения-записи, перестановок, поиска и сортировок элементов файла и научиться решать задачи с использованием алгоритмов чтения-записи, перестановок, поиска и сортировок элементов файлов на языке C++.

Стандартизация ввода-вывода в С++

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

Файловый ввод-вывод, как самостоятельный аппарат, не предусмотрен в функциональной части языка С, так как изначально был зависим от платформенной реализации. Вместе с тем, обмен данными с внешними устройствами осуществляется с помощью организации потоков – общего понятия, которое достаточно наглядно демонстрирует направленную передачу данных по специально организованным каналам. При этом под внешними устройствами следует понимать устройства ввода-вывода данных, к которым также можно отнести и файлы. Прототипы основных функций для работы с файлами входят в стандартную библиотеку stdio.h. Эти функции описаны в стандарте ANSI.

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

Особенности потокового ввода-вывода в файлы

С точки зрения концепции языка С++ файлы представляют собой последовательности байтов (в С и С++ нет понятия <структурированный> или <типизированный> файл, как, например, в языке Pascal). Именно поэтому передача данных в потоки ввода-вывода осуществляется побайтно. Вместе с символами в виде байтов передаются и управляющие последовательности, такие как перевод строки ( '\n' – шестнадцатеричный код 0A ), возврат каретки ( '\r' – шестнадцатеричный код 0D ), символ конца файла (шестнадцатеричный код 1A ) и другие. Операционные системы MS DOS и MS Windows рассматривают пару символов '\r\n' как один символ. Это обеспечивает читабельность содержащейся информации, так как придает определенную структуру файлу в виде строк. В системе Unix строки разделяются одним символом, который интерпретируется как new line. Таким образом, внутреннее представление текста всегда соответствует системе Unix, а внешнее – реально используемой операционной системе.

Поток в С++ можно рассматривать как отдельный класс (тип данных), который представляют объекты со схожей организацией последовательной передачи данных от источника к приемнику. Потоки ввода/вывода, такие как cin и cout, являются примерами объектов класса <поток>. Список функций для работы с файловыми потоками хранится в заголовочном файле fstream.h.

В С++ предусмотрены различные режимы обмена данными в файловых потоках. Разница в организации обмена информацией заключается в наличии/отсутствии буферизации и преобразования типов данных перед помещением в поток или после извлечения из потока.

Пример 1. Программа печатает собственный код на экран и другой файл. Файл с программным кодом сохранен как Task_1.cpp.

#include "stdafx.h"
#include <iostream>
using namespace std;
#include <fstream>
int _tmain(int argc, _TCHAR* argv[]){
  char character;
  ifstream in_stream;
  ofstream out_stream;
  in_stream.open("Task_1.cpp");
  out_stream.open("Copy.txt");
  in_stream.get(character);
  while (!in_stream.eof()) {
    cout << character;
    out_stream.put(character);
    in_stream.get(character);
  }
  out_stream.close();
  in_stream.close();
  system("pause");
  return 0;
}

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

Доступ к элементам файла

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

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

Задачи редактирования файла

Более сложными являются задачи редактирования файлов на программном уровне. К типовым операциям над файлами можно отнести:

  • Исключение из файла фрагмента текста.
  • Вставка в файл фрагмента текста.
  • Замена фрагмента текста файла на другой фрагмент.
  • Упорядочивание элементов файла по определенному ключу.
  • Операцию замены можно выполнить с помощью первых двух операций: исключить один фрагмент текста и вставить другой. Но поскольку эта операция широко распространена, она часто рассматривается как отдельная операция.

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

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

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

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

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

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

    #include "stdafx.h"
    #include <iostream>
    using namespace std;
    #include <conio.h> 
    #include <ctype.h> 
    #include <io.h> 
    #define SIZE 40 
    int _tmain(int argc, _TCHAR* argv[]){
      char *buffer,str[SIZE],*pt,*qt,c; 
      long n; 
      int i,k,len; 
      FILE *f; 
      f = fopen("copy.txt","r+");
      //Режим "r" без '+' создает защиту от записи 
      printf("\n Введите символ и позицию вставки\n"); 
      c = getchar();
      scanf("%d",k); 
      n = filelength (fileno(f));
      //число символов в файле
      len = (int)n-1 ; 
      //поправка на символ конца файла
      buffer = (char*)malloc(len);
      fread(buffer, len,1,f);
      fseek(f,0L,SEEK_SET); 
      /*После команды fread текущая позиция в файле изменилась,
        поэтому возврат ее в исходное положение*/
      //Установить указатель на текущий символ на конец буфера 
      pt = buffer; 
      while(*pt++); 
      qt = pt + 1; // Второй указатель на 1 позицию вправо 
      for (i = 0; i < len - k + 1; i++) 
      // Копирование до точки вставки 
      *qt-- = *pt--; 
      buffer[k - 1] = c; 
      // Вставка символа, поскольку место свободно 
      fwrite(buffer, len,1,f); 
      fseek(f,0L,SEEK_SET);
      // После команды fwrite позиция в файле изменилась 
      fscanf(f,"%s",str); 
      printf("\n Cтрока после вставки равна %s",str); 
      system("pause");
      return 0;
    }

    Пример 3. Вставка фрагмента в файл. В качестве дополнительной области используется другой файл.

    #include "stdafx.h"
    #include <iostream>
    using namespace std;
    #include <io.h> 
    #include <stdlib.h> 
    #include <conio.h> 
    #include <ctype.h> 
    #define SIZE 2000 
    int _tmain(int argc, _TCHAR* argv[]){
      FILE *f1, *f2;
      char str[SIZE],c;
      int i,n;
      f1 = fopen("copy1.txt","w");
      f2 = fopen("copy.txt","r");
      printf("\nВведите позицию и фрагмент для вставки \n"); 
      scanf("%d%s",n,str); 
      if (n > 0  n < (int)filelength (fileno(f2)) ) {
      //Обработка  
        i = 1; 
        // Переписать исходный файл в конечный до точки вставки  
        while(i < n  feof(f2) == 0) {
          c = getc(f2);
          putc(c,f1);
          i++;
        }
        fputs(str,f1); // Переписать вставляемый фрагмент 
        // Переписать исходный файл в конечный до конца 
        while(feof(f2) == 0) {
          c = getc(f2);
            if (c!=-1) putc(c,f1);
        }
      } // Конец оператора if  
      else printf("\nОшибка ввода параметра смещения (%d)",n); 
      system("pause");
      return 0;
    }

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

  • Открыть входной файл в режиме чтения и вспомогательный файл в режиме записи (этот файл будет создан автоматически).
  • Прочесть из входного файла текст до места вставки.
  • Записать прочитанный фрагмент во вспомогательный файл.
  • Записать во вспомогательный файл вставляемый фрагмент.
  • Прочесть из входного файла остаточный фрагмент и записать его во вспомогательный файл.
  • Закрыть оба файла.
  • Пример 4. Для редактирования файла используется временный файл.

    #include "stdafx.h"
    #include <iostream>
    using namespace std;
    #define BSIZE 200 
    int _tmain(int argc, _TCHAR* argv[]){
      char arg[20]="task.txt";
      char buf[BSIZE]; 
      int ch; 
      FILE *in, *tmp; 
      in=fopen(arg,"rb"); 
      if(in==NULL) 
         printf("Не открывается файл %s\n", arg); 
      else {
        tmp = tmpfile(); 
        buf[0]='\t'; 
        while(fgets(buf+1,BSIZE-1,in) != NULL) 
          fputs(buf,tmp); 
        fclose(in); 
        in = fopen(arg,"wb"); 
        fseek(tmp,0l,SEEK_SET); 
        while ((ch=fgetc(tmp)) != EOF) 
          fputc(ch,in); 
        fclose(in); 
        fclose(tmp); 
      }
      system("pause");
      return 0;
    }

    Ключевые термины

    Класс поток – это тип данных, представленный объектами со схожей организацией последовательной передачи данных от источника к приемнику.

    Рабочая память – это дополнительная область памяти, предназначенная для временного хранения изменений при редактировании файла.

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

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

    Элемент файла – это последовательность нулей и единиц размером в один байт.

    Краткие итоги

  • Работу с объемными данными и организацию долговременного хранения данных в языках программирования осуществляют с помощью файлов, расположенных на внешних носителях.
  • Файловый ввод-вывод, как самостоятельный аппарат, не предусмотрен в стандарте языка С.
  • С точки зрения концепции языка С++ файлы представляют собой последовательности байтов.
  • Поток в С++ можно рассматривать как отдельный класс.
  • При буферизированном вводе-выводе данных искажение информации может возникать при чтении управляющих последовательностей.
  • К типовым задачам редактирования файлов на программном уровне относятся: исключение, вставка, замена и упорядочивание элементов. Причем первые два вида задач являются базовыми, и к их реализации можно свести остальные.
  • При буферизированном вводе-выводе данных вставка элемента в середину файла приводит к усечению файла до вставляемого элемента.
  • Редактирование файлов выполняется с использованием рабочей памяти для сохранения промежуточных результатов.
  • Для организации рабочей памяти при редактировании файла используют массивы, вспомогательные фалы или их комбинацию.
  • Лабораторная работа 22. Работа с элементами файлов

    Цель работы: изучить алгоритмы и приемы чтения-записи, перестановок, поиска и сортировок элементов файла и научиться решать задачи с использованием алгоритмов чтения-записи, перестановок, поиска и сортировок элементов файлов на языке C++.

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

    Теоретические сведения.

    Ознакомьтесь с материалом лекции 22.

    Задания к лабораторной работе.

    Выполните приведенные ниже задания.

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

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

    Следует реализовать каждое задание в соответствии с приведенными этапами:

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

    Отчет по лабораторной работе должен соответствовать следующей структуре.

  • Титульный лист.
  • Словесная постановка задачи. В этом подразделе проводится полное описание задачи. Описывается суть задачи, анализ входящих в нее физических величин, область их допустимых значений, единицы их измерения, возможные ограничения, анализ условий при которых задача имеет решение (не имеет решения), анализ ожидаемых результатов.
  • Математическая модель. В этом подразделе вводятся математические описания физических величин и математическое описание их взаимодействий. Цель подраздела – представить решаемую задачу в математической формулировке.
  • Алгоритм решения задачи. В подразделе описывается разработка структуры алгоритма, обосновывается абстракция данных, задача разбивается на подзадачи. Схема алгоритма выполняется по ЕСПД (ГОСТ 19.003-80 и ГОСТ 19.002-80).
  • Листинг программы. Подраздел должен содержать текст программы на языке программирования С++, реализованный в среде MS Visual Studio 2010.
  • Контрольный тест. Подраздел содержит наборы исходных данных и полученные в ходе выполнения программы результаты.
  • Выводы по лабораторной работе.
  • Ответы на контрольные вопросы.
  • Контрольные вопросы

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