Работа с текстовыми и бинарными файлами в C/C++



Скачать 135.57 Kb.
Дата01.12.2017
Размер135.57 Kb.
ТипЛекции

Работа с текстовыми и бинарными файлами в C/C++
В лекции рассмотрен классический способ работы с файлами в C/C++, основанный на библиотеке stdio.h и доступе к данным через структуру FILE. Альтернативный современный механизм работы с файлами в языке C++ на основе потоков и библиотек , , будет изучен в следующей лекции.
Базовые функции для работы с файлами описаны в библиотеке stdio.h. Вся работа с файлом выполняется через файловую переменную - указатель на структуру типа FILE, определённую в стандартной библиотеке:

FILE *fp;
Открыть файл можно функцией fopen, имеющей 2 параметра:

FILE *fopen (char *имя_файла, char *режим_доступа)

Параметр имя_файла может содержать относительный или абсолютный путь к открываемому файлу:

1) "data.txt" - открывается файл data.txt из текущей папки

Важно: при запуске exe-файла "текущая папка" – та, где он находится; при отладке в IDE папка может быть иной, например, в Visual Studio при открытом консольном решении с именем Console файл следует разместить в папке Console/Console, а при запуске исполняемого файла не из IDE – в папке Console/Debug.

2) "f:\\my.dat" - открывается файл my.dat из головной папки диска f:

3) имя файла запрашивается у пользователя:

char buf[80];

printf ("\nвведите имя файла:");

fflush (stdin);

gets (buf);

Параметр режим_доступа определяет, какие действия будут разрешены с открываемым файлом, примеры его возможных значений:

1) "rt" - открываем для чтения текстовый файл;

2) "r+b" - открываем для произвольного доступа (чтение и запись) бинарный файл;

3) "at" – открываем текстовый файл для добавления данных в конец файла;

4) "w" - открываем файл для записи без указания того, текстовый он или бинарный.

Фактически, указание "r" или "t" не накладывает каких-либо ограничений на методы, которые мы будем применять для чтения или записи данных.
После открытия файла следует обязательно проверить, удалась ли эта операция. Для этого есть 2 основных подхода:

1) стандартный обработчик ferror (см. стр.76-77 пособия);

2) сравнить указатель, который вернула fopen, с константой NULL (nullptr) из стандартной библиотеки:

fp = fopen ("text.txt","r+b");

if (fp==NULL) {

//Обработка ситуации "Не удалось открыть файл"

}
Пример. Приложение проверяет, удалось ли открыть файл из текущей папки, имя файла запрашивается у пользователя (Visual Studio)

#include

#include

#include

#include
int main(void) {

setlocale(LC_ALL,"Rus"); SetConsoleCP(1251); SetConsoleOutputCP(1251);

FILE *fp;

char buf[80];

printf ("\nвведите имя файла:");

fflush (stdin);

gets (buf);

fp = fopen (buf,"r+b");

if (fp==NULL) {

printf ("\nне удалось открыть файл");

getchar();

exit (1); //Выйти с кодом завершения 1

}
fflush(stdin); getchar(); return 0;

}
Важно! Функции, возвращающие указатель, в том числе, fopen, считаются небезопасными в ряде новых компиляторов, например, Visual Studio 2015. Если их использование приводит не просто к предупреждению, а к генерации ошибок, есть 2 основных способа решения проблемы:

1) в соответствии с рекомендациями компилятора, заменить старые названия функций на их безопасные версии, например, strcpy на strcpy_s и fopen на fopen_s. При этом может измениться и способ вызова функций, например,

FILE *out; fopen_s(&out,"data.txt", "wt");

вместо

FILE *out = fopen_s("data.txt", "wt");



2) в начало файла (до всех #include) включить директиву

#define _CRT_SECURE_NO_WARNINGS

Если используется предкомпиляция, то можно определить этот макрос в заголовочном файле stdafx.h.
Выбор способа чтения или записи данных зависит от того, какой должна быть структура файла.
Если файл форматированный, то есть, является текстовым и состоит из лексем, разделённых стандартными разделителями (пробел, табуляция, перевод строки), обмен данными с ним можно выполнять методами:


  • fscanf - для чтения

  • fprintf - для записи

Первым параметром этих функций указывается файловая переменная, в остальном работа совпадает со стандартными scanf и printf.
Пример. Файл text.txt в текущей папке приложения имеет следующий вид:

1 1.5 -3.5

2 3.5

Прочитаем его как последовательность вещественных чисел.



FILE *fp = fopen ("text.txt","r");

if (fp==NULL) {

printf ("\nне удалось открыть файл"); getchar(); exit (1);

}

float a;



while (1) {

fscanf (fp,"%f",&a);

if (feof(fp)) break; //Если файл кончился, выйти из цикла

//здесь выполняется обработка очередного значения a, например:

printf ("%.2f ",a);

}

fclose(fp);


Важно!

1. Функции семейства scanf возвращают целое число - количество значений, которые успешно прочитаны в соответствии с указанным форматом. В реальных приложениях эту величину следует проверять в коде:

int i=fscanf (fp,"%f",&a);

if (i!=1) {

//не удалось получить 1 значение

}

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



setlocale(LC_ALL,"Rus");

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

3. Очередное чтение данных изменяет внутренний файловый указатель. Этот указатель в любой момент времени, пока файл открыт, показывает на следующее значение, которое будет прочитано. Благодаря этому наш код с "бесконечным" while не зациклился.

4. Код показывает, как читать из файла заранее неизвестное количество значений – это позволяет сделать стандартная функция feof (проверка, достигнут ли конец файла; вернёт не 0, если прочитано всё).

5. Распространённый в примерах из Сети код вида

while (!feof(fp)) {

fscanf (fp,"%f",&a);

//обработка числа a

}

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


В качестве примера форматной записи в файл сохраним массив a из 10 целочисленных значений в файле с именем result.txt по 5 элементов в строке:

const int n=10;

int a[n],i;

FILE *fp=fopen ("result.txt","wt");

if (fp==NULL) {

puts ("не удалось открыть файл на запись");

getchar(); exit (1);

}

else {



for (i=0; i

for (i=0; i

fprintf (fp,"%5d ",a[i]);

if ((i+1)%5==0) fprintf (fp,"\n");

}

fclose (fp);



//Закрыть файл, делать всегда, если в него писали!

}
Важно! Ввод/вывод функциями библиотеки stdio.h буферизован, то есть, данные "пропускаются" через область памяти заданного размера, обмен данными происходит не отдельными байтами, а "порциями". Поэтому перед чтением данных желательно очищать буфер от возможных "остатков" предыдущего чтения методом fflush, а после записи данных следует обязательно закрывать файл методом fclose, иначе данные могут быть потеряны. Заметим, что консольный ввод/вывод "обычными" методами scanf и printf также буферизован.


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

  • fgetc и fputc - для посимвольного чтения и посимвольной записи данных;

  • fgets и fputs - для чтения и записи строк с указанным максимальным размером.

Как и в случае с функциями для чтения форматированных данных, у всех этих методов имеются аналоги для работы со стандартным вводом/выводом.
Пример. Читая файл, определить длину каждой строки в символах. Для решения задачи воспользуемся тем фактом, что строки завершаются символом "перевод строки" ('\n'). Предполагается, что файл уже открыт для чтения.
int c; int len=0,cnt=0;

while (1) {

c=fgetc(fp);

if (c=='\n') {

printf ("\nString %d, length=%d",++cnt,len); len=0;

}

else len++;



if (feof(fp)) break;

}

if (len) printf ("\nString %d, length=%d",++cnt,len);



Важно! Из-за особенностей реализации fgetc, без последней проверки за телом цикла код мог "не обратить внимания", например, на последнюю строку файла, состоящую только из пробелов и не завершающуюся переводом строки.
Пример. Читаем построчно файл с известной максимальной длиной строки. Предполагается, что файл уже открыт для чтения.
char buf[128];

while (1) {

fgets(buf,127,fp);

if (feof(fp)) break;

int len = strlen(buf);

if (buf[len-1]=='\n') buf[len-1]='\0';

puts (buf); //Вывести прочитанные строки на экран

}
Важно! Без дополнительной обработки прочитанные из файла строки при выводе будут содержать лишние пустые строки между строками данных. Это происходит потому, что функция fgets читает строку файла вместе с символом перевода строки (точней, под Windows - с парой символов \r\n, интерпретируемых как один), а puts добавляет к выводимой строке ещё один перевод строки.


Если максимальная длина строки принципиально не ограничена, помочь может либо предварительное посимвольное чтение файла для её определения, либо работа с файлом как с бинарными данными.

Бинарный файл отличается от текстового тем, что необязательно состоит из печатаемых символов со стандартными разделителями между ними. Соответственно, для него не имеет смысла понятие "строки данных", а основной способ работы с ним – чтение и запись наборов байт указанного размера. Основные функции для чтения и записи бинарных данных – fread и fwrite соответственно. В базовой реализации они имеют по 4 параметра:



  • void *buffer - нетипизированный указатель на место хранения данных;

  • size_t (unsigned) size - размер элемента данных в байтах.

  • size_t count - максимальное количество элементов, которые требуется прочитать (записать);

  • FILE *stream - указатель на структуру FILE

Пример. Целочисленный массив a запишем в двоичный файл.

FILE *fp=fopen ("data.dat","wb");

if (fp==NULL) {

puts ("не удалось открыть файл");

getchar(); exit (1);

}

const int n=10;



int a[n];

for(int i=0; i

for (int i=0; i<10; i++) fwrite (&a[i],sizeof(int),1,fp);

//Записали 10 эл-тов по одному

//Если sizeof(int)=2, получим файл из 20 байт, если 4 - из 40

fclose (fp);

Учитывая, что данные массива хранятся в последовательно идущих адресах памяти, цикл for для записи мы могли заменить одним оператором:

fwrite (&a[0],sizeof(int),n,fp);


Подход к чтению данных с помощью fread аналогичен. Например, если файл уже открыт для чтения в режиме "rb":
unsigned char c;

//…


fread (&c,1,1,fp); //читаем по 1 байту
unsigned char buf[512];

//…


fread (&buf,1,512,fp);

//читаем по 1 сектору - по 512 байт


Для файлов, открытых в режиме r+b, разрешены и чтение, и запись (произвольный доступ). Поэтому при работе с такими файлами нужны функции позиционирования файлового указателя:

  • функции fgetpos и ftell позволяют выполнить чтение текущей позиции указателя в файле;

  • функции fseek и fsetpos позволяют осуществить переход к нужной позиции в файле.


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

fseek (fp, 0, SEEK_END); //Встали на 0 байт от конца файла

long int pos;

pos = ftell (fp); //Получили текущую позицию в файле

if (pos<0) puts ("\nОшибка");

else if (!pos) puts ("\nФайл пуст");

else printf ("\nВ файле %ld байт",pos);
Для ввода/вывода через цветную консоль во многих источниках используются методы библиотеки conio.h. Следует учитывать, что её реализации в компиляторах от Borland и Microsoft значительно отличаются, а в компиляторах под Unix/Linux реализации conio.h могут отсутствовать.

Как вариант, в компиляторах Visual Studio можно использовать аналоги conio.h от сторонних разработчиков, например, открытый проект coniow.h (http://sourceforge.net/projects/coniow/files/)

Приведём законченный пример кода, реализующего несложное консольное меню для Visual Studio. Предполагается, что к проекту подключены заголовочный файл coniow.h и файл исходного кода coniow.c.

#include

#include

#include

#include

#include

#include "coniow.h"
typedef void (* FUN) (void);

//Указатель на функцию void функция(void) - они будут выполнять пункты меню

typedef struct ITEM { //Структура для элемента меню

int x,y; //Столбец и строка консоли, куда выводим строку меню

char *str; //Наименование пункта меню

FUN f; //Функция, привязанная к пункту меню

};

typedef struct WINDOW { //Окно вывода для всех функций



int x1,y1,x2,y2,back,color; //Координаты двух углов, фоновый цвет, цвет вывода

};
void Exit () { //Восстановить консоль по умолчанию и выйти

window (1,1,80,25); textbackground(BLACK); textcolor(LIGHTGRAY); clrscr(); exit(0);

}
void flush () { //Очистить буфер клавиатуры

fflush(stdin);

}
void DrawWindow (WINDOW w) { //Нарисовать окно w

char c[]={'+','=','+','!','!','+','=','+'};

window (1,1,80,25); textbackground(w.back); textcolor(w.color);

gotoxy (w.x1-1,w.y1-1);

cprintf ("%c",c[0]);

for (int i=w.x1; i<=w.x2; i++) cprintf ("%c",c[1]);

cprintf ("%c",c[2]);

for (int j=w.y1; j<=w.y2; j++) {

gotoxy (w.x1-1,j); cprintf ("%c",c[3]);

for (int i=w.x1; i<=w.x2; i++) cprintf (" ");

cprintf ("%c",c[4]);

}

gotoxy (w.x1-1,w.y2+1);



cprintf ("%c",c[5]);

for (int i=w.x1; i<=w.x2; i++) cprintf ("%c",c[6]);

cprintf ("%c",c[7]);

}
void DrawMenu (int n,ITEM *m,WINDOW w) { //Управление меню

int sel=0,back=WHITE,inactivecolor=DARKGRAY,activecolor=RED;

DrawWindow (w); //Нарисовать окно

textbackground(back); //Поставить цвет фона меню

for (int i=0; i

gotoxy(m[i].x,m[i].y);

textcolor(inactivecolor);

cprintf ("%s",m[i].str);

}

while (1) { //Бесконечный цикл обработки нажатий клавиш



gotoxy(m[sel].x,m[sel].y);

textcolor(activecolor);

cprintf ("%s",m[sel].str); //Перепечатать активный пункт выделенным цветом

flush(); //очистить буфер клавиатуры

int ch=getch(); //Жд)м нажатия клавиши

gotoxy(m[sel].x,m[sel].y); //Сбросили цвет активного пункта

textcolor(inactivecolor);

cprintf ("%s",m[sel].str);

if (!ch) { //Это расширенный код?

ch=getch(); //Прочитать его

switch(ch) { //Обработка расширенных кодов клавиш

case 72: case 75: if (sel) sel--; else sel=n-1; break; //Стрелки вверх и влево

case 80: case 77: if (sel

}

}



else { //Обработка не-расширенных кодов клавиш

switch(ch) {

case 13: //Нажата Enter

textbackground(w.back); //Поставить фон и цвет окна функций

textcolor(w.color);

window (w.x1,w.y1,w.x2,w.y2); //Выводить будем в окно функций

m[sel].f(); //Вызов функции, закрепленной за пунктом меню

clrscr(); //Убрать, если не очищаем окно после выхода из функции

textbackground(back); //Восстановить фон и окно во весь экран

window (1,1,80,25);

break;

case 27: Exit(); //По Esc всегда выход из приложения



}

}

}



}
//Ниже начинается часть пользователя

void File() { //Функция по меню File - пока ничего не делает

long int i=0;

//Печатать из функций лучше через cprintf

while (!kbhit()) { delay (200); cprintf ("Работа функции File, шаг %ld\r\n",++i); }

}

void Do() { //Функция по меню Do - пока ничего не делает



cprintf ("Введите целое число: ");

int d;


cscanf ("%d",&d); //Вводить из функций лучше через cscanf

cprintf ("\r\nВы ввели значение %d\r\nНажмите клавишу для выхода...",d);

getch(); getch();

}
void main () {

setlocale (LC_ALL,"Russian");

setlocale(LC_CTYPE, ".65001");

SetConsoleCP(65001);

SetConsoleOutputCP(65001);

//Пример для горизонтального меню

ITEM menu1[3]={ //Описали меню

{1,1,"File",File},

{6,1,"Do",Do},

{9,1,"Exit",Exit}

};

WINDOW w={3,3,77,23,BLUE,YELLOW}; //Описали окно вывода функций



clrscr();

DrawMenu (3,menu1,w); //Вызвали главную функцию системы

/*

//Пример для вертикального меню



ITEM menu1[3]={ //Описали меню

{2,2,"File",File},

{2,3,"Do",Do},

{2,4,"Exit",Exit}

};

WINDOW w={8,2,78,24,BLUE,YELLOW}; //Описали окно вывода функций



textbackground (WHITE);

clrscr();

DrawMenu (3,menu1,w); //Вызвали главную функцию системы

*/

}



Задание к лабораторной работе 4. Базовое задание включает две задачи, первая из которых предполагает обработку файла как текстовых данных, вторая – как бинарных. В качестве дополнительной третьей задачи может быть предложена реализация одной из задач 1, 2, содержащая консольный интерфейс и меню.
Материал для чтения из пособия: пп. 7.6-7.11. Обратите внимание на таблицы с описанными прототипами функций ввода/вывода.



Поделитесь с Вашими друзьями:


База данных защищена авторским правом ©vossta.ru 2019
обратиться к администрации

    Главная страница