Язык C++ не обеспечивает средств для ввода/вывода. Ему это и не нужно; такие
средства легко и элегантно можно создать с помощью самого языка. Описанная здесь
стандартная библиотека потокового ввода/вывода обеспечивает гибкий и эффективный
с гарантией типа метод обработки символьного ввода целых чисел, чисел с плавающей
точкой и символьных строк, а также простую модель ее расширения для обработки
типов, определяемых пользователем. Ее пользовательский интерфейс находится в
. В этой главе описывается сама библиотека, некоторые способы ее применения
и методы, которые использовались при ее реализации.
8.1 Введение
Разработка и реализация стандартных средств ввода/вывода для языка программирования
зарекомендовала себя как заведомо трудная работа. Традиционно средства ввода/вывода
разрабатывались исключительно для небольшого числа встроенных типов данных.
Однако в C++ программах обычно используется много типов, определенных пользователем,
и нужно обрабатывать ввод и вывод также и значений этих типов. Очевидно, средство
ввода/вывода должно быть простым, удобным, надежным в употреблении, эффективным
и гибким, и ко всему прочему полным. Ничье решение еще не смогло угодить всем,
поэтому у пользователя должна быть возможность задавать альтернативные средства
ввода/вывода и расширять стандартные средства ввода/вывода применительно к требованиям
приложения.
C++ разработан так, чтобы у пользователя была возможность определять новые типы
столь же эффективные и удобные, сколь и встроенные типы. Поэтому обоснованным
является требование того, что средства ввода/вывода для C++ должны обеспечиваться
в C++ с применением только тех средств, которые доступны каждому программисту.
Описываемые здесь средства ввода/вывода представляют собой попытку ответить
на этот вызов.
Средства ввода/вывода связаны исключительно с обработкой преобразования типизированных
объектов в последовательности символов и обратно. Есть и другие схемы ввода/вывода,
но эта является основополагающей в системе UNIX, и большая часть видов бинарного
ввода/вывода обрабатывается через рассмотрение символа просто как набор бит,
при этом его общепринятая связь с алфавитом игнорируется. Тогда для программиста
ключевая проблема заключается в задании соответствия между типизированным объектом
и принципиально не типизированной строкой.
Обработка и встроенных и определенных пользователем типов однородным образом
и с гарантией типа достигается с помощью одного перегруженного имени функции
для набора функций вывода. Например:
put(cerr,"x = "); // cerr - поток вывода ошибок
put(cerr,x);
put(cerr,"\n");
Тип параметра определяет то, какая из функций put будет вызываться для каждого
параметра. Это решение применялось в нескольких языках. Однако ему недостает
лаконичности. Перегрузка операции << значением "поместить в" дает более хорошую
запись и позволяет программисту выводить ряд объектов одним оператором. Например:
cerr << "x = " << x << "\n";
где cerr - стандартный поток вывода ошибок. Поэтому, если x является int со
значением 123, то этот оператор напечатает в стандартный поток вывода ошибок
x = 123
и символ новой строки. Аналогично, если X принадлежит определенному пользователем
типу complex и имеет значение (1,2.4), то приведенный выше оператор напечатает
в cerr
x = 1,2.4)
Этот метод можно применять всегда, когда для x определена операция <<, и пользователь
может определять операцию << для нового типа.
8.2 Вывод
8.2.1 Вывод Встроенных Типов
8.2.2 Некоторые Подробности Разработки
8.2.3 Форматированный Вывод
8.2.4 Виртуальная Функция Вывода
В этом разделе сначала обсуждаются средства форматного и бесформатного вывода
встроенных типов, потом приводится стандартный способ спецификации действий
вывода для определяемых пользователем типов.
8.2.1 Вывод Встроенных Типов
Класс ostream определяется вместе с операцией << ("поместить в") для обработки
вывода встроенных типов:
class ostream {
// ...
public:
ostream& operator<<(char*);
ostream& operator<<(int i) { return *this<
8.2.2 Некоторые Подробности Разработки
Операция вывода используется, чтобы избежать той многословности, которую дало
бы использование функции вывода. Но почему < Возможности изобрести новый лексический
символ нет (#6.2). Операция присваивания была кандидатом одновременно и на ввод,
и на вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода
отличалась от операции вывода. Кроме того, = не в ту сторону связывается (ассоциируется),
то есть cout=a=b означает cout=(a=b). Делались попытки использовать операции
< и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей,
что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми.
Помимо этого, "<" находится на большинстве клавиатур как раз на ",", и у людей
получаются операторы вроде такого:
cout < x , y , z;
Для таких операторов непросто выдать хорошие сообщения об ошибках.
Операции << и >> к такого рода проблемам не приводят, они асимметричны в том
смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно
низок, чтобы можно было не использовать скобки для арифметических выражений
в роли операндов. Например:
cout << "a*b+c=" << a*b+c << "\n";
Естественно, при написании выражений, которые содержат операции с более низкими
приоритетами, скобки использовать надо. Например:
cout << "a^b|c=" << (a^b|c) << "\n";
Операцию левого сдвига тоже можно применять в операторе вывода:
cout << "a<
8.2.3 Форматированный Вывод
Пока << применялась только для неформатированного вывода, и на самом деле в
реальных программах она именно для этого главным образом и применяется. Помимо
этого существует также несколько форматирующих функций, создающих представление
своего параметра в виде строки, которая используется для вывода. Их второй (необязательный)
параметр указывает, сколько символьных позиций должно использоваться.
char* oct(long, int =0); // восьмеричное представление
char* dec(long, int =0); // десятичное представление
char* hex(long, int =0); // шестнадцатиричное представление
char* chr(int, int =0); // символ
char* str(char*, int =0); // строка
Если не задано поле нулевой длины, то будет производиться усечение или дополнение;
иначе будет использоваться столько символов (ровно), сколько нужно. Например:
cout << "dec(" << x
<< ") = oct(" << oct(x,6)
<< ") = hex(" << hex(x,4)
<< ")";
Если x==15, то в результате получится:
dec(15) = oct( 17) = hex( f);
Можно также использовать строку в общем формате:
char* form(char* format ...);
cout<
8.2.4 Виртуальная Функция Вывода
Иногда функция вывода должна быть virtual. Рассмотрим пример класса shape, который
дает понятие фигуры (#1.18):
class shape {
// ...
public:
// ...
virtual void draw(ostream& s); // рисует "this" на "s"
};
class circle : public shape {
int radius;
public:
// ...
void draw(ostream&);
};
То есть, круг имеет все признаки фигуры и может обрабатываться как фигура, но
имеет также и некоторые специальные свойства, которые должны учитываться при
его обработке.
Чтобы поддерживать для таких классов стандартную парадигму вывода, операция
<< определяется так:
ostream& operator<<(ostream& s, shape* p)
{
p->draw(s);
return s;
}
Если next - итератор типа определенного в #7.3.3, то список фигур распечатывается
например так:
while ( p = next() ) cout << p;
8.3 Файлы и Потоки
8.3.1 Инициализация Потоков Вывода
8.3.2 Закрытие Потоков Вывода
8.3.3 Открытие Файлов
8.3.4 Копирование Потоков
Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток
ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист
может открывать другие файлы и создавать для них потоки.
8.3.1 Инициализация Потоков Вывода
ostream имеет конструкторы:
class ostream {
// ...
ostream(streambuf* s); // связывает с буфером потока
ostream(int fd); // связывание для файла
ostream(int size, char* p); // связывет с вектором
};
Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс,
управляющий буферами; он описывается в #8.6, как и класс filebuf, управляющий
streambuf для файла. Класс filebuf является производным от класса streambuf.
Описание стандартных потоков вывода cout и cerr, которое находится в исходных
кодах библиотеки потоков ввода/вывода, выглядит так:
// описать подходящее пространство буфера
char cout_buf[BUFSIZE]
// сделать "filebuf" для управления этим пространством
// связать его с UNIX'овским потоком вывода 1 (уже открытым)
filebuf cout_file(1,cout_buf,BUFSIZE);
// сделать ostream, обеспечивая пользовательский интерфейс
ostream cout(&cout_file);
char cerr_buf[1];
// длина 0, то есть, небуферизованный
// UNIX'овский поток вывода 2 (уже открытый)
filebuf cerr_file()2,cerr_buf,0;
ostream cerr(&cerr_file);
Примеры двух других конструкторов ostream можно найти в #8.3.3 и #8.5.
8.3.2 Закрытие Потоков Вывода
Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():
ostream::~ostream()
{
flush(); // сброс
}
Сбросить буфер можно также и явно. Например:
cout.flush();
8.3.3 Открытие Файлов
Точные детали того, как открываются и закрываются файлы, различаются в разных
операционных системах и здесь подробно не описываются. Поскольку после включения
становятся доступны cin, cout и cerr, во многих (если не во всех) программах
не нужно держать код для открытия файлов. Вот, однако, программа, которая открывает
два файла, заданные как параметры командной строки, и копирует первый во второй:
#include
void error(char* s, char* s2)
{
cerr << s << " " << s2 << "\n";
exit(1);
}
main(int argc, char* argv[])
{
if (argc != 3) error("неверное число параметров","");
filebuf f1;
if (f1.open(argv[1],input) == 0)
error("не могу открыть входной файл",argv[1]);
istream from(&f1);
filebuf f2;
if (f2.open(argv[2],output) == 0)
error("не могу создать выходной файл",argv[2]);
ostream to(&f2);
char ch;
while (from.get(ch)) to.put(ch);
if (!from.eof() !! to.bad())
error("случилось нечто странное","");
}
Последовательность действий при создании ostream для именованного файла та же,
что используется для стандартных потоков: (1) сначала создается буфер (здесь
это делается посредством описания filebuf); (2) затем к нему подсоединяется
файл (здесь это делается посредством открытия файла с помощью функции filebuf::open());
и, наконец, (3) создается сам ostream с filebuf в качестве параметра. Потоки
ввода обрабатываются аналогично. Файл может открываться в одной из двух мод:
enum open_mode { input, output };
Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие
с требованием. Если пользователь пытается открыть файл, которого не существует
для output, он будет создан.
Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии
(см. #8.4.2). При завершении программы открытые файлы неявно закрываются.
Файл можно также открыть одновременно для чтения и записи, но в тех случаях,
когда это оказывается необходимо, парадигма потоков редко оказывается идеальной.
Часто лучше рассматривать такой файл как вектор (гигантских размеров). Можно
определить тип, который позволяет программе обрабатывать файл как вектор.