                                     Глава 8 Потоки
           
             Язык С++ не обеспечивает средств для ввода/вывода.  Ему это и  не
           нужно. Такие средства легко и элегантно можно создать с помощью са-
           мого языка.  Описанная здесь стандартная библиотека потокового вво-
           да/вывода обеспечивает гибкий и эффективный,  с гарантией типа, ме-
           тод обработки символьного ввода целых чисел, чисел с плавающей точ-
           кой  и  символьных строк,  а также простую модель ее расширения для
           обработки типов,  определяемых пользователем.  Ее  пользовательский
           интерфейс  находится  в  <stream.h>.  В этой главе описывается сама
           библиотека,  некоторые способы  ее  применения  и  методы,  которые
           использовались при ее реализации.
                                     8.1 Введение
           
             Разработка и реализация стандартных средств ввода/вывода для язы-
           ка программирования зарекомендовала себя как заведомо трудная рабо-
           та. Традиционно средства ввода/вывода разрабатывались исключительно
           для небольшого числа встроенных типов данных. Однако в С++ програм-
           мах обычно используется много типов,  определяемых пользователем, и
           нужно  обрабатывать ввод и вывод также и значений этих типов.  Оче-
           видно, средство ввода/вывода должно быть простым, удобным, надежным
           в  употреблении,  эффективным и гибким,  и ко всему прочему полным.
           Ничье решение еще не смогло угодить всем,  поэтому  у  пользователя
           должна  быть возможность задавать альтернативные средства ввода/вы-
           вода и расширять стандартные средства ввода/вывода применительно  к
           требованиям приложения.
           
             С++ разработан так, чтобы у пользователя была возможность опреде-
           лять новые типы столь же эффективные и удобные,  сколь и встроенные
           типы.  Поэтому обоснованным является требование того,  что средства
           ввода/вывода для С++ должны  обеспечиваться  в  С++  с  применением
           только тех средств, которые доступны каждому программисту. Описыва-
           емые здесь средства ввода/вывода представляют собой  попытку  отве-
           тить на этот вызов.
           
             Средства ввода/вывода <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 определена опера-
           ция <<,  и пользователь может определять операцию << для нового ти-
           па.
                8.2 Вывод
           
             В этом разделе сначала обсуждаются средства форматного и  бесфор-
           матного  вывода  встроенных  типов,  потом  приводится  стандартный
           способ спецификации действий вывода для определяемых  пользователем
           типов.
           
                8.2.1 Вывод Встроенных Типов
           
             Класс 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 может неявно преобразовываться в long. С
           другой стороны, int может преобразовываться также и в double. Нали-
           чие  ostream::operator<<(int)  позволяет  избежать этой неоднознач-
           ности.  Для печати символов в виде символов предоставляется функция
           ostream::put(char),  а  ostream::operator<<(int)  печатает их целые
           значения.
           
                8.2.2 Вывод Определяемых Пользователем Типов
           
             Рассмотрим определяемый пользователем тип:
           
             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,  ни доступа к
           структуре данных (скрытой),  которую этот класс поддерживает. Очень
           удачно,  что имеет место первое, потому что описание класса ostream
           находится в стандартных заголовочных файлах,  к которым у  обычного
           пользователя нет доступа на запись.  Последнее также важно,  потому
           что обеспечивает защиту от случайной порчи  структуры  данных.  Это
           также  позволяет  менять  реализацию  ostream не влияя на пользова-
           тельские программы.
           
                8.2.3 Некоторые Подробности Разработки
           
             Операция вывода используется,  чтобы избежать той многословности,
           которую дало бы использование функции вывода. Но почему <<?
           
             Возможности изобрести новый лексический символ нет (#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<<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;
                8.2.4 Форматированный Вывод
           
             Пока << применялась только для неформатированного  вывода,  и  на
           самом деле в реальных программах она именно для этого главным обра-
           зом и применяется. Помимо этого существует также несколько формати-
           рующих  функций,  создающих  представление  своего параметра в виде
           строки, которая используется для вывода. Их второй (необязательный)
           параметр  указывает,  сколько  символьных  позиций должно использо-
           ваться.
           
             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() возвращает строку, получаемую в результа-
           те преобразования и форматирования  ее  параметров,  которые  стоят
           после первого управляющего параметра - строки формата format. Стро-
           ка формата состоит из объектов двух типов:  обычных символов, кото-
           рые  просто копируются в поток вывода,  и спецификаций преобразова-
           ния, каждая из которых влечет преобразование и печать следующего из
           параметров. Каждая спецификация преобразования начинается с символа
           %. Например:
           
             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 символ,  указывающий,  какой тип преобразования  должен  приме-
                няться. Символы преобразования и их значения таковы:
           
                d целый параметр преобразуется в десятичную запись;
           
                o целый параметр преобразуется в восьмеричную запись;
           
                x целый параметр преобразуется в шестнадцатиричную запись;
           
                f параметр  float или double преобразуется в десятичную запись
                     вида [-]ddd.ddd,  где число,  задаваемое цифрами d  после
                     десятичной точки,  эквивалентно спецификации точности для
                     параметра. Если точность опущена, дается шесть цифр; если
                     точность  явно задана как 0,  то не печатается десятичная
                     точка и не печатается ни одной цифры;
           
                e параметр float или double преобразуется в десятичную  запись
                     вида [-]d.ddde+dd, где перед десятичной точкой стоит одна
                     цифра,  а число, задаваемое цифрами после десятичной точ-
                     ки,  эквивалентно  спецификации  точности  для параметра;
                     когда точность опущена, выдается шесть цифр;
           
                g параметр float или double печатается в том из видов d,f  или
                     e,  который  обеспечивает полную точность при минимальной
                     затрате места;
                c печатается символьный параметр, пустые символы игнорируются;
           
                s параметр воспринимается как строка (указатель на символ),  и
                     печатаются символы из строки до пустого  символа  или  до
                     тех пор, пока не будет достигнуто число символов, указан-
                     ное спецификацией точности;  но если точность равна нулю,
                     печатаются все символы до пустого;
           
                u беззнаковый  целый  параметр  преобразуется в десятичную за-
                     пись.
           
             Несуществующая или недостаточная ширина поля никогда не  приводит
                к обрезанию поля;  дополнение поля записи имеет место только в
                том случае,  если указанная ширина поля превышает  фактическую
                ширину.
           
             Вот более сложный пример:
           
             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().
           
             В настоящее   время  нет  полностью  удовлетворительных  средств,
           обеспечивающих форматированный вывод типов,  определяемых пользова-
           телем*  В частности,  вероятно нужно будет найти стандартный способ
           передавать функции вывода для определяемого пользователем типа  ин-
           формацию, которая позволит ей определить пространственные ограниче-
           ния,  вид заполнения,  левое или правое выравнивание и т.п. такими,
           какими  они  определяются в ее вызове.  Вполне осуществимый,  но не
           идеальный подход состоит в том, чтобы снабжать определяемый пользо-
           вателем тип функциями,  которые порождают соответствующее строковое
           представление объекта, для которого они вызываются, аналогично фор-
           матирующим функциям oct(), hex() и т.д. Например:
           
             class complex { float re,im;
             public:
                 // ...
                 char* string(char* format) { return form(format,re,im); }
             };
             // ...
             cout << z.string("(%.3f,%.3f)");
           
             Память для  хранения  строк,  которые возвращают form(),  hex() и
           т.п.,  берется из одного статически размещаемого циклического буфе-
           ра, поэтому не имеет смысла сохранять указатель, возвращаемый любой
           из этих функций, для дальнейшего использования. Указываемые символы
           будут меняться.
                8.2.5 Виртуальная Функция Вывода
           
             Иногда функция вывода  должна  быть  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 Файлы и Потоки
           
             Потоки обычно связаны с файлами. Библиотека потоков создает стан-
           дартный поток ввода 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 Открытие Файлов
           
             Точные детали того, как открываются и закрываются файлы, различа-
           ются в разных операционных системах и здесь  подробно  не  описыва-
           ются. Поскольку после включения <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)   затем   к   нему  подсоединяется  файл  (здесь  это  делается
           посредством открытия файла с помощью функции  filebuf::open());  и,
           накрнец,  (3) создается сам ostream с filebuf в качестве параметра.
           Потоки ввода обрабатываются аналогично.
           
             Файл может открываться в одной из двух мод:
           
             enum open_mode { input, output };
           
             Действие filebuf::open() возвращает 0, если не может открыть файл
           в  соответствие  с требованием.  Если пользователь пытается открыть
           файл, которого не существует для output, он будет создан.
             Перед завершением программа проверяет, находятся ли потоки в при-
           емлемом состоянии (см.  #8.4.2).  При завершении программы открытые
           файлы неявно закрываются.
           
             Файл можно  также открыть одновременно для чтения и записи,  но в
           тех случаях,  когда это оказывается необходимо,  парадигма  потоков
           редко  оказывается идеальной.  Часто лучше рассматривать такой файл
           как вектор (гигантских размеров).  Можно  определить  тип,  который
           позволяет программе обрабатывать файл как вектор, см. Упражнения 8-
           10.
           
                8.3.4 Копирование Потоков
           
             Есть возможность копировать потоки. Например:
           
             cout = cerr;
           
             В результате этого получаются две переменные, ссылающиеся на один
           и тот же поток.  Гавным образом это бывает полезно для того,  чтобы
           сделать стандартное имя вроде  cin  ссылающимся  на  что-то  другое
           (пример этого см. в #3.1.6)
           
                8.4 Ввод
           
             Ввод аналогичен выводу. Имеется класс istream, который предостав-
           ляет операцию >> ("взять из") для небольшого множества  стандартных
           типов. Функция operator>> может определяться для типа, определяемо-
           го пользователем.
           
                8.4.1 Ввод Встроенных Типов
           
             Класс 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. Необязательный третий параметр используется для за-
           дания символа остановки (иначе,  терминатора или ограничителя),  то
           есть этот символ читаться не будет.  Если будет встречен символ ог-
           раничитель,  он останется как первый символ  потока.  По  умолчанию
           вторая  функция  get  будет читать самое большее n символов,  но не
           больше чем одну строку,  '\n' является ограничителем по  умолчанию.
           Необязательный  третий параметр задает символ,  который читаться не
           будет. Например:
           
             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)
                8.4.2 Состояния Потока
           
             Каждый поток (istream или ostream) имеет  ассоциированное  с  ним
           состояние,  и  обработка ошибок и нестандартных условий осуществля-
           ется с помощью соответствующей установки и проверки  этого  состоя-
           ния.
           
             Поток может находиться в одном из следующих состояний:
           
             enum stream_state { _good, _eof, _fail, _bad };
           
             Если состояние  _good  или _eof,  значит последняя операция ввода
           прошла успешно.  Если состояние _good,  то следующая операция ввода
           может  пройти успешно,  в противном случае она закончится неудачей.
           Другими словами, применение операции ввода к потоку, который не на-
           ходится в состоянии _good, является пустой операцией. Если делается
           попытка читать в переменную v,  и операция  оканчивается  неудачей,
           значение v должно остаться неизменным (оно будет неизменным, если v
           имеет один из тех типов,  которые обрабатываются функциями  членами
           istream или ostream).  Отличие между состояниями _fail и _bad очень
           незначительно и предсавляет интерес только для разработчиков опера-
           ций ввода.  В состоянии _fail предполагается, что поток не испорчен
           и никакие символы не потеряны.  В состоянии _bad может быть все что
           угодно.
           
             Состояние потока можно проверять например так:
           
             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,  которое возвращает
           cin>>z. Чтобы обнаружить, почему цикл или проверка закончились неу-
           дачно,  можно исследовать состояние. Такая проверка потока реализу-
           ется операцией преобразования (#6.3.2).
             Делать проверку  на наличие ошибок после каждого ввода или вывода
           действительно не очень удобно,  и обычно источником  ошибок  служит
           программист,  не сделавший этого в том месте,  где это существенно.
           Например,  операции вывода обычно не проверяются, но они могут слу-
           чайно  не  сработать.  Парадигма потока ввода/вывода построена так,
           чтобы когда в С++ появится (если это произойдет) механизм обработки
           исключительных  ситуаций  (как  средство  языка или как стандартная
           библиотека), его будет легко применить для упрощения и стандартиза-
           ции обработки ошибок в потоках ввода/вывода.
           
                8.4.3 Ввод Типов, Определяемых Пользователем
           
             Ввод для  пользовательского типа может определяться точно так же,
           как вывод,  за тем исключением, что для операции ввода важно, чтобы
           второй параметр был ссылочного типа. Например:
           
             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 инициализируется,  чтобы ее значение не оказалось слу-
           чайно '(' после того,  как операция окончится неудачно. Завершающая
           проверка состояния потока гарантирует, что значение параметра a бу-
           дет изменяться только в том случае, если все идет хорошо.
           
             Операция установки  состояния названа clear() (очистить),  потому
           что она чаще всего используется для установки состояния потока  за-
           ново  как _good.  _good является значением параметра по умолчанию и
           для istream::clear(), и для ostream::clear().
           
             Над операциями ввода надо поработать еще.  Было бы,  в частности,
           замечательно,  если  бы можно было задавать ввод в терминах образца
           (как в языках Snobol и Icon),  а потом проверять, прошла ли успешно
           вся операция ввода. Такие операции должны были бы, конечно, обеспе-
           чивать некоторую дополнительную буферизацию, чтобы они могли восса-
           навливать  поток ввода в его исходное состояние после неудачной по-
           пытки распознавания.
                8.4.4 Инициализация Потоков Ввода
           
             Естественно, тип 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;  это значит,
           что перед тем,  как попытаться читать символы из своего файла,  cin
           выполняет
           
             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).  Это позволяет программе "заглядывать  впе-
           ред" в поток ввода.
                8.5 Работа со Строками
           
             Можно осуществлять действия,  подобные вводу/выводу, над символь-
           ным вектором, прикрепляя к нему 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 знает свою длину и когда он будет  переполняться,  он  будет
           переходить  в  состояние _fail.  И,  наконец,  display может писать
           сообщения в "настоящий" поток вывода.  Этот метод  может  оказаться
           наиболее полезным,  чтобы справляться с ситуациями, в которых окон-
           чательное отображение данных включает в себя нечто  более  сложное,
           чем работу с традиционным построчным устройством вывода.  Например,
           текст из ost мог бы помещаться в располагающуюся где-то  на  экране
           область фиксированного размера.
           
                8.6 Буферизация
           
             При задании операций ввода/вывода мы никак не касались типов фай-
           лов, но ведь не все устройства можно рассматривать одинаково с точ-
           ки зрения стратегии буферизации.  Например, для ostream, подключен-
           ного к символьной строке, требуется буферизация другого вида, неже-
           ли  для  ostream,  подключенного  к файлу.  С этими пробемами можно
           справиться,  задавая различные буферные типы для разных  потоков  в
           момент  инициализации (обратите внимание на три конструктора класса
           ostream). Есть только один набор операций над этими буферными типа-
           ми,  поэтому в функциях 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-
           функций. Для каждой конкретной стратегии буферизации необходимо оп-
           ределять только функции переполнения overflow() и underflow(). Нап-
           ример:
           
             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;
             }
           
                8.7 Эффективность
           
             Можно было бы ожидать,  что раз ввод/вывод <stream.h> определен с
           помощью общедоступных средств языка, он будет менее эффективен, чем
           встроенное средство.  На самом деле это не так.  Для действий вроде
           "поместить символ в поток" используются inline-функции,  единствен-
           ные необходимые на этом уровне вызовы функций возникают из-за пере-
           полнения  сверху  и  снизу.  Для простых объектов (целое,  строка и
           т.п.) требуется по одному вызову на каждый.  Как выясняется, это не
           отличается  от прочих средств ввода/вывода,  работающих с объектами
           на этом уровне.
           
                8.8 Упражнения
           
             1. (*1.5) Считайте файл чисел с плавающей  точкой,  составьте  из
                пар  считанных  чисел комплексные числа и выведите комплексные
                числа.
           
             2. (*1.5) Определите тип name_and_address (имя_и_адрес).  Опреде-
                лите   для   него   <<   и   >>.   Скопируйте  поток  объектов
                name_and_address.
           
             3. (*2) Постройте несколько функций для запроса и чтения  различ-
                ного  вида информации.  Простейший пример - функция y_or_n() в
                #8.4.4. Идеи: целое, число с плавающей точкой, имя файла, поч-
                товый адрес,  дата,  личные данные и т.д. Постарайтесь сделать
                их защищенными от дурака.
           
             4. (*1.5) Напишите программу,  которая печатает (1) все  буквы  в
                нижнем регистре, (2) все буквы, (3) все буквы и цифры, (4) все
                символы,  которые могут встречаться в идентификаторах  С++  на
                вашей системе,  (5) все символы пунктуации, (6) целые значения
                всех управляющих символов, (7) все символы пропуска, (8) целые
                значения всех символов пропуска, и (9) все печатаемые символы.
           
             5. (*4)   Реализуйте   стандартную   библиотеку   ввода/вывода  C
                (<stdio.h>) с помощью стандартной библиотеки ввода/вывода  С++
                (<stream.h>).
           
             6. (*4)   Реализуйте   стандартную  библиотеку  ввода/вывода  С++
                (<stream.h>) с помощью стандартной библиотеки  ввода/вывода  C
                (<stdio.h>).
           
             7. (*4) Реализуйте стандартные библиотеки C и С++ так,  чтобы они
                могли использоваться одновременно.
             8. (*2) Реализуйте класс,  для которого [] перегружено для реали-
                зации случайного чтения символов из файла.
           
             9. (*3) Как Упражнение 8,  только сделайте,  чтобы [] работало  и
                для чтения,  и для записи. Подсказка: сделайте, чтобы [] возв-
                ращало объект "дескрипторного типа", для которого присваивание
                означало бы присвоить файлу через дескриптор, а неявное преоб-
                разование в char означало бы чтение из файла через дескриптор.
           
             10. (*2) Как Упражнение 9,  только разрешите [] индексировать за-
                писи некоторого вида, а не символы.
           
             11. (*3) Сделайте обобщенный вариант класса,  определенного в Уп-
                ражнении 10.
           
             12. (*3.5) Разработайте и реализуйте операцию ввода по  сопостав-
                лению с образцом.  Для спецификации образца используйте строки
                формата в духе printf. Должна быть возможность попробовать со-
                поставить  со  вводом несколько образцов для нахождения факти-
                ческого формата.  Можно было бы вывести класс ввода по образцу
                из istream.
           
             13. (*4) Придумайте (и реализуйте) вид образцов,  которые намного
                лучше.
