Буферизированный (потоковый) ввод-вывод
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, предварительно проверив (т.н. "валидация") формат читаемых и записываемых символов. Во всех случаях нужно жёстко контролировать итоговую длину строки ввода-вывода.