Файлові потоки


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

Таблиця 1.13.Значення аргументу mode функції fopen()

"r" відкриття файлу без дозволу на модифікацію, файл відкривається лише для читання.
"w" створення нового файлу тільки для запису, якщо файл із вказаним ім'ям вже існує, то він перезапишеться.
"a" відкриття файлу тільки для додавання інформації в кінець файлу, якщо файл не існує, він створюється.
"r+" відкриття існуючого файлу для читання та запису.
"w+" створення нового файлу для читання та запису, якщо файл із вказаним ім'ям вже існує, то він перезаписується.
"a+" відкриває файл у режимі читання та запису для додавання нової інформації у кінець файлу; якщо файл не існує, він створюється.

Класичний підхід, прийнятий в Сі, полягає в тому, що інформація про потік заноситься в структуру FILE, яка визначена у файлі stdio.h. Файл відкривається за допомогою функції fopen, яка повертає покажчик на структуру типу FILE.
typedef struct
{
short level; /*рівень буферу*/
unsigned flags; /*статус файлу */
char fd; /*дескриптор файла*/
char hold; /*попередній символ,якщо немає буферу*/
short bsize; /*розмір буферу*/
unsigned char *buffer; /*буфер передавання даних*/
unsigned char *curp; /*поточний активний покажчик*/
short token; /*перевірка коректності*/
} FILE;
Синтаксис функції fopen() :
FILE *fopen(const char *filename, const char *mode);
Дана функція відкриває файл із заданим ім'ям і зв'язує з ним потік. Аргумент mode вказує режим відкриття файла (таблиця 1.13).
До вказаних специфікаторів в кінці або перед символом "+" може додаватися символ "t" (текстовий файл), або "b" (бінарний, двійковий файл).

1.15.1 Текстові файли
Розглянемо спочатку роботу з текстовими файлами. Відкриття текстового файлу test.txt може мати вигляд :
#include<stdio.h>
void main()
{

FILE *f;
if ((f=fopen("test.txt", "rt"))==NULL)
{
printf("Файл не вдалося відкрити.\n");
return;
}

fclose(f);

}
В даному прикладі змінна f зв'язується з файлом "test.txt", який відкривається як текстовий тільки для читання.
З відкритого таким чином файлу можна читати інформацію. Після закінчення роботи з файлом, його необхідно закрити за допомогою функції fclose().
Якщо файл відкривався би за допомогою fopen("test.txt", "rt+"); , то можна було б не тільки читати, але й записувати в нього інформацію.
З текстового файла можна читати інформацію по рядках, по символах або за форматом.
Записування символу в файловий потік здійснюється функцією putc().
int putc(int ch, FILE *f);
Читання рядка здійснюється за допомогою функції fgets().
char *fgets(char *s,int n,FILE *stream);
У виклику функції fgets() : s - покажчик на буфер, в який читається рядок, n - кількість символів. Читання символу в рядок проходить або до появи символу кінця рядка "\n", або читається n-1 символ. В кінці прочитаного рядка записується нульовий символ.
#include<stdio.h>
#include<string.h>
void main()
{
char s[80];
FILE *f;
if ((f=fopen("1.cpp", "rt"))==NULL)
{
printf("There are an error\n");
return;
}
do
{
fgets(s,80,f);
printf("%s",s);
} while (!feof(f));
fclose(f);
}
Функція feof() перевіряє, чи не прочитаний символ завершення файла. Якщо такий символ прочитаний, то feof() повертає ненульове значення і цикл завершується.
Читання з текстового файлу форматованих даних може здійснюватися функцією fscanf(). Синтаксис :
int fscanf(FILE *stream, const char *format[, address, …]);
Параметр format визначає рядок форматування аргументів, які задаються своїми адресами.
При форматованому читанні можуть виникати помилки у зв'язку з досягненням завершення файлу або невірним форматом записаних у файлі даних. Перевірити, чи успішно пройшло читання даних можна за значенням, яке повертає функція fscanf(). При успішному читанні вона повертає кількість прочитаних полів. Тому читання даних можна організовувати наступним чином :
if (fscanf(f,"%d%d%d",&a,&b,&c)!=3)
{
printf("Помилка читання!\n");
};
Існує також і ряд функцій для запису даних у текстовий файл. Найчастіше використовуються функції fgetc(), fputs() та fprintf().
Функція fgetc() використовується для читання чергового символу з потоку, відкритого функцією fopen().
int fgetc(FILE *f);
Синтаксис функції fprintf() :
int fprintf(FILE *stream, const char *format[,argument,…]);
Вона працює майже мак само, як і функція printf(), але їй потрібний додатковий аргумент для посилання на файл. Він є першим у списку аргументів. Наводимо приклад, який ілюструє звертання до наведених вище функцій:
#include<stdio.h>
void main()
{
FILE *fi;
int age;
fi=fopen("age.txt","r"); /* відкриття файла для читання */
fscanf(fi,"%d",&age); /*читання з файла числового значення */
fclose(fi); /* закриття файла */
fi=fopen("data.txt", "a"); /* відкриття файла для додавання інформації в кінець */
fprintf(fi, "Age==%d.\n",age); /* запис рядка в файл */
fclose(fi); /* закриття файла */
}

1.15.2 Двійкові файли
Тепер розглянемо роботу з двійковими файлами. Двійковий файл представляє собою просто послідовність символів. Що саме і в якій послідовності зберігається в двійковому файлі - повинна знати програма.
Двійкові файли мають переваги, порівняно з текстовими при зберіганні числових даних. Операції читання і запису з такими файлами виконуються набагато швидше, ніж з текстовими, так як відсутня необхідність форматування (переведення в текстове представлення та навпаки). Двійкові файли зазвичай мають менший розмір, ніж аналогічні текстові файли. В двійкових файлах можна переміщуватися в будь-яку позицію і читати або записувати дані в довільній послідовності, в той час, як в текстових файлах практично завжди виконується послідовна обробка інформації.
Про те, як відкривати двійкові файли було згадано раніше. Запис і читання в двійкових файлах виконується відповідно функціями fwrite і fscanf.
size_t fwrite(const void *ptr, size_t size, size_t n, FILE*stream);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
В обидві функції повинен передаватися покажчик ptr на дані, які вводяться або виводяться. Параметр size задає розмір в байтах даних, які читаються або записуються.
#include<stdio.h>
#include<conio.h>
struct mystruct
{
int i;
char ch;
};
int main(void)
{
FILE *stream;
struct mystruct s;
if ((stream = fopen("test.txt", "wb")) == NULL)
{
fprintf(stderr, "Неможливо відкрити файл\n");
return 1;
}
s.i = 0;
s.ch = 'A';
fwrite(&s, sizeof(s), 1, stream);
fclose(stream);
return 0;
}
Тепер розглянемо особливості записування і читання рядків.
char s[10];
strcpy(s, "Example");

fwrite(s,strlen(s)+1,sizeof(char),stream);
Записування рядків відбувається посимвольно. В даному прикладі число символів, які записуються - strlen(s)+1 (одиниця додається на нульовий символ в кінці). Читається рядок аналогічно:
fread(s,strlen(s)+1,sizeof(char),stream);
При цьому читання проходить теж посимвольно.
Дуже часто доводиться працювати з рядками різних довжин. В таких випадках можна перед рядком записати у файл ціле число, яке рівне числу символів у рядку.

int i=strlen(s)+1;
fwrite(&i,1,sizeof(int),stream);
fwrite(s,i,1,stream);

fread(&i,1,sizeof(int),stream);
fread(s,i,1,stream)
В усіх наведених вище прикладах читання даних проходило послідовно. Але, працюючи з двійковими файлами, можна організувати читання даних в довільному порядку. Для цього використовується "покажчик файла" (курсор), який визначає поточну позицію у файлі. При читанні даних курсор автоматично зміщується на число прочитаних байтів. Отримати поточну позицію курсору файла можна за допомогою функції ftell().
long ftell(FILE *stream);
А встановлюється поточна позиція курсору у файлі за допомогою функції fseek():
int fseek(FILE *stream, long offset, int whence);
Ця функція задає зміщення на число байтів offset від точки відліку, яка визначається параметром whence. Цей параметр може приймати значення 0, 1, 2 (таблиця 1.14).

Таблиця 1.14. Можливі значення параметра whence функції fseek

Константа whence Точка відліку
SEEK_SET Початок файлу
SEEK_CUR Поточна позиція
SEEK_END Кінець файлу

Якщо задане значення whence=1, то offset може приймати як додатне, так і від'ємне значення, тобто зсув вперед або назад.
Функція rewind() переміщує курсор на початок файлу.
void rewind(FILE *stream);
Те ж саме можна зробити за допомогою функції fseek() :
fseek(stream, 0L, SEEK_SET);
Приклад програми, в якій використовуються описані вище функції :
#include <stdio.h>
long filesize(FILE *stream);
int main(void)
{
FILE *stream;
stream = fopen("test.txt", "w+");
fprintf(stream, "This is a test");
printf("Розмір файла test.txt рівний %ld байт\n",
filesize(stream));
fclose(stream);
return 0;
}
long filesize(FILE *stream)
{
long curpos, length;
curpos = ftell(stream);
fseek(stream, 0L, SEEK_END);
length = ftell(stream);
fseek(stream, curpos, SEEK_SET);
return length;
}

1.15.3 Використання дескрипторів файлів
В мові Сі передбачений ще один механізм роботи з файлами - використання дескрипторів. Файли, які відкриваються таким чином не розраховані на роботу з буферами та форматованими даними.
На початку роботи будь-якої програми відкриваються п'ять стандартних потоків зі своїми дескрипторами.

Таблиця 1.15. Дескриптори стандартних потоків введення-виведення

потік дескриптор  
stdin стандартний вхідний потік
stdout стандартний вихідний потік
stderr стандартний потік повідомлень про помилки
stdaux стандартний потік зовнішнього пристрою
stdprn стандартний потік виведення на принтер

Але будь-яка програма може і явним чином відкривати будь-які файли з дескрипторами.
Функції, які працюють з дескрипторами файлів, описані в модулі io.h.
Файли відкривається функцією open(), яка повертає дескриптор файлу:
int open(const char *path, int access [ , unsigned mode ] );
Параметр path задає ім'я файлу відкриття. Параметр access визначає режим доступу до файлу.
mode є не обов'язковим та задає режим відкриття файла.
Параметр access формується за допомогою операції АБО (|) з переліку прапорців.

O_RDONLY тільки для читання
O_WRONLY тільки для запису
O_RDWR для читання і запису
O_CREAT створення нового файлу
O_TRUNC якщо файл існує, то він стає порожнім
O_BINARY двійковий файл
O_TEXT текстовий файл

Параметр mode може приймати наступні значення

S_IWRITE дозволити запис
S_IREAD дозволити читання

Використання функції fopen() демонструє наступний приклад :
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
int main(void)
{
int handle;
char msg[] = "Hello world";
if ((handle = open("TEST.TXT", O_CREAT | O_TEXT)) == -1)
{
perror("Error:");
return 1;
}
write(handle, msg, strlen(msg));
close(handle);
return 0;
}
Як видно з прикладу, файл, відкритий функцією open() повинен бути закритий за допомогою функції close().
int close(int handle);
Читання і запис даних при роботі з файлами, що визначаються дескрипторами handle, здійснюється функціями write() і read().
int read(int handle, void *buf, unsigned len);
int write(int handle, void *buf, unsigned len);
В наведених функціях buf - покажчик на буфер, з якого записується в файл інформація, або в який читається len байтів з файла.

Буферизація потоків. В мові Сі існує ряд функцій, які дозволяють керувати буферизацією потоків.
Функція setbuf() дозволяє користувачу встановлювати буферизацію вказаного потоку stream. Синтаксис функції setbuf():
void setbuf(FILE *stream, char *buf);
Значення аргументу stream повинне відповідати стандартному або вже відкритому потоку.
Якщо значення аргументу buffer рівне NULL, то буферизацію буде відмінено. Інакше, значення аргументу buffer буде визначати адресу масиву символів довжини BUFSIZ, де BUFSIZ - розмір буфера (константа, визначена в stdio.h).
Визначений користувачем буфер використовується для буферизованого введення/виведення для вказаного потоку stream замість буферу, що виділяється системою по замовчуванню.
Потоки stderr і stdout по замовчуванню небуферизовані, але для них можна встановлювати буферизацію засобами setbuf.
Примітка. Наслідки буферизації будуть непередбаченими, якщо тільки функція setbuf() не викликана зразу вслід за функцією fopen() або fseek() для заданого потоку.
В мові Сі для керування буферизацією потоків існує ще одна функція: setvbuf(). Вона дозволяє користувачу керувати буферизацією та розміром буфера потоку stream. Синтаксис :
int setvbuf(FILE *stream, char *buf, int type, size_t size);
Потік stream повинен відноситися до відкритого потоку.
Якщо значення параметру buf не NULL, то масив, адреса якого задається значенням параметра buf буде використовуватися в якості буфера.
Якщо потік буферизується, значення параметра type визначає тип буферизації. Тип буферизації може бути або _IONBF, або _IOFBF, або _IOLBF.
Якщо тип рівний _IOFBF або _IOLBF, то значення параметра size використовується як розмір буфера.
Якщо тип рівний _IONBF, то потік небуферизований, і значення параметрів size і buf ігноруються.
Допустиме значення параметра size: більше 0 і менше, ніж максимальний розмір цілого (int).
Значення констант _IONBF, _IOFBF та _IOLBF визначені у файлі stdio.h.
_IOFBF 0 /* буферизація на повний об'єм буфера */
_IOLBF 1 /* порядкова буферизація */
_IONBF 2 /* потік не буферизується */
Для примусового виштовхування буферу можна використовувати функцію fflush(). Її синтаксис :
int fflush(FILE *stream);
Дана функція виштовхує вміст буфера, зв'язаного з потоком stream. Потік залишається відкритим. Якщо потік небуферизований, то виклик функції fflush() не викличе ніяких ефектів.
Буфер потоку автоматично виштовхується, коли він заповнюється, коли закривається потік або коли програма завершує своє виконання.
Приклад 1.
#include <stdio.h>
#include<conio.h>
char outbuf[BUFSIZ];
int main(void)
{
clrscr();
setbuf(stdout, outbuf);
puts("This is a test of buffered output.\n\n");
puts("This output will go into outbuf\n");
puts("and won't appear until the buffer\n");
puts("fills up or we flush the stream.\n");
getch();
fflush(stdout);
getch();
return 0;
}
Приклад 2.
#include <stdio.h>
int main(void)
{
FILE *input, *output;
char bufr[512];
input = fopen("file.in", "r+b");
output = fopen("file.out", "w");
if (setvbuf(input, bufr, _IOFBF, 512) != 0)
printf("Помилка встановлення буферизацiї для вхiдного файла\n");
else
printf("Для вхiдного файла встановлено буферизацiю\n");
if (setvbuf(output, NULL, _IOLBF, 132) != 0)
printf("Помилка встановлення буферизацiї для вихiдного файла\n");
else printf("Буфер для вихiдного файла встановлено\n");
fclose(input);
fclose(output);
return 0;
}