search

Файловый ввод-вывод

В С++-системе ввода-вывода также предусмотрены средства для выполнения соответствующих операций с использованием файлов. Файловые операции ввода-вывода можно реализовать после включения в программу заголовка <fstream>, в котором определены все необходимые для этого классы и значения.

Как открыть и закрыть файл

В C++ файл открывается путем связывания его с потоком. Как вы знаете, существуют потоки трех типов: ввода, вывода и ввода-вывода. Чтобы открыть входной поток, необходимо объявить потоковый объект типа ifstream. Для открытия выходного потока нужно объявить поток класса ofstream. Поток, который предполагается использовать для операций как ввода, так и вывода, должен быть объявлен как объект класса fstream. Например, при выполнении следующего фрагмента кода будет создан входной поток, выходной и поток, позволяющий выполнение операций в обоих направлениях.


ifstream in; // входной поток
ofstream out; // выходной поток
fstream both; // поток ввода-вывода	

Чтобы открыть файл, используйте функцию open(). Создав поток, его нужно связать с файлом. Это можно сделать с помощью функции open(), причем в каждом из трех потоковых классов есть своя функция-член 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);	

Здесь элемент filename означает имя файла, которое может включать спецификатор пути. Элемент mode определяет способ открытия файла. Он должен принимать одно или несколько значений перечисления openmode, которое определено в классе ios.


ios::арр
ios::ate
ios::rbinary
ios::in
ios::out
ios::trunc	

Несколько значений перечисления openmode можно объединять посредством логического сложения (ИЛИ).

На заметку. Параметр mode для функции fstream::open() может не устанавливаться по умолчанию равным значению in | out (это зависит от используемого компилятора). Поэтому при необходимости этот параметр вам придется задавать в явном виде.

Включение значения ios::арр в параметр mode обеспечит присоединение к концу файла всех выводимых данных. Это значение можно применять только к файлам, открытым для вывода данных. При открытии файла с использованием значения ios::ate поиск будет начинаться с конца файла. Несмотря на это, операции ввода-вывода могут по-прежнему выполняться по всему файлу.

Значение ios::in говорит о том, что данный файл открывается для ввода данных, а значение ios::out обеспечивает открытие файла для вывода данных.

Значение ios::binary позволяет открыть файл в двоичном режиме. По умолчанию все файлы открываются в текстовом режиме. Как упоминалось выше, в текстовом режиме могут происходить некоторые преобразования символов (например, последовательность, состоящая из символов возврата каретки и перехода на новую строку, может быть преобразована в символ новой строки). При открытии файла в двоичном режиме никакого преобразования символов не выполняется. Следует иметь в виду, любой файл, содержащий форматированный текст или еще необработанные данные, можно открыть как в двоичном, так и в текстовом режиме. Единственное различие между этими режимами состоит в преобразовании (или нет) символов.

Использование значения ios::trunc приводит к разрушению содержимого файла, имя которого совпадает с параметром filename, а сам этот файл усекается до нулевой длины. При создании выходного потока типа ofstream любой существующий файл с именем filename автоматически усекается до нулевой длины

При выполнении следующего фрагмента кода открывается обычный выходной файл.

ofstream out; out.open("тест");

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

Не открытый в результате неудачного выполнения функции open() поток при использовании в булевом выражении устанавливается равным значению ЛОЖЬ. Этот факт может служить для подтверждения успешного открытия файла, например, с помощью такой if-инструкции.


if(!mystream) {
cout << "He удается открыть файл.\n";
// обработка ошибки
}	

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

Можно также проверить факт успешного открытия файла с помощью функции is_open(), которая является членом классов fstream, ifstream и ofstream. Вот ее прототип,


bool is_open();	

Эта функция возвращает значение ИСТИНА, если поток связан с открытым файлом, и ЛОЖЬ — в противном случае. Например, используя следующий код, можно узнать, открыт ли в данный момент потоковый объект mystream.


if(!mystream.is_open()) {
cout << "Файл не открыт.\n";
// ...
}	

Хотя вполне корректно использовать функцию open() для открытия файла, в большинстве случаев это делается по-другому, поскольку классы ifstream, ofstream и fstream включают конструкторы, которые автоматически открывают заданный файл. Параметры у этих конструкторов и их значения (действующие по умолчанию) совпадают с параметрами и соответствующими значениями функции open(). Поэтому чаще всего файл открывается так, как показано в следующем примере,


ifstream mystream("myfile"); // файл открывается для ввода	

Если по какой-то причине файл открыть невозможно, потоковая переменная, связываемая с этим файлом, устанавливается равной значению ЛОЖЬ.

Чтобы закрыть файл, используйте функцию-член close(). Например, чтобы закрыть файл, связанный с потоковым объектом mystream, используйте такую инструкцию,


mystream.close();
Функция close() не имеет параметров и не возвращает никакого значения.

Чтение и запись текстовых файлов

Проще всего считывать данные из текстового файла или записывать их в него с помощью операторов "<<" и ">>". Например, в следующей программе выполняется запись в файл test целого числа, значения с плавающей точкой и строки.


// Запись данных в файл.
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream out("test");
if(!out) {
cout << "He удается открыть файл.\n";
return 1;
}
out << 10 << " " << 123.23 << "\n";
out << "Это короткий текстовый файл.";
out.close();
return 0;
}
Следующая программа считывает целое число, float-значение, символ и строку из файла,
созданного при выполнении предыдущей программой.
// Считывание данных из файла.
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
char ch;
int i;
float f;
char str[80];
ifstream in("test");
if(!in) {
cout << "He удается открыть файл.\n";
return 1;
}
in >> i;
in >> f;
in >> ch;
in >> str;
cout << i << " " << f << " " << ch << "\n";
cout << str;
in.close();
return 0;
}

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

Неформатированный ввод-вывод данных в двоичном режиме

Форматированные текстовые файлы (подобные тем, которые использовались в предыдущих примерах) полезны во многих ситуациях, но они не обладают гибкостью неформатированных двоичных файлов. Поэтому C++ поддерживает ряд функций файлового ввода-вывода в двоичном режиме, которые могут выполнять операции без форматирования данных.

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

Функция get() считывает символ из файла, а функция put() записывает символ в файл. В общем случае существует два способа записи неформатированных двоичных данных в файл и считывания их из файла. Первый состоит в использовании функции-члена put() (для записи байта в файл) и функции-члена get() (для считывания байта из файла). Второй способ предполагает применение "блочных" С++-функций ввода-вывода read() и write(). Рассмотрим каждый способ в отдельности.

Использование функций get() и put()

Функции get() и put() имеют множество форматов, но чаще всего используются следующие их версии:


istream &get(char &ch);
ostream &put(char ch);	

Функция get() считывает один символ из соответствующего потока и помещает его значение в переменную ch. Она возвращает ссылку на поток, связанный с предварительно открытым файлом. При достижении конца этого файла значение ссылки станет равным нулю. Функция put() записывает символ ch в поток и возвращает ссылку на этот поток.

При выполнении следующей программы на экран будет выведено содержимое любого заданного файла. Здесь используется функция get().


/* Отображение содержимого файла с помощью функции get().
*/
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char *argv[])
{
char ch;
if(argc!=2) {
cout << "Применение: имя_программы <имя_файла>\n";
return 1;
}
ifstream in(argv[1], ios::in | ios::binary);
if(!in) {
cout << "He удается открыть файл.\n";
return 1;
}
while(in) {
/* При достижении конца файла потоковый объект in примет
значение false. */
in.get(ch);
if(in) cout << ch;
}
in.close();
return 0;
}

При достижении конца файла потоковый объект in примет значение ЛОЖЬ, которое остановит выполнение цикла while. Существует более короткий вариант цикла, предназначенного для считывания и отображения содержимого файла.


while(in.get(ch)) cout << ch;	

Этот вариант также имеет право на существование, поскольку функция get() возвращает потоковый объект in, который при достижении конца файла примет значение false. В следующей программе для записи строки в файл используется функция put().


/* Использование функции put() для записи строки в файл.
*/
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
char *p = "Всем привет!";
ofstream out("test", ios::out | ios::binary);
if(!out) {
cout << "He удается открыть файл.\n";
return 1;
}
while(*p) out.put(*p++);
out.close();
return 0;
}

Считывание и запись в файл блоков данных

Чтобы считывать и записывать в файл блоки двоичных данных, используйте функциичлены read() и write(). Их прототипы имеют следующий вид.


istream &read(char *buf, streamsize num);

ostream &write(const char *buf, int streamsize num);	

Функция read() считывает num байт данных из связанного с файлом потока и помещает их в буфер, адресуемый параметром buf. Функция write() записывает num байт данных в связанный с файлом поток из буфера, адресуемого параметром buf. Как упоминалось выше, тип streamsize определен как некоторая разновидность целочисленного типа. Он позволяет хранить самое большое количество байтов, которое может быть передано в процессе любой операции ввода-вывода.

Функция read() вводит блок данных, а функция write() выводит его. При выполнении следующей программы сначала в файл записывается массив целых чисел, а затем он же считывается из файла.


// Использование функций read() и write().
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
int n[5] = {1, 2, 3, 4, 5};
register int i;
ofstream out("test", ios::out | ios::binary);
if(!out) {
cout << "He удается открыть файл.\n";
return 1;
}
out.write((char *) &n, sizeof n);
out.close();
for(i=0; i < 5; i++) // очищаем массив
n[i] = 0;
ifstream in ("test", ios::in | ios::binary);
if(!in) {
cout << "He удается открыть файл.\n";
return 1;
}
in.read((char *) &n, sizeof n);
for(i=0; i < 5; i++) // Отображаем значения, считанные из файла.
cout << n[i] << " ";
in.close();
return 0;
}

Обратите внимание на то, что в инструкциях обращения к функциям read() и write() выполняются операции приведения типа, которые обязательны при использовании буфера, определенного не в виде символьного массива.

Функция gcount() возвращает количество символов, считанных при выполнении последней операции ввода данных.

Если конец файла будет достигнут до того, как будет считано num символов, функция read() просто прекратит выполнение, а буфер будет содержать столько символов, сколько удалось считать до этого момента. Точное количество считанных символов можно узнать с помощью еще одной функции-члена gcount(), которая имеет такой прототип.


streamsize gcount();	

Функция gcount() возвращает количество символов, считанных в процессе выполнения последней операции ввода данных

Обнаружение конца файла

Обнаружить конец файла можно с помощью функции-члена eof(), которая имеет такой прототип.


bool eof();

Эта функция возвращает значение true при достижении конца файла; в противном случае она возвращает значение false.
Функция eof() позволяет обнаружить конец файла.
В следующей программе для вывода на экран содержимого файла используется функция eof().


/* Обнаружение конца файла с помощью функции eof().
*/
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char *argv[])
{
char ch;
if(argc!=2) {
cout << "Применение: имя_программы <имя_файла>\n";
return 1;
}
ifstream in(argv[1], ios::in | ios::binary);
if(!in) {
cout << "He удается открыть файл.\n";
return 1;
}
while(!in.eof()) {
// использование функции eof()
in.get(ch);
if( !in.eof()) cout << ch;
}
in.close();
return 0;
}	

Пример сравнения файлов

Следующая программа иллюстрирует мощь и простоту применения в C++ файловой системы. Здесь сравниваются два файла с помощью функций двоичного ввода-вывода read(), eof() и gcount(). Программа сначала открывает сравниваемые файлы для выполнения двоичных операций (чтобы не допустить преобразования символов). Затем из каждого файла по очереди считываются блоки информации в соответствующие буферы и сравнивается их содержимое. Поскольку объем считанных данных может быть меньше размера буфера, в программе используется функция gcount(), которая точно определяет количество считанных в буфер байтов. Нетрудно убедиться в том, что при использовании файловых С++-функций для выполнения этих операций потребовалась совсем небольшая по размеру программа.


// Сравнение файлов.
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char *argv[])
{
register int i;
unsigned char buf1[1024], buf2[1024];
if(argc!=3) {
cout << "Применение: имя_программы <имя_файла1> "<< "
<имя_файла2>\n";
return 1;
}
ifstream f1(argv[1], ios::in | ios::binary);
if(!f1) {
cout << "He удается открыть первый файл.\n";
return 1;
}
ifstream f2(argv[2], ios::in | ios::binary);
if(!f2) {
cout << "He удается открыть второй файл.\n";
return 1;
}
cout << "Сравнение файлов ...\n";
do {
f1.read((char *) buf1, sizeof buf1);
f2.read((char *) buf2, sizeof buf2);
if(f1.gcount() != f2.gcount()) {
cout << "Файлы имеют разные размеры.\n";
f1.close();
f2.close();
return 0;
}
// Сравнение содержимого буферов.
for(i=0; i < f1.gcount(); i++)
if(buf1[i] != buf2[i]) {
cout << "Файлы различны.\n";
f1.close();
f2.close();
return 0;
}
}while(!f1.eof() && !f2.eof());
cout << "Файлы одинаковы.\n";
f1.close();
f2.close();
return 0;
}

Проведите эксперимент. Размер буфера в этой программе жестко установлен равным 1024. В качестве упражнения замените это значение const-переменной и опробуйте другие размеры буферов. Определите оптимальный размер буфера для своей операционной среды.

Использование других функций двоичного ввода-вывода

Помимо приведенного выше формата использования функции get() существуют и другие ее перегруженные версии. Приведем прототипы для трех из них, которые используются чаще всего.


istream &get(char *buf, streamsize num);
istream &get(char *buf, streamsize num, char delim);
int get();	

Первая версия позволяет считывать символы в массив, заданный параметром buf, до тех пор, пока либо не будет считано num-1 символов, либо не встретится символ новой строки, либо не будет достигнут конец файла. После выполнения функции get() массив, адресуемый параметром buf, будет иметь завершающий нуль-символ. Символ новой строки, если таковой обнаружится во входном потоке, не извлекается. Он остается там до тех пор, пока не выполнится следующая операция ввода-вывода.

Вторая версия предназначена для считывания символов в массив, адресуемый параметром buf, до тех пор, пока либо не будет считано num-1 символов, либо не обнаружится символ, заданный параметром delim, либо не будет достигнут конец файла. После выполнения функции get() массив, адресуемый параметром buf, будет иметь завершающий нуль-символ. Символ-разделитель (заданный параметром delim), если таковой обнаружится во входном потоке, не извлекается. Он остается там до тех пор, пока не выполнится следующая операция ввода-вывода

Третья перегруженная версия функции get() возвращает из потока следующий символ. Он содержится в младшем байте значения, возвращаемого функцией. Следовательно, значение, возвращаемое функцией get(), можно присвоить переменной типа char. При достижении конца файла эта функция возвращает значение EOF, которое определено в заголовке <iostream>.

Функцию get() полезно использовать для считывания строк, содержащих пробелы. Как вы знаете, если для считывания строки используется оператор ">>", процесс ввода останавливается при обнаружении первого же пробельного символа. Это делает оператор ">>" бесполезным для считывания строк, содержащих пробелы. Но эту проблему, как показано в следующей программе, можно обойти с помощью функции get(buf,num).


streamsize gcount();/* Использование функции get() для считывания строк содержащих
пробелы.
*/
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
char str[80];
cout << "Введите имя: ";
cin.get (str, 79);
cout << str << '\n';
return 0;
}

Здесь в качестве символа-разделителя при считывании строки с помощью функции get() используется символ новой строки. Это делает поведение функции get() во многом сходным с поведением стандартной функции gets(). Однако преимущество функции get() состоит в том, что она позволяет предотвратить возможный выход за границы массива, который принимает вводимые пользователем символы, поскольку в программе задано максимальное количество считываемых символов. Это делает функцию get() гораздо безопаснее функции gets().

Рассмотрим еще одну функцию, которая позволяет вводить данные. Речь идет о функции getline(), которая является членом каждого потокового класса, предназначенного для ввода информации. Вот как выглядят прототипы версий этой функции,


istream &getline(char *buf, streamsize num);
istream &getline(char *buf, streamsize num, char delim);	

Функция getline() представляет собой еще один способ ввода данных. При использовании первой версии символы считываются в массив, адресуемый указателем buf, до тех пор, пока либо не будет считано num-1 символов, либо не встретится символ новой строки, либо не будет достигнут конец файла. После выполнения функции getline() массив, адресуемый параметром buf, будет иметь завершающий нуль-символ. Символ новой строки, если таковой обнаружится во входном потоке, при этом извлекается, но не помещается в массив buf

Вторая версия предназначена для считывания символов в массив, адресуемый параметром buf, до тех пор, пока либо не будет считано num-1 символов, либо не обнаружится символ, заданный параметром delim, либо не будет достигнут конец файла. После выполнения функции getline() массив, адресуемый параметром buf, будет иметь завершающий нуль-символ. Символ-разделитель (заданный параметром delim), если таковой обнаружится во входном потоке, извлекается, но не помещается в массив buf.

Как видите, эти две версии функции getline() практически идентичны версиям get (buf, num) и get (buf, num, delim) функции get(). Обе считывают символы из входного потока и помещают их в массив, адресуемый параметром buf, до тех пор, пока либо не будет считано num-1 символов, либо не обнаружится символ, заданный параметром delim. Различие между функциями get() и getline() состоит в том, что функция getline() считывает и удаляет символразделитель из входного потока, а функция get() этого не делает.

Функция реек() считывает следующий символ из входного потока, не удаляя его. Следующий символ из входного потока можно получить и не удалять его из потока с помощью функции реек(). Вот как выглядит ее прототип.

Функция peek() возвращает следующий символ потока, или значение EOF, если достигнут конец файла. Считанный символ возвращается в младшем байте значения, возвращаемого функцией. Поэтому значение, возвращаемое функцией реек(), можно присвоить переменной типа char.

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


istream &putback(char с);

Здесь параметр с содержит символ, считанный из потока последним.

Функция flush() сбрасывает на диск содержимое файловых буферов. При выводе данных немедленной их записи на физическое устройство, связанное с потоком, не происходит. Подлежащая выводу информация накапливается во внутреннем буфере до тех пор, пока этот буфер не заполнится целиком. И только тогда его содержимое переписывается на диск. Однако существует возможность немедленной перезаписи на диск хранимой в буфере информации, не дожидаясь его заполнения. Это средство состоит в вызове функции flush(). Ее прототип имеет такой вид.


ostream &flush();	

К вызовам функции flush() следует прибегать в случае, если программа предназначена для выполнения в неблагоприятных средах (для которых характерны частые отключения электричества, например).

Произвольный доступ

До сих пор мы использовали файлы, доступ к содержимому которых был организован строго последовательно, байт за байтом. Но в C++ также можно получать доступ к файлу в произвольном порядке. В этом случае необходимо использовать функции seekg() и seekp(). Вот их прототипы.


istream &seekg(off_type offset, seekdir origin);
ostream &seekp(off_type offset, seekdir origin);	

Используемый здесь целочисленный тип of _type (он определен в классе ios) позволяет хранить самое большое допустимое значение, которое может иметь параметр of set. Тип seekdir определен как перечисление, которое имеет следующие значения.

Статусную информацию о результате выполнения операций ввода-вывода можно получать двумя способами. Во-первых, можно вызвать функцию rdstate(), которая является членом класса ios. Она имеет такой прототип.


iostate rdstate();	

Функция rdstate() возвращает текущий статус флагов ошибок. Нетрудно догадаться, что, судя по приведенному выше списку флагов, функция rdstate() возвратит значение goodbit при отсутствии каких бы то ни было ошибок. В противном случае она возвращает соответствующий флаг ошибки. Во-вторых, о наличии ошибки можно узнать с помощью одной или нескольких следующих функций-членов класса ios.


bool bad();
bool eof();
bool fail();
bool good();

Функция eof() рассматривалась выше. Функция bad() возвращает значение ИСТИНА, если в результате выполнения операции ввода-вывода был установлен флаг badbit. Функция fail() возвращает значение ИСТИНА, если в результате выполнения операции ввода-вывода был установлен флаг failbit. Функция good() возвращает значение ИСТИНА, если при выполнении операции ввода-вывода ошибок не произошло. В противном случае они возвращают значение ЛОЖЬ.

Если при выполнении операции ввода-вывода произошла ошибка, то, возможно, прежде чем продолжать выполнение программы, имеет смысл сбросить флаги ошибок. Для этого используйте функцию clear() (член класса ios), прототип которой выглядит так.


void clear (iostate flags = ios::goodbit);	

Если параметр flags равен значению goodbit (оно устанавливается по умолчанию), все флаги ошибок очищаются. В противном случае флаги устанавливаются в соответствии с заданным вами значением.

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

Использование перегруженных операторов ввода-вывода при работе с файлами

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

В следующей программе используется перегруженный (для класса three_d) оператор вывода для записи значений координат в файл threed.


/* Использование перегруженного оператора ввода-вывода для
записи объектов класса three_d в файл.
*/
#include <iostream>
#include <fstream>
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты; они теперь закрыты
public:
three_d(int a, int b, int с) { x = a; у = b; z = c; }
friend ostream &operator<<(ostream &stream, three_d obj); /*
Отображение координат X, Y, Z (оператор вывода для класса
three_d). */
};
ostream &operator<<(ostream &stream, three_d obj)
{
stream << obj.x << ", ";
stream << obj.у << ", ";
stream << obj.z << "\n";
return stream; // возвращает поток
}
int main()
{
three_d a(1, 2, 3), b(3, 4, 5), c(5, 6, 7);
ofstream out("threed");
if(!out) {
cout << "He удается открыть файл.";
return 1;
}
out << a << b << c;
out.close();
return 0;
}

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

Важно! Прежде чем переходить к следующей главе, не пожалейте времени и поработайте с С++-функциями ввода-вывода. Создайте собственный класс, а затем определите для него операторы ввода и вывода. А еще создайте собственные манипуляторы.