Буферизированный (потоковый) ввод-вывод
6.1. Стандартные потоки в операционных системах
В операционных системах корпорации Microsoft (начиная с версии MS-DOS 2.0) и всех "клонах" UNIX существует возможность "потокового ввода-вывода" данных, например, на консоль, на принтер или в файл.
Принцип "потокового ввода-вывода" следующий:
- В оперативной памяти средствами операционной системы создаётся некоторый "промежуточный буфер" для хранения данных, читаемых из файла, устройства или записывания на него этих данных;
- Средствами программы, созданной прикладным программистом, происходит чтение или запись информации (символов) в этот буфер;
- Средствами операционной системы осуществляется "синхронизация" этого буфера ("потока данных") с файлом или устройством;
- При создании или открытии файла для него выделяется буфер в оперативной памяти компьютера, а после закрытия файла этот буфер очищается.
В буфер заносятся только символьные данные. Как следствие, при потоковом выводе нельзя (или "практически нельзя") изменить атрибуты выводимых символов, их шрифтовое и абзацное оформление и т.д. Кроме того, в стандартные потоки ввода-вывода нельзя выводить "двоичные" файлы (то есть файлы, имеющую кодировку, отличную от ASCII и совместимой с ней кодировки). В операционных системах корпорации Microsoft при потоковом вводе-выводе для вывода на консоль кириллических символов возможна только кодировка OEM 866 (кодовая страница MS-DOS для русскоязычных пользователей). Это в настоящее время резко ограничивает применение потокового ввода-вывода для создания программ в современном программировании. Однако этот подход широко востребован в UNIX и её клонах, работающих в режиме командной строки.
Вследствие вышесказанного автор просто обязан рассказать о технологии потокового ввода-вывода. Тем более что при помощи функций sprintf и sscanf (см. Приложение №1) возможна "эмуляция потокового вывода" информации в строку текста, откуда её можно вывести средствами API системных библиотек ввода-вывода (таких как Windows API, GTK+, Qt и др.).
В качестве "стандартных потоков", присутствующих в операционной системе всегда и никогда не удаляемых из оперативной памяти, используются следующие потоки (см. таблицу 6.1):
- Стандартный поток ввода (обозначение: stdin, cin и др.) - используется для ввода символьных данных в программу. По-умолчанию этот поток закреплён за клавиатурой компьютера;
- Стандартный поток вывода (обозначается как: stdout, cout и др.) - используется для вывода символьной информации, полученной в результате работы программы в "штатном режиме". По-умолчанию этот поток закреплён за экраном дисплея;
- Стандартный поток ошибок (обозначение: stderr, cerr и др.) - используется для вывода символьных диагностических сообщений, ошибок и предупреждений, возникших в результате работы программы. По-умолчанию этот поток закреплён за экраном дисплея;
Примечание: стандартный поток и поток ошибок разделены в связи с тем, что при перенаправлении вывода часто совсем не нужно записывать в результаты работы программы диагностические сообщения. Эти сообщения будут лишними, например, при формировании таблицы базы данных в виде текстового файла;
- Стандартный поток печати (обозначение: stdprn и др.) - используется для вывода результатов работы программы на печать. По-умолчанию этот поток закреплён за текущим принтером в системе, подключённым к порту LPT1. В настоящее время этот поток почти не используется, поскольку чаще проще и безопаснее перенаправить стандартный поток вывода на принтер, чем разделять потоки отдельно для экрана и отдельно для принтера.
Все остальные потоки создаются или уничтожаются с помощью функций открытия и закрытия файлов, на период чтения/записи/добавления информации в эти файлы.
6.2. Ввод со стандартного потока ввода
6.2.1. Ввод средствами языка Си
Ввод со стандартного потока в Си осуществляется при помощи следующих функций:
- для ввода одиночного символа - функцию getchar ;
- для ввода строки символов без ограничения на длину - функцию gets ;
- для "форматированного ввода" символов и их преобразования в "двоичные значения" переменных - функцию scanf ;
С синтаксисом и правилами использования этих функций можно ознакомиться в приложении №I к данной лекции. Автор хочет отметить, что использование функций ввода со стандартного потока является небезопасным, а, следовательно, и нежелательным способом ввода данных из стандартного потока. По возможности, заменяйте эти функции функциями потокового чтения данных с "явным" указанием потоков и "длины" прочтённой строки.
Пример 6.1
/* Файл ex06001.c */ /* Функция иллюстрирует потоковый ввод-вывод (как это делать нельзя)*/ #include <stdio.h> #define STR_LENGTH 3 // Длина строки 3 символа void main() { char str[STR_LENGTH]; // Текстовый буфер char *s; // Временная переменна s = gets( str ); puts( s ); }
Например, функция, приведённая в примере 6.1, при вводе строки: "aaaaaaaa\n", - вызовет аварийное завершение с ошибкой "переполнение буфера". Поскольку длина текстового буфера - 2 символа (последний символ - "нулевой", знак окончания строки '\0' ), то лишние символы, введённые с клавиатуры, попадают в "запредельную" системную область данных. В этом случае происходит операция (прерывание) "отказ системы", и адрес этой системной области оказывается доступной "взломщику системы". Функции, приведённые в примерах 6.2 и 6.3, лишены этих недостатков;
Пример 6.2
/* Файл ex06002.c */ /* Функция иллюстрирует потоковый ввод-вывод (как это делать надо)*/ /* Пример тестировался в системе программировани Borland C/C++ 3.10 */ #include <stdio.h> #include <conio.h> #define BUF_LENGTH 7 // Длина строки 3 символа #define STR_LENGTH 2 // Длина буфера без дескриптора // Всё те же 2 символа void main() { char str[BUF_LENGTH]; // Текстовый буфер char *s, c = '\0'; // Временная переменная. int icsize; // Временная переменная. icsize = BUF_LENGTH - 2; str[0] = (char) icsize; s = cgets( str ); puts( s ); puts( "\nPress any key to continue..."); while( !( c = getch()) ); // Цикл пока не нажата клавиша }
Пример 6.3
/* Файл ex06003.c */ /* Функция иллюстрирует потоковый ввод-вывод (как это делать надо)*/ /* Пример тестировался в системе программировани Borland C/C++ 3.10 */ #include <stdio.h> #include <conio.h> #include <string.h> #define STR_LENGTH 3 // Длина строки 3 символа void main() { char str[STR_LENGTH]; // Текстовый буфер char *s, c = '\0'; // Временная переменная. int icsize; // Временная переменная. memset( str, '\0', STR_LENGTH ); // Обнуляем буфер icsize = STR_LENGTH; s = fgets( str, icsize, stdin ); // Читаем не более 2 символов с входного потока // (вместе с нулевым символом) puts( s ); puts( "\nPress any key to continue..."); while( !(c = getch()) ); // Цикл пока не нажата клавиша }
6.2.2. Ввод средствами языка C++
Консольный потоковый ввод в языке C++ осуществляется при помощи операторов ">>", а также при помощи функций get, peek и ignore. Стандартный поток ввода в языке C++ обозначается как cin (см. таблицу 6.1).
Обозначения потоков | |||||
---|---|---|---|---|---|
Поток | Ввод (стандартный) | Вывод (стандартный) | Вывод ошибок | Передача на линию | Печать |
Значение дескриптора | 0 | 1 | 2 | 3 | 4 |
Обозначение в Си | stdin | stdout | stderr | stdaux | stdprn |
Обозначение в C++ | cin | cout | cerr | нет | нет |
Обозначение в Java | System.in | System.out | ? | нет | нет |
Обозначение в Perl | STDIN | STDOUT | STDERR | нет | нет |
Обозначение в Python | sys.stdin | sys.stdout | sys.stderr | нет | нет |
Обозначение в VBScript | StdIn | StdOut | StdErr | нет | нет |
Описание функций для потокового ввода средствами языка C++ смотри в приложении №III к данной лекции.
C помощью оператора ">>" можно осуществить как неформатированный, так и форматированный ввод-вывод. Однако автор советует для ввода использовать неформатированный ввод из потока с помощью функции cin.get, а затем "выделить и ввести" нужные двоичные значения при помощи функции sscanf (см. пример 6.4.).
Пример 06.004.
/* File ex06004.cpp */ /* Пример ввода-вывода средствами C++*/ /* Пример тестировался в системе программировани Borland C/C++ 3.10 */ #include <iostream.h> #include <stdio.h> #include <conio.h> #include <string.h> #ifndef STR_LENGTH #define STR_LENGTH 35 // Длина строки 34 символа + завершающий символ '\0' #endif #ifndef INT_WIDTH #define INT_WIDTH 5 // Ширина под поле целого числа - 4 символов #endif void main( void ) { const char format_in[] = "%5d"; // Формат ввода const char format_out[] = "\nВведённое значение: %5d"; // Формат вывода char str[STR_LENGTH], c='\0'; int iValue; (void) memset( str, '\0', STR_LENGTH ); // Обнуляем строку; cout << "\nВведите число: "; cin.get( str, INT_WIDTH ); if( !sscanf( str, format_in, &iValue ) ) // Если ошибка ввода { cerr << "\nНичего не введено или неправильный формат данных!\nПрограмма завершена с ошибкой"; return; } (void) memset( str, '\0', STR_LENGTH ); // Обнуляем строку; if( sprintf( str, format_out, iValue ) == EOF ) // Если ошибка вывода { cerr << "\nНичего не выведено или неправильный формат данных!\nПрограмма завершена с ошибкой"; return; } cout << str; // Выводим сформированную строку cerr << "\nПрограмма завершена нормально"; cerr << "\nPress any key to continue..."; while( !(c = getch()) ); // Цикл пока не нажата клавиша }Листинг .
В этом примере для ввода символьной строки использовалась функция cin.get, для вывода - оператор "<<", а для форматных преобразований - функции sprintf и sscanf.
Описание функций sscanf и sprintf см. в приложении №I к данной лекции.
Замечание: из личного опыта автора родился следующий совет: при работе с консолью, когда не требуется программирование фильтров и перенаправления потоков, лучше использовать консольные функции ввода-вывода. Для программирования фильтров, наоборот, требуется ввод-вывод исключительно файловыми потоковыми функциями. В обоих случаях для форматного преобразования необходимо использовать только функции sprintf и sscanf, предварительно проверив (т.н. "валидация") формат читаемых и записываемых символов. Во всех случаях нужно жёстко контролировать итоговую длину строки ввода-вывода.