22 мая 2007 г.

Простой пример файлового ввода-вывода

Ну, поглядим, как это все работает. Допустим нам надо вывести числа, хранящиеся в векторе, в файл через пробелы – достаточно распространенная задача. Для этого можно написать примерно такую функцию:
#include <fstream>
#include <string>
#include <vector>
#include <iostream>
using namespace std;
int output(vector<int> &values, string filename)
{
ofstream ofile(filename.c_str());
if (!ofile)
{
cerr << "Error opening file!" << endl;
return -1;
}
for (int i = 0; i < values.size(); ++i)
ofile << values[i] << ' ';
ofile.close();
return 0;
}

На мой взгляд, код достаточно очевидный, чтобы его объяснять построчно. Обращу внимание только на то, что вывод в файл (ofile << values[i] << ' ';) и вывод в стандартный поток ошибок (cerr << "Error opening file!" << endl;) выполняются одинаково, с помощью переопределенного оператора << . А таинственный cerr (наряду с cout и clog) – это всего лишь объект класса ostream, определенный в файле iostream; работает он точно так же, как и созданный нами собственноручно ofile (ну, вообще-то, работает не совсем так же, но тонкости рассмотрим позже).
Справедливости ради создадим функцию, которая выполняет обратное действие – считывает последовательность чисел из файла:
int input(vector<int> &values, string filename)
{
ifstream ifile(filename.c_str());
if (!ifile)
{
cerr << "Error opening file!" << endl;
return -1;
}
int v;
while (!ifile.eof())
{
ifile >> v;
values.push_back(v);
}
ifile.close();
return 0;
}

Объект потока не обязательно должен лежать на стеке, возможны всевозможные варианты:
// можно так (на стеке)
ifstream ifile;
ifile.open(filename.c_str());
// а можно и так (в куче)
ifstream *ifile = new ifstream;
ifile->open(filename.c_str());
// можно даже так
ifstream *ifile = new ifstream(filename.c_str());

Домашнее задание: замените ofstream на ostrstream, ifstream на istrstream, а имя файла - на адрес char-строки. Убедитесь, что ввод-вывод на символьных строках выполняется точно так же, как и файловый.

Библиотека iostream

Как и в старичке C, в C++ нет встроенных средств для ввода и вывода данных куда бы то ни было. Редкие в наше время программисты на C++ под MS-DOS, да и вообще старшее поколение программистов, могут возразить – мол, ввод и вывод суть чтение и запись по определенным адресам в оперативной памяти (угу, а программирование суть написание машинных команд). Однако это мало того что неудобно, так еще и обладает такими нехорошими свойствами, как абсолютная платформенная непереносимость, а также несовместимость с объектно-ориентированной парадигмой. Короче говоря, по абсолютным адресам в памяти мы писать не хотим, и волшебные слова «контроллер», «DMA» и «драйвер» оставим тем, кому это интересно.
К счастью, добрые люди присобачили к C++ стандартную библиотеку, выдержанную в традициях ООП и позволяющую все, что только душа пожелает (ну, почти).
В этой и нескольких следующих статьях я собираюсь дать небольшое обзорное описание библиотеки iostream, но несколько нетрадиционное. Обычно авторы книг по C++ начинают с элементарных
cout << “Hello, World!” << endl;

и только потом описывают манипуляторы, работу с файлами и т.д. Прилежные ученики послушно переписывают примеры из книги, убеждаются, что они работают, и запоминают на всю жизнь, что cout<< – это такая хитрая языковая конструкция, позволяющая выводить на экран строки, и совершенно не связанная с файловым вводом-выводом. В головах возникает каша, которую потом долго приходится разгребать (зато сколько будет приятных сюрпризов!). Я пойду по несколько другому пути – начну с самых основ и, продвигаясь наверх, в сторону конкретизации абстрактных понятий, покажу, как там все на самом деле работает. Начнем с того, из чего состоит библиотека iostream.
Каждый уважающий себя компилятор C++ поставляется со стандартной библиотечкой ввода-вывода по имени iostream. Ключевым понятием в этой библиотеке является поток (stream) – не путать с потоком управления (thread)! Не вдаваясь в детали реализации, можно сказать, что поток – это объект, который может служить источником или приемником байтов (или и тем, и другим сразу). Не все сразу могут понять отличие потоковых средств ввода-вывода от традиционных read/write. Суть этих отличий в том, что поток – это более высокая ступень абстракции; он может использоваться как, например, для чтения из файла (и при этом он ведет себя почти как обычный read), так и для чтения данных из последовательного порта или с клавиатуры. В последнем случае данные уже имеют ярко выраженную потоковую природу.
Кстати, надо заметить, что консерваторы по-прежнему могут использовать привычную, аки домашние тапочки, библиотеку stdio, но в таком получается меньше контроля над потоками, меньше удобства и меньше соответствия парадигме ООП.
Для начала посмотрим, какие в библиотеке iostream есть заголовочные файлы:
  • fstream – классы, инкапсулирующие ввод-вывод на внешних файлах.
  • ios – базовые классы iostream; обычно этот заголовочный файл не нужно включать напрямую, он включается в других заголовочниках.
  • iostream – определяет объекты ввода-вывода на стандартных потоках.
  • ostream – определяет шаблонный базовый класс basic_ostream, управляющий выводом в потоки; этот файл также обычно не включается в программу напрямую.
  • streambuf – определяет шаблонный базовый класс basic_streambuf, управляющий буферизацией; не нужно включать напрямую.
  • iomanip – определяет ряд манипуляторов (про них ниже).
  • iosfwd – объявляет (но не определяет!) кучу шаблонных классов и typedef’ов.
  • istream – брат-близнец ostream, только в отношении потокового ввода.
  • sstream – определяет ряд классов для потоковой работы с хранящимися в памяти последовательностями.
  • strstream – определяет ряд классов для потоковой работы с char-строками.
Как нетрудно догадаться, большую часть этих заголовочников нормальный программист никогда в своей жизни не увидит (и хорошо). Обычно используются только fstream, iostream, iomanip, sstream, strstream и соответствующие им классы.
Теперь рассмотрим структуру и использование одной из частей библиотеки iostream, а именно классы для потокового ввода-вывода на файлах. Работа с ними ничуть не отличается от потокового ввода-вывода на памяти, на строках или на стандартных потоках (то есть консоли и клавиатуре).
Заголовочный файл fstream определяет ряд классов: filebuf, wfilebuf, fstream, wfstream, ifstream, wifstream, ofstream, wofstream, basic_filebuf, basic_fstream, basic_ifstream, basic_ofstream. На самом деле из приведенного списка только последние четыре пункта – классы. Они определяются следующим образом:
template <class Elem, class Tr = char_traits<Elem> >
class basic_filebuf : public basic_streambuf<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_fstream : public basic_iostream<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_ifstream : public basic_istream<Elem, Tr>
template <class Elem, class Tr = char_traits<Elem> >
class basic_ofstream : public basic_ostream<Elem, Tr>

Они очень похожи – шаблонные классы с двумя параметрами: Elem – собственно, тип вводимо-выводимых элементов, и Tr – класс символьных характеристик этих элементов. Tr обязательно должен быть типа char_traits (лучше оставлять указанное по умолчанию char_traits<Elem>).
Остальные имена из приведенного выше списка – всего лишь специализации этих классов; например, классы ofstream и wofstream, отвечающие, соответственно, за вывод в файл последовательностей char- и wchar-символов, определяются так:
typedef basic_ofstream<char, char_traits<char> > ofstream;
typedef basic_ofstream
<wchar_t, char_traits<wchar_t> > wofstream;
В следующий раз посмотрим, как вся эта радость работает.