Язык С++ не обеспечивает средств для ввода/вывода. Ему это и не нужно. Такие средства легко и элегантно можно создать с помощью самого языка. Описанная здесь стандартная библиотека потокового ввода/вывода обеспечивает гибкий и эффективный, с гаран
Разработка и реализация стандартных средств ввода/вывода для языка программирования зарекомендовала себя как заведомо трудная работа. Традиционно средства ввода/вывода разрабатывались исключительно для небольшого числа встроенных типов данных. Однако в
С++ разработан так, чтобы у пользователя была возможность определять новые типы столь же эффективные и удобные, сколь и встроенные типы. Поэтому обоснованным является требование того, что средства ввода/вывода для С++ должны обеспечиваться в С++
Средства ввода/вывода <stream.h> связаны исключительно с обработкой преобразования типизированных объектов в последовательности символов и обратно. Есть и другие схемы ввода/вывода, но эта является основополагающей в системе 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 определена операция <<, и пользователь может определять операцию << для нового типа.
В этом разделе сначала обсуждаются средства форматного и бесформатного вывода встроенных типов, потом приводится стандартный способ спецификации действий вывода для определяемых пользователем типов.
Класс ostream определяется вместе с операцией << ("поместить в") для обработки вывода встроенных типов:
class ostream { // ... public: ostream& operator<<(char*); ostream& operator<<(int i){return *this<<long(i);} ostream& operator<<(long); ostream& operator<<(double); ostream& put(char); };Функция operator<< возвращает ссылку на ostream, для которого она была вызвана, чтобы к ней можно было применять другой ostream. Например:
cerr << "x = " << x;где x является int, будет интерпретироваться как:
(cerr.operator<<("x = ")).operator<<(x);В частности, отсюда следует, что когда один оператор вывода печатает несколько элементов, они будут печататься в ожидаемом порядке: слева направо. Наличие operator<<, которая получает int, является избыточным, поскольку int может неявно преобразовыват
Рассмотрим определяемый пользователем тип:
class complex { double re, im; public: complex(double r = 0, double i = 0) { re=r; im=i; } friend double real(complex& a) { returna.re; } friend double real(complex& a) { returna.re; } friend complex operator+(complex, complex); friend complex operator-(complex, complex); friend complex operator*(complex, complex); friend complex operator/(complex, complex); // ... };Операцию << для нового типа complex можно определить так:
ostream& operator<<(ostream&s, complex z) { return s << "(" << real(z) << "," << imag(z) << ")"; }и использовать точно так же, как для встроенного типа:
complex x(1,2); // ... cout << "x = " << x << "\n";получая при этом
x = (1,2)Определение действия вывода для определяемого пользователем типа не требует ни модификации описания класса ostream, ни доступа к структуре данных (скрытой), которую этот класс поддерживает. Очень удачно, что имеет место первое, потому что описание
Операция вывода используется, чтобы избежать той многословности, которую дало бы использование функции вывода. Но почему <
Возможности изобрести новый лексический символ нет (#6.2). Операция присваивания была кандидатом одновременно и на ввод, и на вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода отличалась от операции вывода. Кроме того, = н
Делались попытки использовать операции < и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей, что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми. Помимо этого, "<" находится на большинстве клавиа
cout < x , y , z;Для таких операторов непросто выдавать хорошие сообщения об ошибках.
Операции << и >> к такого рода проблемам не приводят, они асимметричны в том смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно низок, чтобы можно было не использовать скобки для арифметических выражений в роли операндо
cout << "a*b+c=" << a*b+c << "\n";Естественно, при написании выражений, которые содержат операции с более низкими приоритетами, скобки использовать надо. Например:
cout << "a^b|c=" << (a^b|c) << "\n";Операцию левого сдвига тоже можно применять в операторе вывода:
cout << "a<<b=" << (a<<b) << "\n";В С++ нет выражений с символьными значениями, в частности, '\n' является целым (со значением 10, если используется набор символов ASCII), поэтому
cout << "x = " << x << '\n';напечатает число 10, а не ожидаемый символ новой строки. Эту и аналогичные проблемы можно сгладить, определив несколько макросов (в которых используются стандартные имена символов ASKII):
#define sp << " " #define ht << "\t" #define nl << "\n"Теперь предыдущий пример запишется в виде:
cout << "x = " << x nl;Для печати символов предоставляются функции ostream::put(char) и chr(int) (см. #8.2.4). Хотя в некоторых кругах несинтаксические макросы считаются худшим видом макросов, мне они нравятся.
Рассмотрим примеры:
cout << x << " " << y << " " << z << "\n"; cout << "x = " << x << ", y = " << y << "\n";Люди находят их трудно читаемыми из-за большого числа кавычек и того, что операция вывода внешне выглядит слишком непривычно. Здесь могут помочь приведенные выше макросы и несколько отступов:
cout << x sp << y sp << z nl; cout << "x = " << x << ", y = " << y nl;
Пока << применялась только для неформатированного вывода, и на самом деле в реальных программах она именно для этого главным образом и применяется. Помимо этого существует также несколько форматирующих функций, создающих представление своего па
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<<form() эквивалентно применению стандартной функции вывода языка C printf()*. form() возвращает строку, получаемую в результате преобразования и форматирования ее параметров, которые стоят после первого управляющего параметра - строки фор
cout<<form("there were %d members present",no_of_members);Здесь %d указывает, что no_of_members должно рассматриваться как int и печататься в виде соответствующей последовательности десятичных цифр. Если no_of_members==127, вывод будет такой:
there were 127 members present
* Объяснение того, как применяются строки формата, - это слегка отредактированный вариант спецификации printf(). (прим. автора)
Множество спецификаций преобразования довольно велико и обеспечивает высокую степень гибкости. После % может стоять:
- необязательный знак минус, который задает выравнивание преобразованного значения влево в указанном поле;
d необязательная строка цифр, задающая ширину поля. Если преобразованное значение имеет меньше цифр, чем ширина поля, оно будет дополняться пробелами слева (или справа, если был задан индикатор выравнивания влево) до заполнения всей ширины
. необязательная точка, для отделения ширины поля от следующей строки цифр;
d необязательная строка цифр, специфицирующая точность, которая задает число цифр после десятичной точки для преобразований e и f или печатаемых символов для строки;
* в ширине поля или точности вместо строки цифр может стоять *. В этом случае ширина поля и точность задается целым параметром;
h необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру короткое целое;
l необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру длинное целое;
% указывает, что должен быть напечатан символ %, никакие параметры при этом не затрагиваются;
c символ, указывающий, какой тип преобразования должен применяться. Символы преобразования и их значения таковы:
Вот более сложный пример:
char* src_file_name; int line; char* line_format = "\n#line %d \"%s\"\n"; //... cout << "int a;\n"; cout << form(line_format,line,src_file_name); cout << "int b;\n";который печатает
int a; #line 13 "С++/main.c" int b;Применение form() небезопасно в смысле того, что не выполняется проверка типа. Вот, например, хорошо хорошо известный способ получить непредсказуемый вывод и/или дамп (core dump):
char x; // ... cout << form("bad input char: %s",x);Правда, она дает большую гибкость в том виде, который хорошо знаком программистам на C. Потоковый вывод можно смешивать с выводом в стиле printf().
В настоящее время нет полностью удовлетворительных средств, обеспечивающих форматированный вывод типов, определяемых пользователем* В частности, вероятно нужно будет найти стандартный способ передавать функции вывода для определяемого пользов
class complex { float re,im; public: // ... char* string(char* format) { return form(format,re,im); } }; // ... cout << z.string("(%.3f,%.3f)");Память для хранения строк, которые возвращают form(), hex() и т.п., берется из одного статически размещаемого циклического буфера, поэтому не имеет смысла сохранять указатель, возвращаемый любой из этих функций, для дальнейшего использования. Указыв
Иногда функция вывода должна быть 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;
Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.
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.
Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():
ostream::~ostream() { flush(); // сброс }Сбросить буфер можно также и явно. Например:
cout.flush();
Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах и здесь подробно не описываются. Поскольку после включения <stream.h> становятся доступны cin, cout и cerr, во многих (если не во всех) программ
#include <stream.h> 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) затем к нему подсоединяется файл (здесь
Файл может открываться в одной из двух мод:
enum open_mode { input, output };Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан.
Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии (см. #8.4.2). При завершении программы открытые файлы неявно закрываются.
Файл можно также открыть одновременно для чтения и записи, но в тех случаях, когда это оказывается необходимо, парадигма потоков редко оказывается идеальной. Часто лучше рассматривать такой файл как вектор (гигантских размеров). Можно определ
Есть возможность копировать потоки. Например:
cout = cerr;В результате этого получаются две переменные, ссылающиеся на один и тот же поток. Гавным образом это бывает полезно для того, чтобы сделать стандартное имя вроде cin ссылающимся на что-то другое (пример этого см. в #3.1.6)
Ввод аналогичен выводу. Имеется класс istream, который предоставляет операцию >> ("взять из") для небольшого множества стандартных типов. Функция operator>> может определяться для типа, определяемого пользователем.
Класс istream определяется так:
class istream { // ... public: istream& operator>>(char*);//строка istream& operator>>(char&);//символ istream& operator>>(short&); istream& operator>>(int&); istream& operator>>(long&); istream& operator>>(float&); istream& operator>>(double&); //... };Функции ввода определяются в таком духе:
istream& istream::operator>>(char& c); { // пропускает пропуски int a; // неким образом читает символ в "a" c = a; }Пропуск определяется как стандартнчй пропуск в C, через вызов isspase() в том виде, как она определена в <ctype.h> (пробел, табуляция, символ новой строки, перевод формата и возврат каретки).
В качестве альтернативы можно использовать функции get():
class istream { // ... istream& get(char& c); // char istream& get(char* p, int n,int ='\n'); // строка };Они обрабатывают символы пропуска так же, как остальные символы.
Функция istream::get(char) читает один символ в свой параметр; другая istream::get читает не более n символов в вектор символов, начинающийся в p. Необязательный третий параметр используется для задания символа остановки (иначе, терминатора или огра
cin.get(buf,256,'\t');будет читать в buf не более 256 символов, а если встретится табуляция ('\t'), то это приведет к возврату из get. В этом случае следующим символом, который будет считан из cin, будет '\t'.
Стандартный заголовочный файл <ctype.h> определяет несколько функций, которые могут оказаться полезными при осуществлении ввода:
int isalpha(char) // 'a'..'z' 'A'..'Z' int isupper(char) // 'A'..'Z' int islower(char) // 'a'..'z' int isdigit(char) // '0'..'9' int isxdigit(char) // '0'..'9' 'a'..'f' 'A'..'F' int isspase(char) // ' ' '\t' возврат новая строка // перевод формата int iscntrl(char) // управляющий символ // (ASCII 0..31 и 127) int ispunct(char) // пунктуация: ни один из вышеперечисленных int isalnum(char) // isalpha() | isdigit() int isprint(char) // печатаемый: ascii ' '..'-' int isgraph(char) // isalpha() | isdigit() | ispunct() int isascii(char c) { return 0<=c && c<=127; }Все кроме isascii() реализуются внешне одинаково, с применением символа в качестве индекса в таблице атрибутов символов. Поэтому такие выражения, как
(('a'<=c && c<='z') || ('A'<=c && c<='Z')) // алфавитныйне только утомительно пишутся и чреваты ошибками (на машине с набором символов EBCDIC оно будет принимать неалфавитные символы), они также и менее эффективны, чем применение стандартной функции:
isalpha(c)
Каждый поток (istream или ostream) имеет ассоциированное с ним состояние, и обработка ошибок и нестандартных условий осуществляется с помощью соответствующей установки и проверки этого состояния.
Поток может находиться в одном из следующих состояний:
enum stream_state { _good, _eof, _fail, _bad };Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку,
Состояние потока можно проверять например так:
switch (cin.rdstate()) { case _good: // последняя операция над cin прошла успешно break; case _eof: // конец файла break; case _fail: // некоего рода ошибка форматирования // возможно, не слишком плохая break; case _bad: // возможно, символы cin потеряны break; }Для любой переменной z типа, для которого определены операции << и >>, копирующий цикл можно написать так:
while (cin>>z) cout << z << "\n";Например, если z - вектор символов, этот цикл будет брать стандартный ввод и помещать его в стандартный вывод по одному слову (то есть, последовательности символов без пробела) на строку.
Когда в качестве условия используется поток, происходит проверка состояния потока, и эта проверка проходит успешно (то есть, значение условия не ноль) только если состояние _good. В частности, в предыдущем цикле проверялось состояние istream, ко
Делать проверку на наличие ошибок после каждого ввода или вывода действительно не очень удобно, и обычно источником ошибок служит программист, не сделавший этого в том месте, где это существенно. Например, операции вывода обычно не проверяются,
Ввод для пользовательского типа может определяться точно так же, как вывод, за тем исключением, что для операции ввода важно, чтобы второй параметр был ссылочного типа. Например:
istream& operator>>(istream& s, complex& a) /* форматы ввода для complex; "f" обозначает float: f ( f ) ( f , f ) */ { double re = 0, im = 0; char c = 0; s >> c; if (c == '(') { s >> re >> c; if (c == ',') s >> im >> c; if (c != ')') s.clear(_bad); // установить state } else { s.putback(c); s >> re; } if (s) a = complex(re,im); return s; }Несмотря на то, что не хватает кода обработки ошибок, большую часть видов ошибок это на самом деле обрабатывать будет. Локальная переменная c инициализируется, чтобы ее значение не оказалось случайно '(' после того, как операция окончится неудачно
Операция установки состояния названа clear() (очистить), потому что она чаще всего используется для установки состояния потока заново как _good. _good является значением параметра по умолчанию и для istream::clear(), и для ostream::clear().
Над операциями ввода надо поработать еще. Было бы, в частности, замечательно, если бы можно было задавать ввод в терминах образца (как в языках Snobol и Icon), а потом проверять, прошла ли успешно вся операция ввода. Такие операции должны были бы
Естественно, тип istream, так же как и ostream, снабжен конструктором:
class istream { // ... istream(streambuf* s, int sk =1, ostream* t =0); istream(int size, char* p, int sk =1); istream(int fd, int sk =1, ostream* t =0); };Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своег
cout.flush(); // пишет буфер выводаС помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream. Например:
int y_or_n(ostream& to, istream& from) /* "to", получает отклик из "from" */ { ostream* old = from.tie(&to); for (;;) { cout << "наберите Y или N: "; char ch = 0; if (!cin.get(ch)) return 0; if (ch != '\n') { // пропускает остаток строки char ch2 = 0; while (cin.get(ch2) && ch2 != '\n') ; } switch (ch) { case 'Y': case 'y': case '\n': from.tie(old); // восстанавливает старый tie return 1; case 'N': case 'n': from.tie(old); // восстанавливает старый tie return 0; default: cout << "извините, попробуйте еще раз: "; } } }Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игноиру
Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядывать вперед" в поток ввода.
Можно осуществлять действия, подобные вводу/выводу, над символьным вектором, прикрепляя к нему istream или ostream. Например, если вектор содержит обычную строку, завершающуюся нулем, для печати слов из этого вектора можно использовать приведенн
void word_per_line(char v[], int sz) /* печатет "v" размера "sz" по одному слову на строке */ { istream ist(sz,v); // сделать istream для v char b2[MAX]; // больше наибольшего слова while (ist>>b2) cout << b2 << "\n"; }Завершающий нулевой символ в этом случае интерпретируется как символ конца файла.
В помощью ostream можно отформатировать сообщения, которые не нужно печатать тотчас же:
char* p = new char[message_size]; ostream ost(message_size,p); do_something(arguments,ost); display(p);Такая операция, как do_something, может писать в поток ost, передавать ost своим подоперациям и т.д. с помощью стандартных операций вывода. Нет необходимости делать проверку на переполнение, поскольку ost знает свою длину и когда он будет переполняться,
При задании операций ввода/вывода мы никак не касались типов файлов, но ведь не все устройства можно рассматривать одинаково с точки зрения стратегии буферизации. Например, для ostream, подключенного к символьной строке, требуется буферизация другого
Описание буфера потока в <stream.h> выглядит так:
struct streambuf { // управление буфером потока char* base; // начало буфера char* pptr; // следующий свободный char char* qptr; // следующий заполненный char char* eptr; // один из концов буфера char alloc; // буфер, выделенный с помощью new // Опустошает буфер: // Возвращает EOF при ошибке и 0 в случае успеха virtual int overflow(int c =EOF); // Заполняет буфер // Возвращет EOF при ошибке или конце ввода, // иначе следующий char virtual int underflow(); int snextc() // берет следующий char { return (++qptr==pptr) ? underflow() : *qptr&0377; } // ... int allocate() // выделяет некоторое пространство буфера streambuf() { /* ... */} streambuf(char* p, int l) { /* ...*/} ~streambuf() { /* ... */} };Обратите внимание, что здесь определяются указатели, необходимые для работы с буфером, поэтому обычные посимвольные действия можно определить (только один раз) в виде максимально эффективных inline - функций. Для каждой конкретной стратегии буферизаци
struct filebuf : public streambuf { int fd; // дескриптор файла char opened; // файл открыт int overflow(int c =EOF); int underflow(); // ... // Открывает файл: // если не срабатывает, то возвращет 0, // в случае успеха возвращает "this" filebuf* open(char *name, open_mode om); int close() { /*...*/ } filebuf() { opened = 0; } filebuf(int nfd) { /* ... */ } filebuf(int nfd, char* p, int l) : (p,l) { /* ... */ } ~filebuf() { close(); } }; int filebuf::underflow() // заполняет буфер из fd { if (!opened || allocate()==EOF) return EOF; int count = read(fd, base, eptr-base); if (count < 1) return EOF; qptr = base; pptr = base + count; return *qptr & 0377; }
Можно было бы ожидать, что раз ввод/вывод <stream.h> определен с помощью общедоступных средств языка, он будет менее эффективен, чем встроенное средство. На самом деле это не так. Для действий вроде "поместить символ в поток" используются inline
Copyright © by Alex Skums