Язык C++ не обеспечивает средств для ввода/вывода. Ему это и не нужно; такие средства легко и элегантно можно создать с помощью самого языка. Описанная здесь стандартная библиотека потокового ввода/вывода обеспечивает гибкий и эффективный с гарантией типа метод обработки символьного ввода целых чисел, чисел с плавающей точкой и символьных строк, а также простую модель ее расширения для обработки типов, определяемых пользователем. Ее пользовательский интерфейс находится в . В этой главе описывается сама библиотека, некоторые способы ее применения и методы, которые использовались при ее реализации.
Разработка и реализация стандартных средств ввода/вывода для
языка программирования зарекомендовала себя как заведомо трудная
работа. Традиционно средства ввода/вывода разрабатывались
исключительно для небольшого числа встроенных типов данных. Однако
в C++ программах обычно используется много типов, определенных
пользователем, и нужно обрабатывать ввод и вывод также и значений
этих типов. Очевидно, средство ввода/вывода должно быть простым,
удобным, надежным в употреблении, эффективным и гибким, и ко всему
прочему полным. Ничье решение еще не смогло угодить всем, поэтому у
пользователя должна быть возможность задавать альтернативные
средства ввода/вывода и расширять стандартные средства ввода/вывода
применительно к требованиям приложения.
C++ разработан так, чтобы у пользователя была возможность
определять новые типы столь же эффективные и удобные, сколь и
встроенные типы. Поэтому обоснованным является требование того, что
средства ввода/вывода для C++ должны обеспечиваться в C++ с
применением только тех средств, которые доступны каждому
программисту. Описываемые здесь средства ввода/вывода представляют
собой попытку ответить на этот вызов.
Средства ввода/вывода связаны исключительно с
обработкой преобразования типизированных объектов в
последовательности символов и обратно. Есть и другие схемы
ввода/вывода, но эта является основополагающей в системе UNIX, и
большая часть видов бинарного ввода/вывода обрабатывается через
рассмотрение символа просто как набор бит, при этом его
общепринятая связь с алфавитом игнорируется. Тогда для программиста
ключевая проблема заключается в задании соответствия между
типизированным объектом и принципиально не типизированной строкой.
Обработка и встроенных и определенных пользователем типов
однородным образом и с гарантией типа достигается с помощью одного
перегруженного имени функции для набора функций вывода. Например:
put(cerr,"x = "); // cerr - поток вывода ошибок put(cerr,x); put(cerr,"\n");
cerr << "x = " << x << "\n";
x = 123
x = 1,2.4)
8.2.1 Вывод Встроенных Типов | |
8.2.2 Некоторые Подробности Разработки | |
8.2.3 Форматированный Вывод | |
8.2.4 Виртуальная Функция Вывода |
В этом разделе сначала обсуждаются средства форматного и бесформатного вывода встроенных типов, потом приводится стандартный способ спецификации действий вывода для определяемых пользователем типов.
Класс ostream определяется вместе с операцией << ("поместить в") для обработки вывода встроенных типов:
class ostream { // ... public: ostream& operator<<(char*); ostream& operator<<(int i) { return *this<
Операция вывода используется, чтобы избежать той многословности,
которую дало бы использование функции вывода. Но почему <<?
Возможности изобрести новый лексический символ нет (#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<
Пока << применялась только для неформатированного вывода, и на самом деле в реальных программах она именно для этого главным образом и применяется. Помимо этого существует также несколько форматирующих функций, создающих представление своего параметра в виде строки, которая используется для вывода. Их второй (необязательный) параметр указывает, сколько символьных позиций должно использоваться.
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) << ")";
dec(15) = oct( 17) = hex( f);
char* form(char* format ...); cout<
Иногда функция вывода должна быть 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; }
while ( p = next() ) cout << p;
8.3.1 Инициализация Потоков Вывода | |
8.3.2 Закрытие Потоков Вывода | |
8.3.3 Открытие Файлов | |
8.3.4 Копирование Потоков |
Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.
ostream имеет конструкторы:
class ostream { // ... ostream(streambuf* s); // связывает с буфером потока ostream(int fd); // связывание для файла ostream(int size, char* p); // связывет с вектором };
// описать подходящее пространство буфера 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 сбрасывает буфер с помощью открытого члена функции ostream::flush():
ostream::~ostream() { flush(); // сброс }
cout.flush();
Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах и здесь подробно не описываются. Поскольку после включения становятся доступны 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("случилось нечто странное",""); }
enum open_mode { input, output };
Есть возможность копировать потоки. Например:
cout = cerr;
8.4.1 Ввод Встроенных Типов | |
8.4.2 Состояния Потока | |
8.4.3 Ввод Типов, Определяемых Пользователем | |
8.4.4 Инициализация Потоков Ввода |
Ввод аналогичен выводу. Имеется класс 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; }
class istream { // ... istream& get(char& c); // char istream& get(char* p, int n, int ='\n'); // строка };
cin.get(buf,256,'\t');
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; }
(('a'<=c && c<='z') || ('A'<=c && c<='Z')) // алфавитный
isalpha(c)
Каждый поток (istream или ostream) имеет ассоциированное с ним
состояние, и обработка ошибок и нестандартных условий
осуществляется с помощью соответствующей установки и проверки этого
состояния.
Поток может находиться в одном из следующих состояний:
enum stream_state { _good, _eof, _fail, _bad };
switch (cin.rdstate()) { case _good: // последняя операция над cin прошла успешно break; case _eof: // конец файла break; case _fail: // некоего рода ошибка форматирования // возможно, не слишком плохая break; case _bad: // возможно, символы cin потеряны break; }
while (cin>>z) cout << z << "\n";
Ввод для пользовательского типа может определяться точно так же, как вывод, за тем исключением, что для операции ввода важно, чтобы второй параметр был ссылочного типа. Например:
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; }
Естественно, тип 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); };
cout.flush(); // пишет буфер вывода
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 << "извините, попробуйте еще раз: "; } } }
Можно осуществлять действия, подобные вводу/выводу, над символьным вектором, прикрепляя к нему 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"; }
char* p = new char[message_size]; ostream ost(message_size,p); do_something(arguments,ost); display(p);
При задании операций ввода/вывода мы никак не касались типов файлов, но ведь не все устройства можно рассматривать одинаково с точки зрения стратегии буферизации. Например, для ostream, подключенного к символьной строке, требуется буферизация другого вида, нежели для ostream, подключенного к файлу. С этими проблемами можно справиться, задавая различные буферные типы для разных потоков в момент инициализации (обратите внимание на три конструктора класса ostream). Есть только один набор операций над этими буферными типами, поэтому в функциях ostream нет кода, их различающего. Однако функции, которые обрабатывают переполнение сверху и снизу, виртуальные. Этого достаточно, чтобы справляться с необходимой в данное время стратегией буферизации. Это также служит хорошим примером применения виртуальных функций для того, чтобы сделать возможной однородную обработку логически эквивалентных средств с различной реализацией. Описание буфера потока в выглядит так:
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() { /* ... */} };
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; }
Можно было бы ожидать, что раз ввод/вывод определен с помощью общедоступных средств языка, он будет менее эффективен, чем встроенное средство. На самом деле это не так. Для действий вроде "поместить символ в поток" используются inline-функции, единственные необходимые на этом уровне вызовы функций возникают из-за переполнения сверху и снизу. Для простых объектов (целое, строка и т.п.) требуется по одному вызову на каждый. Как выясняется, это не отличается от прочих средств ввода/вывода, работающих с объектами на этом уровне.
Релятивисты и позитивисты утверждают, что "мысленный эксперимент" весьма полезный интрумент для проверки теорий (также возникающих в нашем уме) на непротиворечивость. В этом они обманывают людей, так как любая проверка может осуществляться только независимым от объекта проверки источником. Сам заявитель гипотезы не может быть проверкой своего же заявления, так как причина самого этого заявления есть отсутствие видимых для заявителя противоречий в заявлении.
Это мы видим на примере СТО и ОТО, превратившихся в своеобразный вид религии, управляющей наукой и общественным мнением. Никакое количество фактов, противоречащих им, не может преодолеть формулу Эйнштейна: "Если факт не соответствует теории - измените факт" (В другом варианте " - Факт не соответствует теории? - Тем хуже для факта").
Максимально, на что может претендовать "мысленный эксперимент" - это только на внутреннюю непротиворечивость гипотезы в рамках собственной, часто отнюдь не истинной логики заявителя. Соответсвие практике это не проверяет. Настоящая проверка может состояться только в действительном физическом эксперименте.
Эксперимент на то и эксперимент, что он есть не изощрение мысли, а проверка мысли. Непротиворечивая внутри себя мысль не может сама себя проверить. Это доказано Куртом Гёделем.
Понятие "мысленный эксперимент" придумано специально спекулянтами - релятивистами для шулерской подмены реальной проверки мысли на практике (эксперимента) своим "честным словом". Подробнее читайте в FAQ по эфирной физике.
|
![]() |