Пособие
.pdfЗ відкритого таким чином файлу можна читати інформацію. Після закінчення роботи з файлом, його необхідно закрити за допомогою функції 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 символ. У кінці прочитаного рядка записується нульовий символ
[16].
#include <stdio.h> #include <string.h> #include <conio.h> int main()
{
char s[80]; FILE *f;
if ((f=fopen("1.cpp", "rt"))==NULL)
{
printf("There are an error\n"); return 0;
_getch();
}
do
{
fgets(s,80,f);
printf("%s",s); } while (!feof(f));
fclose(f);
141
_getch(); return 0;
}
Функція 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(), але їй потрібний додатковий аргумент для посилання на файл. Він є першим у
142
списку аргументів. Наведемо приклад, що ілюструє звертання до наведених вище функцій.
Приклад.
#include<stdio.h> #include <conio.h> int 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); /* закриття файлу */ _getch();
return 0;
}
18.2 Двійкові файли
Розглянемо роботу з двійковими файлами. Двійковий файл представляє собою просто послідовність символів. Що саме і в якій послідовності зберігається в двійковому файлі - повинна знати програма.
Двійкові файли мають переваги порівняно з текстовими при зберіганні числових даних. Операції читання і запису з такими файлами виконуються набагато швидше, ніж з текстовими, так як відсутня необхідність форматування (переведення в текстове представлення та навпаки). Двійкові файли зазвичай мають менший розмір, ніж аналогічні текстові файли. В двійкових файлах можна переміщуватися в будь-яку позицію і читати або записувати дані в довільній послідовності, в той час, як в текстових файлах практично завжди виконується послідовна обробка інформації.
143
Запис і читання в двійкових файлах виконується відповідно фун-
кціями fwrite і fread.
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);
_getch(); return 0;
}
Тепер розглянемо особливості записування і читання рядків.
char s[10];
strcpy(s, "Example");
144
fwrite(s, sizeof(char),strlen(s)+1, stream);
У даному прикладі число символів, що записуються – strlen(s)+1 (одиниця додається на нульовий символ в кінці).
Дуже часто доводиться працювати з рядками різних довжин. У таких випадках можна перед рядком записати у файл ціле число, що рівне числу символів у рядку.
…
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 (табл. 18.2).
|
|
Таблиця 18.2. |
Можливі значення параметра whence функції fseek() |
||
Константа |
whence |
Точка відліку |
SEEK_SET |
0 |
Початок файлу |
SEEK_CUR |
1 |
Поточна позиція |
SEEK_END |
2 |
Кінець файлу |
|
|
145 |
Якщо задане значення whence=1, то offset може приймати як додатне, так і від’ємне значення, тобто зсув уперед або назад.
Функція rewind() переміщує курсор на початок файлу.
void rewind(FILE *stream);
Те ж саме можна зробити за допомогою функції fseek():
fseek(stream, 0L, SEEK_SET);
Приклад програми, в якій використовуються описані вище функції:
#include <stdio.h> #include <conio.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;
}
146
19 ІНДИВІДУАЛЬНІ ЗАВДАННЯ ДО П. 18
Необхідно організувати файл даних із структурою взятої з попереднього індивідуального завдання та передбачити функції, що дозволяють :
–коригування обраного запису файлу;
–пошук інформації за різними полями;
–додавання записів у кінець файлу;
–вилучення інформації з файлу;
–перегляд вмісту файлу.
147
20ДИНАМІЧНІ СТРУКТУРИ ДАНИХ
20.1Загальні відомості про динамічні структури даних
Будь-яка програма призначена для опрацювання даних, від спо-
собу організації яких залежать алгоритми роботи, тому вибір структур даних повинен передувати створенню алгоритмів. Вище були розглянуті стандартні способи організації даних, що надаються мовою C, основні і складені типи. Найбільш часто в програмах використовуються масиви, структури та їх поєднання, наприклад, масиви структур, полями яких є масиви і структури [11].
Пам’ять під дані виділяється або на етапі компіляції (у цьому випадку необхідний обсяг повинен бути відомий до початку виконання програми, тобто заданий у вигляді константи), або під час виконання програми за допомогою функції mallос (необхідний обсяг повинен бути відомий до розподілу пам’яті). В обох випадках виділяється безперервна ділянка пам’яті.
Якщо до початку роботи з даними неможливо визначити, скільки пам’яті потрібно для їх зберігання, пам’ять виділяється окремими блоками, зв’язаними один з одним за допомогою покажчиків. Такий спосіб організації даних називається динамічними структурами даних, оскільки їх розмір змінюється під час виконання програми. З динамічних структур у програмах найчастіше використовуються лінійні списки, стеки, черги і бінарні дерева. Вони розрізняються способами зв’язку окремих елементів і допустимими операціями. Динамічна структура може займати несуміжні ділянки оперативної пам’яті.
Динамічні структури широко застосовують і для більш ефективної роботи з даними, розмір яких відомий, особливо для вирішення завдань сортування, оскільки упорядкування динамічних структур не вимагає перестановки елементів, а зводиться до зміни покажчиків на ці елементи. Наприклад, якщо в процесі виконання програми потрібно багато разів впорядковувати великий масив даних, має сенс організувати його у вигляді лінійного списку. При вирішенні завдань пошуку елемента в тих випадках, коли важлива швидкість, дані найкраще представити у вигляді бінарного дерева.
Елемент будь-якої динамічної структури даних являє собою структуру (в смислі struct), що містить принаймні два поля: для зберігання даних і для покажчика. Полів даних і покажчиків може бути декілька. Поля даних можуть бути будь-якого типу: основного,
148
складеного або типу покажчик. Опис найпростішого елемента (вузла) виглядає наступним чином:
struct Node
{
Data d; // тип даних Data повинен // бути визначено заздалегідь
Node *р:
};
Розглянемо реалізацію основних операцій з динамічними структурами даних.
20.2 Лінійні списки
Найпростіший спосіб зв’язати безліч елементів - зробити так, щоб кожен елемент містив посилання на наступний. Такий список називається односпрямованим (однозв’язним). Якщо додати в ко-
жен елемент ще одне посилання на попередній елемент, вийде двоспрямований список (двозв’язний), якщо останній елемент зв’язати покажчиком з першим, вийде кільцевий список.
Кожен елемент списку містить ключ, що ідентифікує цей елемент. Ключ зазвичай буває або цілим числом, або рядком і є частиною поля даних.
В якості ключа в процесі роботи зі списком можуть виступати різні частини поля даних. Наприклад, якщо створюється лінійний список із записів, що містять прізвище, рік народження, стаж роботи та стать, будь-яка частина запису може виступати в якості ключа: при впорядкування списку за алфавітом ключем буде прізвище, а при пошуку, наприклад, ветеранів праці ключем буде стаж . Ключі різних елементів списку можуть збігатися.
Над списками можна виконувати такі операції:
-початкове формування списку (створення першого елемента);
-додавання елемента в кінець списку;
-читання елемента із заданим ключем;
-вставка елемента в задане місце списку (до або після елемента із заданим ключем);
-видалення елемента із заданим ключем;
-впорядкування списку за ключем.
Схематично лінійний односпрямований список виглядає так :
149
Рис. 20.1 Схематичне зображення односпрямованого лінійного списку
Реалізація основних операцій :
1. Включення елемента в початок списку.
val next
val next |
val next |
|
...
first
Рис. 20.2 Схема дії операції включення елемента в початок списку
list *addbeg(list *first, elemtype x)
{
list *n_sp;
n_sp = (list *) malloc(sizeof(list)); n_sp->val=x;
n_sp->next=first; first=n_sp; return first;
}
2. Видалення елемента з початку списку.
Рис. 20.3. Схема дії операції видалення елемента
150