Функции malloc() и free(). Ссылки 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Функции malloc() и free(). Ссылки



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

void* malloc(size_t); //функция выделения памяти
void free(void* memblock); //функция освобождения памяти

Здесь size_t – размер выделяемой области памяти в байтах; void* - обобщенный тип указателя, т.е. не привязанный к какому-либо конкретному типу. Рассмотрим работу данных функций на примере выделения памяти для 10 элементов типа double.

Листинг 4.3. Программирование динамического массива.

#include
#include
int main()
{
double* ptd;
ptd = (double *)malloc(10 * sizeof(double));
if(ptd!= NULL)
{
for(int i = 0;i < 10;i++)
ptd[i] = i;
} else printf(“Не удалось выделить память.”);
free(ptd);
return 0;
}

При вызове функции malloc() выполняется расчет необходимой области памяти для хранения 10 элементов типа double. Для этого используется функция sizeof(), которая возвращает число байт, необходимых для хранения одного элемента типа double. Затем ее значение умножается на 10 и в результате получается объем для 10 элементов типа double. В случаях, когда по каким-либо причинам не удается выделить указанный объем памяти, функция malloc() возвращает значение NULL. Данная константа определена в нескольких библиотеках, в том числе в и. Если функция malloc() возвратила указатель на выделенную область памяти, т.е. не равный NULL, то выполняется цикл, где записываются значения для каждого элемента. При выходе из программы вызывается функция free(), которая освобождает ранее выделенную память. Формально, программа написанная на языке С++ при завершении сама автоматически освобождает всю ранее выделенную память и функция free(), в данном случае, может быть опущена. Однако при составлении более сложных программ часто приходится много раз выделять и освобождать память. В этом случае функция free() играет большую роль, т.к. не освобожденная память не может быть повторно использована, что в результате приведет к неоправданным затратам ресурсов ЭВМ.

Использование указателей досталось в "наследство" от языка С. Чтобы упростить процесс изменения параметров в С++ вводится такое понятие как ссылка. Ссылка представляет собой псевдоним (или второе имя), который программы могут использовать для обращения к переменной. Для объявления ссылки в программе используется знак & перед ее именем. Особенность использования ссылок заключается в необходимости их инициализации сразу же при объявлении, например:

int var;
int &var2 = var;

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

Листинг 4.4. Пример использования ссылок.

void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int agr1 = 10, arg2 = 5;
swap(arg1, arg2);
return 0;
}

В данном примере функция swap() использует два аргумента, представляющие собой ссылки на две переменные. Используя имена ссылок a и b, осуществляется манипулирование переменными arg1 и arg2, заданных в основной функции main() и переданных как параметры функции swap(). Преимущество функции swap() (которая использует ссылки, а не указатели на переменные) заключается гарантии того, что функция в качестве аргументов будет принимать соответствующие типы переменные, а не какую-либо другую информацию, и ссылки будут инициализированы корректно перед их использованием. Это отслеживается компилятором в момент преобразования текста программы в объектный код и выдается сообщение об ошибке, если использование ссылок неверно. В отличие от указателей со ссылками нельзя выполнять следующие операции:

• нельзя получить адрес ссылки, используя оператор адреса C++;
• нельзя присвоить ссылке указатель;
• нельзя сравнить значения ссылок, используя операторы сравнения C++;
• нельзя выполнять арифметические операции над ссылкой, например, добавить смещение;

Стек

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

Рис. 4.4. Организация структуры стека

Из рис. 4.4 видно, что каждый объект стека связан с последующим с помощью указателя next. Если указатель next равен NULL, то достигнут конец стека. Особенность работы со стеком заключается в том, что новый объект всегда добавляется в начало списка. Удаление также возможно только для первого элемента. Таким образом, данная структура реализует очередь по принципу «первым вошел, последним вышел». Такой принцип характерен при вызове функций в рекурсии, когда адрес каждой последующей функции записывается в стеке для корректной передачи управления при ее завершении.

Для описания объектов составляющих стек можно воспользоваться структурами, например, следующее определение задает шаблон для описания объекта данных стека:

typedef struct tag_obj {
char name[100];
struct tag_obj* next;
} OBJ;

Здесь поле name – символическое имя объекта, а next – указатель на следующий объект. Зададим указатель top как глобальную переменную со значением равным NULL:

OBJ* top = NULL;

и введем функцию для добавления нового объекта в стек:

void push(char* name)
{
OBJ* ptr = (OBJ *)malloc(sizeof(OBJ));
strcpy(ptr->name,name);
ptr->next = top;
top = ptr;
}

Данная функция в качестве аргумента принимает указатель на строку символов, которые составляют имя добавляемого объекта. Внутри функции инициализируется указатель ptr на новый созданный объект. В поле name записывается переданная строка, а указатель next инициализируется на первый объект. Таким образом, добавленный объект ставится на вершину списка.

Для извлечения объекта из стека реализуется следующая функция:

void pop()
{
if(top!= NULL)
{
OBJ* ptr = top->next;
printf("%s - deleted\n",top->name);
free(top);
top = ptr;
}
}

В данной функции сначала выполняется проверка указателя top. Если он не равен значению NULL, то в стеке имеются объекты и самый верхний из них следует удалить. Перед удалением инициализируется указатель ptr на следующий объект для того, чтобы он был доступен после удаления верхнего. Затем вызывается функция printf(), которая выводит на экран сообщение об имени удаленного объекта. Наконец, вызывается функция free() для удаления самого верхнего объекта, а указатель top инициализируется на следующий объект.

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

void show_stack()
{
OBJ* ptr = top;
while(ptr!= NULL)
{
printf("%s\n",ptr->name);
ptr = ptr->next;
}
}

Работа данной функции реализуется с помощью цикла while, который работает до тех пор, пока указатель ptr не достигнет конца стека, т.е. пока не будет равен значению NULL. Внутри цикла вызывается функция printf() для вывода имени текущего объекта на экран и указатель ptr перемещается на следующий объект.

Рассмотрим применение данных функций в функции main():

int main()
{
char str[100];
for(int i = 0;i < 5;i++) {
sprintf(str,"Object %d",i+1);
push(str);
}
show_stack();
while(top!= NULL) pop();
return 0;
}

Здесь создается стек, состоящий из 5 объектов, с помощью оператора цикла for. Внутри цикла инициализируется переменная str с именем объекта, которая, затем, передается в качестве аргумента функции push(). После вызова функции show_stack() на экране появляются следующие строки:

Object 5
Object 4
Object 3
Object 2
Object 1

Полученные результаты показывают, что последний 5-й добавленный объект находится на вершине стека, а первый – в конце. При вызове функции pop() в цикле while() осуществляется удаление элементов стека из памяти ЭВМ. В результате на экране появляются строки:

Object 5 - deleted
Object 4 - deleted
Object 3 - deleted
Object 2 - deleted
Object 1 – deleted

Таким образом, функция pop() удаляет верхние объекты стека с 5-го по 1-й.

Связные списки

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

Идея связных списков состоит в представлении данных в виде объектов, связанных друг с другом указателями (рис. 4.5).

4.5. Графическая интерпретация связных списков

Здесь *prev и *next – указатели на предыдущий и следующий объекты соответственно; *head и *tail – указатели на первый и последний объекты; *current – указатель на текущий объект, с которым идет работа. Если предыдущего или последующего объекта не существует, то указатели *prev и *next принимают значение NULL. Указатели *head и *tail являются вспомогательными и служат для быстрого перемещения к первому и последнему объекту соответственно. Рассмотрим работу связных списков на примере представления следующей информации.

Строки данной таблицы можно описать с помощью структуры:

typedef struct tag_lib {
char title[100];
char author[100];
int year;
} LIB;

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

typedef struct tag_obj {
LIB lib;
typedef struct tag_obj* prev, *next;
} OBJ;

Здесь *prev и *next – указатели на предыдущую и следующую строки соответственно.

По умолчанию указатели head и tail равны NULL:

OBJ* head = NULL, *tail = NULL;

При добавлении записей выполняется инициализация этих указателей, а также prev и next внутри объектов:

OBJ* add_obj(char* title, char* author, int year)
{
OBJ* current = (OBJ *)malloc(sizeof(OBJ));
strcpy(current->lib.title, title);
strcpy(current->lib.author, author);
current->lib.year = year;
current->prev = tail;
current->next = NULL;
if(tail!= NULL) tail->next = current;
if(head == NULL) head = current;
tail = current;
return current;
}

Данная функция использует три параметра для ввода данных в структуру LIB. В первой строке функции создается новая структура типа OBJ. Во второй, третьей и четвертой строках осуществляется запись информации в структуру LIB. Затем, инициализируются указатели prev и next добавленного объекта. Учитывая, что добавление осуществляется в конец списка, то указатель next должен быть равен NULL, а указатель prev указывать на предыдущий объект, т.е. быть равен указателю tail. В свою очередь, объект, на который указывает указатель tail, становится предпоследним и его указатель next должен указывать на последний объект, т.е. быть равным указателю current. Затем проверяется, является ли добавляемый объект первым (head == NULL), и если это так, то указатель head приравнивается указателю current. Наконец, указатель tail инициализируется на последний объект. Последняя строка функции возвращает указатель на созданный объект.

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

OBJ* del_obj(OBJ* current)
{
if(current == head)
if(current->prev!= NULL) head = current->prev;
else head = current->next;
if(current == tail)
if(current->next!= NULL) tail = current->next;
else tail = current->prev;
if(current->prev!= NULL)
current->prev->next = current->next;
if(current->next!= NULL)
current->next->prev = current->prev;
free(current);
return head;
}

Функция del_obj() в качестве аргумента использует указатель на объект, который следует удалить. Сначала выполняется проверка для инициализации указателя head, в том случае, если удаляется первый объект, на который он указывает. Аналогичная проверка осуществляется для tail. Затем осуществляется проверка: если предыдущий объект относительно текущего существует, то его указатель на следующий объект следует переместить. Аналогичная проверка выполняется и для следующего объекта относительно удаляемого. После настройки всех указателей вызывается функция free() для удаления объекта из памяти и возвращается указатель на первый объект.

Введенные функции в программе можно использовать следующим образом:

int main()
{
OBJ *current = NULL;
int year;
char title[100], author[100];
do
{
printf("Введите название книги: ");
scanf("%s",title);
printf("Введите автора: ");
scanf("%s",author);
printf("Введите год издания: ");
scanf("%d",&year);
current = add_obj(title,author,year);
printf("Для выхода введите 'q'");
} while(scanf("%d",&year) == 1);
current = head;
while(current!= NULL)
{
printf("Title: %s, author %s, year = %d\n",
current->lib.title, current->author.old, current->lib.year);
current = current->next;
}
while(head!= NULL)
del_obj(head);
return 0;
}

Функция main() осуществляет ввод названия книги, автора и года издания в цикле do while(). Там же вызывается функция add_obj() с соответствующими параметрами, которая формирует связанный список на основе введенных данных. Пользователь выполняет ввод до тех пор, пока не введет какой либо символ на последний запрос. В результате цикл завершится, и указатель current передвигается на первый объект. Затем, в цикле while осуществляется вывод информации текущего объекта на экран, а указатель current передвигается на следующий объект. Данная процедура выполняется до тех пор, пока указатель не станет равным NULL, что означает достижение конца списка. Перед выходом из программы связный список удаляется с помощью функции del_obj(), у которой в качестве аргумента всегда используется указатель head. Если данный указатель принимает значение NULL, то это означает, что связный список пуст и не содержит ни одного объекта. После этого программа завершает свою работу.

Бинарные деревья

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

Рис. 4.6. Графическая интерпретация бинарного дерева

Каждый объект бинарного дерева имеет два указателя: на «левый» и «правый» вершины. Самый верхний уровень называется корнем дерева. Если указатели объекта left и right равны NULL, то он называет листом дерева.

При описании структуры каталогов с помощью бинарного дерева можно воспользоваться следующим правилом. Переход по «левым» указателям будет означать список файлов, а переход по «правым» – список каталогов. Например, для описания простой структуры (рис. 4.7), бинарное дерево будет иметь вид, представленный на рис. 4.8.

Рис. 4.7. Структура каталогов Рис. 4.8. Структура бинарного дерева

Объекты, из которых состоит бинарное дерево удобно представить в виде структур. Также как и в связных списках, первая структура будет описывать данные, хранящиеся в вершинах дерева, а вторая представлять связи между вершинами.

typedef struct tag_data {
char name[100];
} DATA;

typedef struct tag_tree {
DATA data;
struct tag_tree* left, *right;
} TREE;
TREE* root = NULL;

Для формирования дерева введем функцию add_node(), которая в качестве аргументов будет принимать указатель на вершину дерева, к которому добавляются новые вершины, имя вершины и тип вершины: левая или правая. Кроме того, данная функция будет возвращать указатель на новую созданную вершину.

TREE* add_node(TREE* node,char* name, TYPE type = LEFT)
{
TREE* new_node = (TREE *)malloc(sizeof(TREE));
if(type == LEFT && node!= NULL) node->left = new_node;
else if(node!= NULL) node->right = new_node;
strcpy(new_node->data.name,name);
new_node->left = NULL;
new_node->right = NULL;
return new_node;
}

Последний аргумент функции имеет тип TYPE, который удобно определить как перечисляемый тип:

typedef enum tag_type {RIGHT, LEFT} TYPE;

и определить параметр по умолчанию LEFT.

Для отображения дерева целесообразно воспользоваться рекурсивными функциями show_next(), которые вызываются из функции show_tree() следующим образом:

void show_next(TREE* node,int off)
{
if(node!= NULL)
{
for(int i=0;i < off;i++) putchar(' ');
printf("%s\n",node->data.name);
show_next(node->left,off);
show_next(node->right,off+1);
}
}
void show_tree()
{
if(root!= NULL)
{
printf("%s\n",root->data.name);
show_next(root->left,0);
show_next(root->right,1);
}
}

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

При завершении программы необходимо удалить созданные объекты дерева. Для этого также удобно воспользоваться рекурсивными функциями. По аналогии введем рекурсивную функцию del_next(), и основную del_tree(), из которой вызывается функция del_next(). Реализация этих функций приведена ниже:

void del_next(TREE* node)
{
if(node!= NULL)
{
del_next(node->left);
del_next(node->right);
printf("node %s - deleted\n",node->data.name);
free(node);
}
}
void del_tree()
{
if(root!= NULL)
{
del_next(root->left);
del_next(root->right);
printf("node %s - deleted\n",root->data.name);
free(root);
}
}

Аргумент функции del_next() используется для перехода к следующим вершинам дерева. В самой функции выполняется проверка: если следующая вершина существует, т.е. указатель не равен NULL, то выполняется просмотр дерева сначала по левой вершине, а затем по правой. При достижении листьев дерева функции del_next() вызываются с аргументом NULL и не выполняют никаких действий, поэтому программа переходит к функции printf(), которая вывод на экран сообщение об имени удаляемой вершины, а затем вызывается функция free() для освобождения памяти, занятой данным объектом. После этого осуществляется переход к предыдущей функции del_next() и описанный процесс повторяется до тех пор, пока не будут удалены все объекты кроме корневого. Корень дерева удаляется непосредственно в функции del_tree(), после чего можно говорить об удалении всего дерева.

Использование описанных функций в функции main() реализуются следующим образом:

int main()
{
root = add_node(NULL,"Root");
TREE* current = add_node(root,"File 1",LEFT);
current = add_node(current,"File 2",LEFT);
current = add_node(root,"Folder 1",RIGHT);
current = add_node(current,"File 11",LEFT);
current = add_node(current,"File 12",LEFT);
current = add_node(root->right,"Folder 2",RIGHT);
current = add_node(current,"File 21",LEFT);
show_tree();
del_tree();
root = NULL;
return 0;
}

Данная функция сначала инициализирует глобальный указатель root как корень дерева и в качестве первого аргумента функции add_node() записывает NULL, что означает, что предыдущего объекта не существует. Затем вводится вспомогательный указатель current, с помощью которого выполняется создание двоичного дерева, описывающее файловую структуру (рис. 4.7). После создания дерева вызывается функция show_tree() для его отображения на экран, затем оно удаляется и программа завершает свою работу.

Контрольные вопросы и задания

1. Для чего предназначены и как задаются указатели в языке С++?

2. Что такое адрес переменной?

3. Объявите целочисленную переменную и проинициализируйте на нее указатель.

4. Чему будет равно значение указателя int* ptr = 0; после выполнения операции ptr++?

5. Каким образом можно задавать указатель на массив?

6. Для чего предназначена функция malloc()?

7. Запишите программу копирования одной строки в другую с помощью указателей на эти строки.

8. Что делает функция free() и в какой библиотеке она определена?

9. Какие операции с указателями допустимы?

10. Опишите структуру стека.

11. Объясните принцип работы функции вывода на экран элементов стека.

12. Дайте понятие связного списка.

13. Какие удобства хранения информации представляет связный список по сравнению с массивом.

14. Объясните работу функцию удаления элементов связного списка.

15. Как в программе описывается объект связного списка?

16. Опишите структуру бинарного дерева.

17. Какой тип информации удобно представлять с помощью бинарных деревьев?

18. Объясните принцип работы рекуррентных функций для отображения и удаления элементов бинарного дерева.

Работа с текстовыми файлами

Для работы с файлами в языке С++ имеется набор функций, определенных в библиотеке stdio.h. Перед началом работы с файлом его следует открыть, что достигается с помощью функции fopen(), имеющей следующий синтаксис:

FILE *fopen(const char *filename, const char *mode);

Здесь filename – строка, содержащая путь и имя файла; mode – строка, определяющая режим открытия файла: на чтение или на запись; FILE – специальный тип данных для работы с файлами. Данная функция возвращает значение NULL, если файл не был успешно открыт, иначе – другое значение. Рассмотрим последовательность действий по созданию простого текстового файла на языке C++ и записи в него текстовой информации.

Листинг 5.1. Запись текстовой информации в файл.

#include
int main()
{
char str_file[]=”Строка для файла”;
FILE* fp = fopen(“my_file.txt”,”w”);
if(fp!= NULL)
{
printf(“Идет запись информации в файл…\n”);
for(int i=0;i < strlen(str_file);i++)
putc(str_file[i],fp);
}
else printf(“Невозможно открыть файл на запись.\n”);
fclose(fp);
return 0;
}

В данном примере задается специализированный указатель fp типа FILE, который инициализируется функцией fopen(). Функция fopen() в качестве первого аргумента принимает строку, в которой задан путь и имя файла. Вторым параметром определяется способ обработки файла, в данном случае, значение “w”, которое означает открытие файла на запись с удалением всей прежней информации из него. Если файл открыт успешно, то указатель fp не будет равен NULL и с ним возможна работа. В этом случае с помощью функции putc() выполняется запись символов в файл, на который указывает указатель fp. Перед завершением программы открытый файл следует закрыть во избежание в нем потери данных. Это достигается функцией fclose(), которая принимает указатель на файл и возвращает значение 0 при успешном закрытии файла, иначе значение EOF.

Рассмотрим теперь пример программы считывания информации из файла.

Листинг 5.2. Считывание текстовой информации из файла.

#include
int main()
{
char str_file[100];
FILE* fp = fopen(“my_file.txt”,”r”);
if(fp!= NULL)
{
int i=0;
char ch;
while((ch = getc(fp))!= EOF)
str_file[i++]=ch;
str_file[i] = ‘\0’;
printf(str_file);
}
else printf(“Невозможно открыть файл на чтение.\n”);
fclose(fp);
return 0;
}

В приведенном листинге функция fopen() открывает файл на чтение, что определяется значением второго аргумента равного «r». Это значит, что в него невозможно произвести запись данных, а только считывание. Сначала выполняется цикл while, в котором из файла считывается символ с помощью функции getc() и выполняется проверка: если считанное значение не равно символу конца файла EOF, то значение переменной ch записывается в массив str_file. Данный цикл будет выполняться до тех пор, пока не будут считаны все символы из файла, т.е. пока не будет достигнут символ EOF. После завершения цикла формируется строка str_file, которая выводится на экран с помощью функции printf(). Перед завершением программы также выполняется функция закрытия файла fclose().

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

Листинг 5.3. Использование функций fpust() и fgets().

#include
int main()
{
char str_file[]=”Строка для файла”;
FILE* fp = fopen(“my_file.txt”,”w”);
if(fp!= NULL) fputs(str_file,fp);
else printf(“Невозможно открыть файл на запись.\n”);
fclose(fp);
fp = fopen(“my_file.txt”,”r”);
if(fp!= NULL)
{
fgets(str_file,sizeof(str_file),fp);
printf(str_file);
}
fclose(fp);
return 0;
}

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

Листинг 5.4. Использование функций fprintf() и fscanf().

#include
#define N 2
struct tag_book
{
char name[100];
char author[100];
int year;
} books[N];

int main(void)
{
for(int i=0;i < N;i++)
{
scanf("%s",books[i].name);
scanf("%s",books[i].author);
scanf("%d",&books[i].year);
}

for(i=0;i < N;i++)
{
puts(books[i].name);
puts(books[i].author);
printf("%d\n",books[i].year);
}

for(i=0;i < N;i++)
fprintf(fp,"%s %s %d\n",books[i].name,books[i].author,
books[i].year);
fclose(fp);
fp = fopen("my_file.txt","r");
for(i=0;i < N;i++)
fscanf(fp,"%s %s %d\n",books[i].name,books[i].author,
&books[i].year);
fclose(fp);
printf("------------------------------------------------\n");
for(i=0;i < N;i++)
{
puts(books[i].name);
puts(books[i].author);
printf("%d\n",books[i].year);
}
return 0;
}

При выполнении данной программы вводится информация по книгам в массив структур books и выводится введенная информация на экран. Затем открывается файл my_file.txt на запись, в который заносится информация по книгам в порядке: наименование, автор, год издания. Так как число книг в данном случае равно двум, то выходной файл будет выглядеть следующим образом:

Onegin Pushkin 1983
Oblomov Griboedov 1985

Затем, файл my_file.txt открывается на чтение и с помощью функции scanf() осуществляется считывание информации в элементы структуры. В заключении считанная информация выводится на экран монитора.

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

При внимательном рассмотрении предыдущих примеров можно заметить, что функции считывания информации из файла «знают» с какой позиции следует считывать очередную порцию данных. Действительно, в последнем примере функция fscanf(), вызываемая в цикле, «знает» что нужно считать сначала первую строку из файла, затем вторую и т.д. И программисту нет необходимости задавать позицию для считывания данных. Все происходит автоматически. Вследствие чего появляется такая особенность работы? Дело в том, что у любого открытого файла в программе написанной на С++ имеется указатель позиции (номера), с которой осуществляется считывание данных из файла. При открывании файла на чтение номер этой позиции указывает на начало файла. Поэтому функция fscanf(), вызванная первый раз, считывает данные первой строки. По мере считывания информации из файла, позиция сдвигается на число считанных символов. И функция fscanf() вызванная второй раз будет работать уже со второй строкой в файле. Несмотря на то, что указатель позиции в файле перемещается автоматически, в языке С++ имеются функции fseek() и ftell(), позволяющие программно управлять положением позиции в файле. Синтаксис данных функций следующий:

int fseek(FILE *stream, long offset, int origin);
long ftell(FILE *stream);

где *stream – указатель на файл; offset – смещение позиции в файле (в байтах); origin – флаг начального отсчета, который может принимать значения: SEEK_END – конец файла, SEEK_SET – начало файла; SEEK_CUR – текущая позиция. Последняя функция возвращает номер текущей позиции в файле.

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

Листинг 5.5. Использование функций fseek() и ftell().

#include
int main(void)
{
FILE* fp = fopen("my_file.txt","w");
if(fp!= NULL)
{
fprintf(fp,"It is an example using fseek and ftell functions.");
}
fclose(fp);
fp = fopen("my_file.txt","r");
if(fp!= NULL)
{
char ch;
fseek(fp,0L,SEEK_END);
long length = ftell(fp);
printf("length = %ld\n",length);
for(int i = 1;i <= length;i++)
{
fseek(fp,-i,SEEK_END);
ch = getc(fp);
putchar(ch);
}
}
fclose(fp);
return 0;
}

В данном примере сначала создается файл, в который записывается строка “It is an example using fseek and ftell functions.”. Затем этот файл открывается на чтение и с помощью функции fseek(fp,0L,SEEK_END) указатель позиции помещается в конец файла. Это достигается за счет установки флага SEEK_END, который перемещает позицию в конец файла при нулевом смещении. В результате функция ftell(fp) возвратит число символов в открытом файле. В цикле функция fseek(fp,-i,SEEK_END) смещает указатель позиции на –i символов относительно конца файла, после чего считывается символ функцией getc(), стоящий на i-й позиции с конца. Так как переменная i пробегает значения от 1 до length, то на экран будут выведены символы из файла в обратном порядке.

Работа с бинарными файлами

Следует отметить, что во всех рассмотренных выше примерах функция fopen() в режимах “r” и “w” открывает текстовый файл на чтение и запись соответственно. Это означает, что некоторые символы форматирования текста, например возврат каретки ‘\r’ не могут быть считаны как отдельные символы, их как бы не существует в файле, но при этом они там есть. Это особенность текстового режима файла. Для более «тонкой» работы с содержимом файлов существует бинарный режим, который представляет содержимое файла как последовательность байтов где все возможные управляющие коды являются просто числами. Именно в этом режиме возможно удаление или добавление управляющих символов недоступных в текстовом режиме. Для того чтобы открыть файл в бинарном режиме используется также функция fopen() с последним параметром равным “rb” и “wb” соответственно для чтения и записи. Продемонстрируем особенности обработки бинарного файла на примере подсчета числа управляющих символов возврата каретки ‘\r’ в файле, открытый в текстовом режиме и бинарном.

Листинг 5.6. Программа подсчета числа символов ‘\r’ в файле.

#include
int main(void)
{
FILE* fp = fopen("my_file.txt","w");
if(fp!= NULL)
{
fprintf(fp,"It is\nan example using\nan binary file.");
}
fclose(fp);
char ch;
int cnt = 0;
fp = fopen("my_file.txt","r");
if(fp!= NULL)
{
while((ch = getc(fp))!= EOF)
if(ch == '\r') cnt++;
}
fclose(fp);
printf("Text file: cnt = %d\n",cnt);
cnt=0;
fp = fopen("my_file.txt","rb");
if(fp!= NULL)
{
while((ch = getc(fp))!= EOF)
if(ch == '\r') cnt++;
}
fclose(fp);
printf("Binary file: cnt = %d\n",cnt);
return 0;
}

Результат работы будет следующий:

Text file: cnt = 0
Binary file: cnt = 2

Анализ полученных данных показывает, что при открытии файла в текстовом режиме, символы возврата каретки ‘\r’ не считываются функцией getc(), а в бинарном режиме доступны все символы.

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

Для работы с бинарными файлами предусмотрены функции fread() и fwrite() со следующим синтаксисом:

size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

где *buffer – указатель на буфер памяти, в который будут считываться данные из файла; size – размер элемента в байтах; count - число считываний элементов; *stream – указатель на файл.

size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);

где *buffer – указатель на буфер памяти, из которого будут считываться данные в файл; size – размер элемента в байтах; count - число записей; *stream – указатель на файл.

Приведем пример использования функций fwrite() и fread().

Листинг 5.7. Использование функций fwrite() и fread().

#include
void main(void)
{
FILE *stream;
char list[30];
int i, numread, numwritten;
if((stream = fopen("fread.out", "wb"))!= NULL)
{
for (i = 0; i < 25; i++)
list[i] = (char)('z' - i);
numwritten = fwrite(list, sizeof(char), 25, stream);
printf("Wrote %d items\n", numwritten);
fclose(stream);
}
else printf("Problem opening the file\n");
if((stream = fopen("fread.out", "rb"))!= NULL)
{
numread = fread(list, sizeof(char), 25, stream);
printf("Number of items read = %d\n", numread);
printf("Contents of buffer = %.25s\n", list);
fclose(stream);
}
else printf("File could not be opened\n");
}

В данном примере массив list выступает в качестве буфера для вывода и ввода информации из бинарного файла. Сначала элементы буфера инициализируются буквами латинского алфавита от z до b, а затем записываются в файл с помощью функции fwrite(list, sizeof(char), 25, stream). Здесь оператор sizeof(char) указывает размер элемента (буквы), а число 25 соответствует числу записываемых букв. Аналогичным образом осуществляется считывание информации из файла fread(list, sizeof(char), 25, stream), где в массив list помещаются 25 символов, хранящихся в файле.

Функции fwrite() и fread() удобно использовать при сохранении данных структуры в файл. Запишем пример хранения информации по двум книгам в бинарном файле.

Листинг 5.8. Пример сохранения структур в бинарном файле.

#include
#define N 2
struct tag_book
{
char name[100];
char author[100];
int year;
} books[N];

int main(void)
{
for(int i=0;i < N;i++)
{
scanf("%s",books[i].name);
scanf("%s",books[i].author);
scanf("%d",&books[i].year);
}
FILE* fp = fopen("my_file.txt","wb");
fwrite(books, sizeof(books),1,fp);
fclose(fp);

fp = fopen("my_file.txt","rb");
fread(books,sizeof(books),1,fp);
fclose(fp);
printf("------------------------------------------------\n");
for(i=0;i < N;i++)
{
puts(books[i].name);
puts(books[i].author);
printf("%d\n",books[i].year);
}
return 0;
}

В данном примере с помощью функции fwrite() целиком сохраняется массив books, состоящий из двух элементов, а оператор sizeof(books) определяет размер массива books. Аналогичным образом реализуется и функция fread(), которая считывает из файла сразу весь массив. По существу функции fwrite() и fread(), в данном примере, осуществляют копирование заданной области памяти в файл, а затем обратно. Это их свойство удобно использовать при хранении «сложных» форм данных, когда простая поэлементная запись данных в файл становится трудоемкой или невозможной.

Следует отметить, что функция fopen() при открытии файла на запись уничтожает все данные из этого файла, если они были. Вместе с тем существует необходимость добавлять данные в файл, не уничтожая ранее записанную информацию. Это достигается путем открытия файла на добавление информации. В этом случае функции fopen() третьим аргументом передается строка “a” или “ab”, что означает открыть файл на добавление информации в его конец. Продемонстрируем работу данного режима на следующем примере.

Листинг 5.9. Добавление информации в файл.



Поделиться:


Последнее изменение этой страницы: 2017-02-05; просмотров: 344; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.119.133.228 (0.111 с.)