ofstream File ( "E:\\test.txt" ); - создали поток и открыли файл
После открытия файла необходимо обязательно проверить открылся ли файл.
Если файл открыть не удалось, то переменная потока (File) принимает значение false, если файл открыт – true. Следовательно, проверку успешного открытия файла можно выполнит так:
if ( ! File)
// Ошибка
Еще один способ – использовать функцию потока is_open (), которая также возвращает логическое значение в зависимости от результата операции открытия файла:
if ( ! File.is_open () )
// Ошибка
Файл закрывается с помощью функции потока close ():
File.close ();
Подробнее об открытии файла. Каждый поток использует свой вариант функции open. Их прототипы выглядят так:
void ifstream::open ( const char * FileName,
ios::openmode Mode = ios::in );
void ofstream::open ( const char * FileName,
ios::openmode Mode = ios::out | ios::trunc );
void fstream::open ( const char * FileName,
ios::openmode Mode = ios::in | ios::out );
Первый параметр определяет имя открываемого файла (представляет собой массив символов).
Второй параметр определяет режим открытия файла. Этот параметр имеет значение по умолчанию, то есть является необязательным. Возможные значения этого параметра:
· ios::app – при открытии файла на запись (поток ofstream) обеспечивает добавление всех выводимых в файл данных в конец файла;
· ios::ate – обеспечивает начало поиска данных в файле начиная с конца файла;
· ios::in – файл открывается для чтения из него данных;
· ios::out – файл открывается для записи данных в файл;
· ios::binary – открытие файла в двоичном режиме (по умолчанию все файлы открываются в текстовом режиме);
· ios::trunc – содержимое открываемого файла уничтожается (его длина становится равной 0).
Эти флаги можно комбинировать с помощью побитовой операции ИЛИ (|).
Если файл открывается без использования функции open, эти флаги тоже можно использовать:
fstream File ( "E:\\test.txt", ios :: out | ios :: app ); - файл открывается на вывод с добавлением записываемых данных в конец файла.
Запись и чтение данных в текстовых файлах
Запись и чтение данных в текстовых файлах ничем не отличается от способов ввода-вывода данных с помощью потоков cin и cout. Методы форматирования вывода и вводы данных остаются такими же (флаги форматирования, манипуляторы, функции потоков).
Необходимо помнить, что при использовании операции >> для чтения данных из текстовых файлов, процесс чтения останавливается при достижении пробельного символа (так же как и в потоках cin и cout). Поэтому для чтения строки текста, содержащей несколько слов, необходимо, как и раньше, использовать, например, функцию getline ().
Запись и чтение данных в двоичном режиме
Для работы с файлом в двоичном режиме его необходимо открыть с флагом ios :: binary.
Чтение и запись двоичных данных в этом режиме можно осуществлять двумя способами:
· по одному байту – функции файловых потоков get () и put ();
· блокам определенной длины - функции файловых потоков read () и write ().
Один из вариантов прототипов функций get () и put () (чаще всего используемый) выглядит так:
ifstream & get (char & ch);
ofstream & put (char ch);
Функция get () берет один символ из потока ввода, помещает его в символьный параметр ch и возвращает ссылку на поток ввода. Когда достигается конец файла, значение ссылки на поток становится равным 0.
Функция put () помещает символ ch в поток вывода и возвращает ссылку на поток вывода.
В следующей программе с помощью этих функций осуществляется запись в файл массива А из 5 целых чисел, чтение из файла этих данных в массив В и вывод массива В на экран:
int main ()
{
setlocale (0, "");
// Запись массива А в_файл "E:\test.dat"
ofstream o_File; // Создали поток вывода для записи данных в файл
Первый параметр этих функций определяет адрес буфера (некоторого массива символов - байт) для чтения или записи данных в соответствующий файловый поток. Второй параметр задает количество символов – байт, которые необходимо взять из потока или записать в поток (тип данных streamsize – целый тип данных). Размер буфера должен соответствовать значению второго параметра.
Функция write () обеспечивает запись из буфера, адрес которого указан в первом параметре функции, n символов данных в поток вывода и возвращает ссылку на поток.
Функция read () обеспечивает запись из потока ввода n символов данных в память по адресу, указанному в первом параметре buf. При достижении конца файла функция возвращает ссылку на поток, равную 0, а фактическое количество взятых из потока символов может быть меньше, чем значение n второго параметра (буфер заполнен не полностью). Фактическое количество считанных из потока ввода символов после выполнения последней операции чтения можно определить с помощью функции потока ввода gcount(). Следующий пример иллюстрирует использование функций блочного чтения и записи данных из произвольного файла.
void FileToScr (char * FileName)
{
fstream File ( FileName, ios::in | ios::binary );
if ( !File ) // Проверили удалось ли открыть файл
{
cout << "Открыть файл не удалось! \n" ;
return;
}
char Buf [1024];
do
{
File.read ( Buf, sizeof ( Buf ) ); // Читаем данные из файла в буфер
Функция FileToScr () обеспечивает чтение любого указанного файла в двоичном режиме и вывод его на экран в символьном виде с использованием функций блочного чтения и записи. Этот пример показывает, что функции блочного чтения и записи применимы и к стандартным потокам cin и cout (впрочем, как и функции get () и put ()). Ключевым моментом здесь является использование функции потока ввода gcount (). С помощью этой функции удается точно определить количество символов, которое необходимо вывести на экран из буфера после очередной операции чтения из файла.
Как обнаружить конец файла?
При чтении данных из файла обычно используются циклические алгоритмы. При этом возникает задача определить сколько раз необходимо выполнить чтение из файла, чтобы прочитать все данные и не выйти за пределы файла.
Обнаружить конец файла при чтении данных позволяет функция потока:
bool eof ();
Эта функция не имеет параметров и возвращает значение true, когда достигается конец файла. Пример использования этой функции можно увидеть в одном из предыдущих примеров.
Другой способ определения конца файла основан на том, что функции чтения данных из файла (get (), read ()) при достижении конца файла возвращают нулевую ссылку на поток. Этот способ иллюстрируется в последнем примере.
Прямой доступ при работе с файлами
Во всех предыдущих примерах чтение и запись данных в файлы осуществлялась последовательно. Однако существуют методы, обеспечивающие возможность произвольного доступа к любому байту файла для осуществления записи или чтения данных именно в эту точку файла.
Поддержка этих методов основана на понятии указателя позиции в файле (или просто – указателя). В языке C++ поддерживается работа с двумя указателями. Один указатель (get-указатель) определяет место в файле, откуда должны быть прочитаны данные. Другой – put-указатель определяет место для записи данных. При выполнении операций чтения-записи соответствующие указатели автоматически перемещаются в файле на расстояние, равное объему прочитанных или записанных данных, тем самым подготавливая следующую операцию чтения или записи.
Замечание. Рассматриваемые далее методы ориентированы на использование в двоичном режиме работы с файлами. В текстовом режиме позиционирование указателей может оказаться не точным.
Перемещение указателей в потоках осуществляется с помощью соответствующих потоковых функций:
Перемещение соответствующего указателя осуществляется на offset байт (тип данных off_type – целочисленный тип данных, может принимать отрицательные и положительные значения) относительно точки, задаваемой параметром way.
Тип данных seekdir параметра way имеет три возможных значения:
· ios :: beg – начало файла;
· ios :: cur – текущая позиция;
· ios :: end – конец файла.
Существуют перегруженные функции позиционирования указателей, обеспечивающие абсолютную адресацию указателя в заданную позицию файла:
· ifsnteam & seekg ( pos_type pos ); - перемещение get-указателя в позицию pos;
· ofsnteam & seekp ( pos_type pos ); - перемещение put-указателя в позицию pos.
Тип данных pos_type является целочисленным беззнаковым типом. Позиция 0 соответствует началу файла.
Определить текущие позиции указателей можно с помощью следующих потоковых функций:
pos_type tellg (); - возвращает текущую позицию get-указателя;
pos_type tellp (); - возвращает текущую позицию put-указателя;
В качестве иллюстрации напишем функцию, возвращающую размер в байтах заданного файла:
Потоки ввода-вывода после выполнения каждой операции ввода-вывода находятся в определенном состоянии, характеризующимся, так называемым, статусом потока. Статус потока ввода-вывода представляет собой перечисление, имеющее следующие значения:
Значение статуса после завершения очередной операции ввода-вывода можно узнать с помощью функции rdstate (), которая возвращает значение одного из перечисленных выше значений:
fstream File (……..);
while (File.rdstate () = ios :: goodbit)
{
// Выполняем очередную операцию с файлом
}
Еще один способ узнать значение статуса потока ввода-вывода состоит в использовании одной из следующих функций потока, возвращающих значение true, при установке соответствующего флага статуса:
· bool good ()
· bool eof () – эту функцию мы уже использовали раньше
· bool fail ()
· bool bed ()
Например:
fstream File (……..);
while ( File.good () )
{
// Выполняем очередную операцию с файлом
}
Если после выполнения очередной операции ввода-вывода установлен статус потока отличающийся от ios :: goodbit, дальнейшее выполнение операций по работе с потоком может стать невозможным. Для попытки продолжения работы с потоком в этом случае следует воспользоваться потоковой функцией clear (). Эта функция очищает статус потока ввода-вывода и устанавливает флаг статуса ios :: goodbit.
Некоторые другие функции управления потоками ввода-вывода
Функция потока ввода peek () позволяет прочитать из потока очередной символ, не удаляя его из потока.
Функция putback (char c) возвращает в поток ввода символ c.
При выводе данных на некоторое устройство эти данные сначала накапливаются в буфере вывода и, когда этот буфер заполняется, сбрасываются на устройство (например, запись на диск). Этот прием называется буферизацией данных. Он позволяет существенно увеличить быстродействие операций вывода данных. Однако в некоторых ситуациях требуется осуществлять запись данных на устройство, не дожидаясь заполнения буфера. Для этого можно использовать функцию потока вывода flush ().
Примеры по работе с файлами
Пример 1. Для выполнения этой программы скопируйте ее текст в новый проект.
//////////////////////////////////
// Эта программа предназначена для иллюстрации работы с файлами в двоичном режиме.
// Обеспечивает:
// 1. Ввод и добавление в файл Persons.dat сведений о людях.
// 2. Чтение данных из файла Persons.dat и вывод их на экран.
// Данные о людях (персонах) включают в себя фамилию, имя и год рождения,
// оформленные в виде структуры t_Person.
// Различного рода запросы, требующие ответа "да" или "нет", реализуются
// с помощью функции MessageBox() из заголовочного файла <windows.h>.
// Эта функция возвращает целочисленные значения 6 и 7, означающие "да" или "нет"
// соответственно.
// Для очистки экрана в программе используется функция system("cls"), которая
// выполняет команду операционной системы cls - очистка экрана.
// Для русификации консольного ввода-вывода используются потоки консольного
// ввода-вывода wcin и wcout, а вместо типа char двухбайтные символы типа wchar_t
// и кодовая страница .866, устанавливаемая в главной функции с помощью
// функции setlocale(LC_ALL, ".866")
////////////////////////////////////
#include "stdafx.h"
#include <iostream> // Для работы со стандартными потоками ввода-вывода
#include <fstream> // Для работы с файлами
#include <windows.h> // Для использования функции MessageBox()
#include <iomanip> // Для использования функции setw()
using namespace std;
bool FileExists ( char *FileName)
// Возвращает true, если файл FileName существует
{
ifstream File ( FileName ); // Открываем файл для чтения
bool Fl = (bool)File; // Фиксируем результат открытия файла в переменной Fl
File.close(); // Закрываем файл
return Fl; // Возвращаем значение true, если файл существует
}
struct t_Person // Тип данных для "персоны"
{
wchar_t Fam[20]; // Фамилия
wchar_t Name[20]; // Имя
int Year; // Год рождения
};
void ReadConPerson(t_Person &P)
// Ввод данных персоны с клавиатуры
{
int W = 17; // Ширина поля вывода для подсказки
wcout << setw(W) << L"Фамилия: "; // Выводим подсказку для фамилии
wcin >> P.Fam; // Вводим фамилию
wcout << setw(W) << L"Имя: "; // Выводим подсказку для имени
wcin >> P.Name; // Вводим имя
wcout << setw(W) << L"Год рождения: "; // Выводим подсказку для года рождения
Схема распределения памяти под программу показана на следующем рисунке:
Большие адреса
Меньшие адреса
Стек
Динамическая область памяти
(heap - куча)
Область глобальных данных
Код программы
Область кода программы предназначена для хранения инструкций функций программы, обеспечивающих обработку данных. Данные в программе представляются переменными и константами. Для хранения глобальных данных (существуют в течение всего времени работы программы) предназначена область глобальных данных. Стек программы используется при вызове функций для передачи параметров и хранения локальных данных.
Распределение памяти для хранения всех обычных переменных осуществляется компилятором, и адреса и объемы соответствующих участков памяти (в области глобальных данных) жестко закреплены за этими переменными на все время работы программы и изменено быть не может.
Однако во многих задачах невозможно заранее предсказать, сколько места (количество переменных, объемы массивов и т.д.) потребуется для решения задачи – это так называемые задачи с неопределенной размерностью. Решить эту проблему можно лишь в том случае, если иметь механизм, позволяющий создавать новые объекты по мере возникновения необходимости в этих объектах или изменять объемы памяти, выделенные под эти объекты (например, объемы массивов).
Между областью глобальных данных и стеком располагается так называемая динамическая область памяти, которую как раз и можно использовать в процессе работы программы для реализации механизма динамического управления памятью.
Динамическое выделение и освобождение памяти в стиле C++
Для динамического управления памятью в языке C++ используются две инструкции new и delete. Формат этих инструкций:
<Переменная-указатель> = new <Тип данных переменной-указателя>
delete <Переменная-указатель>
Инструкция new выделяет в динамической области участок памяти, достаточный для размещения данных, тип которых определяется типом данных переменной-указателя, и возвращает адрес этого участка. Этот адрес присваивается переменной-указателю. Например:
double *p; // Переменная-указатель на тип double
p = new double; // Выделение памяти
или так:
double *p = new double;
В этом примере инструкция new выделяет в динамической области участок памяти объемом sizeof (double) и присваивает адрес этого участка переменной-указателю p. Дальнейшая работа с переменной-указателем осуществляется как с обычным указателем на тип данных double. Например:
*p = 3.14;
cout << *p * 2 << endl; // На экран выведено значение 6.28
cin >> *p; // Вводим с клавиатуры некоторое вещественное значение
cout << *p << endl; // На экран выведено значение, введенное с клавиатуры
Размер динамической области памяти ограничен, поэтому при многократном последовательном использовании инструкции new может создаться ситуация, при которой попытка выделения очередного участка памяти с помощью операции new завершится неудачей (возникнет ошибка, связанная с переполнением динамической области памяти). Для того чтобы избежать подобных ошибок, необходимо принудительно освобождать динамическую память с помощью инструкции delete:
delete p;
Инструкция delete возвращает участок памяти по адресу p в список свободной памяти, и в дальнейшем этот участок памяти может быть использован повторно для динамического размещения других данных.
Замечание. Инструкции new и delete это парные инструкции, то есть они всегда должны использоваться совместно – каждой инструкции new должна соответствовать инструкция delete. Динамическая область памяти автоматически освобождается только при завершении программы, поэтому неконтролируемое использование инструкции new может привести к переполнению динамической области памяти и, следовательно, к ошибкам в работе программы.
Тип данных переменной указателя может быть практически любым. Рассмотрим пример создания в динамической области некоторой структуры данных.
struct t_Person // Тип данных для "персоны"
{
char Fam[20]; // Фамилия
char Name[20]; // Имя
int Year; // Год рождения
};
setlocale ( 0, "" ); // Русификация консоли
t_Person *p = new t_Person; // Создаем структуру в динамической памяти
cout << "Имя: " << (*p). Name << endl; // Выводим имя
cout << "Год рождения: " << (*p).Year << endl; // Выводим год рождения
delete p; // Освобождаем память
Для обращения к отдельным полям структуры через переменную-указатель мы использовали следующие конструкции:
(*p).Fam, (*p).Name, (*p).Year
Здесь (*p) обеспечивает разыменование указателя (получение данных персоны, расположенных в памяти по адресу p), а затем с помощью оператора “точка” осуществляется обращение к данным соответствующего поля.
Существует другой способ доступа к полям структур через указатель на структуру с помощью оператора “стрелка” (не требующий предварительного разыменования указателя). Это делается так:
p -> Fam, p -> Name, p -> Year
То есть следующий вариант той же программы будет также корректным:
struct t_Person // Тип данных для "персоны"
{
char Fam[20]; // Фамилия
char Name[20]; // Имя
int Year; // Год рождения
};
setlocale ( 0, "" ); // Русификация консоли
t_Person *p = new t_Person; // Создаем структуру в динамической памяти
cout << "Имя: " << p -> Name << endl; // Выводим имя
cout << "Год рождения: " << p -> Year << endl; // Выводим год рождения
delete p; // Освобождаем память
Для некоторых типов данных одновременно с динамическим выделением памяти можно осуществлять и ее инициализацию. Например:
double *p = new double (3.14); // Инициализация значением 3.14
cout << *p << endl; // На экран выведено значение 3.14
Динамическое выделение и освобождение памяти в стиле C
Язык C++ поддерживает и “старый”, заимствованный от языка C, стиль работы с динамической областью. Достаточно часто бывает полезно использовать именно этот механизм управления динамической памятью, так как он предоставляет несколько более широкий спектр “услуг”.
В языке C отсутствуют инструкции new и delete. Вместо них для управления динамической памятью используются библиотечные функции, имеющие следующие прототипы:
void *malloc ( size_t size );
void *calloc( size_t num, size_t size );
void free( void *memblock );
void *realloc( void *memblock, size_t size );
Функция malloc выделяет в динамической области size байт памяти (тип данных size_t представляет собой разновидность беззнакового целого типа данных) и возвращает адрес этого участка в виде нетипизированного указателя (void *). Поскольку возвращаемый указатель не привязан ни к какому типу данных, при работе с ним потребуется явное приведение типов данных (см. пример ниже).
Функция calloc выделяет в динамической области size * num байт памяти и возвращает адрес этого участка в виде не типизированного указателя (void *).
Функция free освобождает участок динамической памяти по адресу memblock и возвращает его в список свободной памяти для повторного использования.
Функция realloc позволяет изменить размер (уменьшить или увеличить) ранее выделенной по адресу memblock памяти, установив новый размер выделенного участка равным size байт. При увеличении размера выделенного участка данные, которые хранились в старом участке, копируются в новый участок памяти. При уменьшении объема выделенного участка, данные которые хранились в нем, усекаются до нового размера. Функция возвращает нетипизированный указатель на область памяти нового размера.
Предыдущий пример, переделанный под стиль C, выглядит так:
struct t_Person // Тип данных для "персоны"
{
char Fam[20]; // Фамилия
char Name[20]; // Имя
int Year; // Год рождения
};
setlocale ( 0, "" ); // Русификация консоли
t_Person *p; // Определяем указатель на тип t_Person
p = ( t_Person * ) malloc ( sizeof ( t_Person ) ); // Выделяем память и используем
// приведение нетипизированного указателя к указателю на тип данных t_Person
cout << "Имя: " << p -> Name << endl; // Выводим имя
cout << "Год рождения: " << p -> Year << endl; // Выводим год рождения
free ( p ); // Освобождаем память
Для переделки программы под стиль C потребовались всего два изменения в программе (эти места выделены красным цветом).
В одной и той же программе не рекомендуется смешивать использование “старого” и “нового” стилей работы с динамической памятью (возможны проблемы с совместимостью).
Другие возможности (некоторые преимущества) этого стиля работы с динамической памятью будут рассмотрены далее при изучении динамических массивов.
Возможные ошибки при работе с динамической памятью
Некорректная работа с динамической памятью чревата серьезными ошибками. Одну из них мы обсудили ранее. Это ошибка связанна с возможным переполнением динамической области памяти, когда после окончания использования динамических данных мы “забываем” освободить память с помощью инструкции delete или функции free. Обнаружение подобных ошибок с целью предотвращения неправильной работы программы зависит от средств, используемых для выделения памяти.
При неудачной попытке выделить память с помощью инструкции new возникает так называемая исключительная ситуация (или исключение). Обработкой исключений в C++ занимается специальная подсистема обработки исключительных ситуаций, использование и изучение которой выходит за рамки настоящего курса лекций. Впрочем, подобные ошибки в рассматриваемых нами примерах и программах вряд ли будут возникать.
Поведение функций malloc и calloc с точки зрения обнаружения таких ошибок более “разумно”. В случае невозможности выделить требуемый объем памяти в динамической области эти функции возвращают нулевой указатель. Контролируя это значение можно избежать ошибок в работе программы:
Однако имеется другой вариант инструкции new, который работает так же, как и функции malloc и calloc (также возвращает нулевой указатель). Вот как его использовать:
double *p = new ( nothrow ) double; // Пытаемся выделить память
if ( !p ) // Память выделить не удалось
{
// Принимаем меры по исправлению ситуации
}
// Продолжаем работу
Другая категория ошибок называется “утечкой памяти”. Например:
int * p; // Объявляем указатель на целый тип данных
p = new int; // Выделяем память по некоторому адресу p
………
p = new int; // Еще раз выделяем память, и ее адрес записываем опять в p
В этом примере повторное присвоение переменной p другого адреса нового участка памяти приводит к потере адреса участка памяти, выделенного первой инструкцией new. Этот “забытый” участок памяти будет занят до конца работы программы, и его нельзя ни освободить, ни использовать для хранения данных – говорят, что произошла утечка памяти. Такие “утечки” могут привести к тому, что опять произойдет переполнение динамической области памяти. Для недопущения подобных ошибок необходимо внимательно следить за своевременным освобождением памяти, на которую ссылается переменная-указатель.
Еще одна категория ошибок связана с попытками обращения к динамической памяти через указатели, не инициализированные с помощью инструкции new и функциями malloc и calloc, а также при попытке обращения к динамической памяти через указатель после освобождения памяти с помощью инструкции delete или функции free.
Динамические массивы
Для того чтобы создать в динамической области некоторый объект необходима одна обычная (не динамическая переменная) переменная-указатель. Сколько таких объектов нам понадобится для одновременной обработки – столько необходимо иметь обычных переменных-указателей. Таким образом, проблема “задач неопределенной размерности” созданием одиночных динамических объектов решена быть не может.
Решить эту проблему поможет возможность создавать в динамической области памяти массивы объектов с таким количеством элементов, которое необходимо в данный момент работы программы – то есть создание динамических массивов. Действительно, для представления массива требуется всего одна переменная-указатель, а в самом массиве, на который ссылается этот указатель, может быть столько элементов, сколько требуется в данный момент времени.
Сначала рассмотрим одномерные динамические массивы.
Для создания одномерного динамического массива, элементами которого являются, например, действительные числа, используется следующий синтаксис инструкции new:
double *Arr = new double [1000];
Здесь в динамической области памяти будет выделено пространство на 1000 значений типа double, и адрес этой области будет присвоен переменной-указателю Arr. Таким образом, переменная-указатель Arr, как и переменная для обычного массива, будет содержать адрес первого элемента массива.
Освободить динамическую область от этого массива можно так:
delete [ ] Arr;
После этого участок памяти объемом 1000 * sizeof ( double ) байт будет возвращен в список свободной памяти и может быть повторно использован для размещения других динамических объектов.
С помощью функций malloc и calloc тот же самый одномерный динамический массив создается так:
Освобождение памяти в этих случаях осуществляется с помощью функции free:
Free ( Arr );
Работа с одномерным динамическим массивом осуществляется так же, как и с обычным. Рассмотрим пример, в котором создадим динамический массив целых с количеством элементов, введенном с клавиатуры; заполним его случайными значениями в диапазоне от 1 до 100; подсчитаем и выведем на экран среднее значение всех элементов этого массива:
int n; // Количество элементов массива
cin >> n; // Вводим количество элементов массива с клавиатуры
int *Arr = new int [ n ]; // Создаем массив Arr целых чисел на n элементов
for ( int i = 0; i < n; ++ i) // Заполняем массив случайными значениями
Arr [ i ] = rand ( ) % 100 + 1;
int Sum = 0; // Сумма элементов массива
for ( int i = 0; i < n; ++ i ) // Подсчитываем сумму элементов массива
Sum += Arr [ i ];
cout << (double) Sum / n << endl; // Выводим на экран среднее значение
delete [ ] Arr; // Освобождаем память
Очень часто в процессе работы программы требуется изменять размеры уже созданных и заполненных данными массивов. Общий алгоритм решения этой задачи таков:
Создать исходный массив размерности N1 и заполнить его данными;
2. создать промежуточный массив размерности N2 (пусть N2 > N1);
Скопировать данные из исходного массива в промежуточный массив;
Освободить память от исходного массива;
Переменной-указателю исходного массива присвоить значение переменной-указателя промежуточного массива;