Работа с элементами файлов
Цель лекции: изучить алгоритмы и приемы чтения-записи, перестановок, поиска и сортировок элементов файла и научиться решать задачи с использованием алгоритмов чтения-записи, перестановок, поиска и сортировок элементов файлов на языке 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 – стандарт языка С, опубликованный Американским национальным институтом стандартов.
Элемент файла – это последовательность нулей и единиц размером в один байт.
Краткие итоги
- Работу с объемными данными и организацию долговременного хранения данных в языках программирования осуществляют с помощью файлов, расположенных на внешних носителях.
- Файловый ввод-вывод, как самостоятельный аппарат, не предусмотрен в стандарте языка С.
- С точки зрения концепции языка С++ файлы представляют собой последовательности байтов.
- Поток в С++ можно рассматривать как отдельный класс.
- При буферизированном вводе-выводе данных искажение информации может возникать при чтении управляющих последовательностей.
- К типовым задачам редактирования файлов на программном уровне относятся: исключение, вставка, замена и упорядочивание элементов. Причем первые два вида задач являются базовыми, и к их реализации можно свести остальные.
- При буферизированном вводе-выводе данных вставка элемента в середину файла приводит к усечению файла до вставляемого элемента.
- Редактирование файлов выполняется с использованием рабочей памяти для сохранения промежуточных результатов.
- Для организации рабочей памяти при редактировании файла используют массивы, вспомогательные фалы или их комбинацию.