Опубликован: 15.11.2010 | Уровень: для всех | Доступ: платный
Лекция 6:

Буферизированный (потоковый) ввод-вывод

Аннотация: На этой лекции Вы познакомитесь с "потоковым" вводом-выводом данных, и подробно рассмотрите операцию потокового ввода с клавиатуры.

6.1. Стандартные потоки в операционных системах

В операционных системах корпорации Microsoft (начиная с версии MS-DOS 2.0) и всех "клонах" UNIX существует возможность "потокового ввода-вывода" данных, например, на консоль, на принтер или в файл.

Принцип "потокового ввода-вывода" следующий:

  1. В оперативной памяти средствами операционной системы создаётся некоторый "промежуточный буфер" для хранения данных, читаемых из файла, устройства или записывания на него этих данных;
  2. Средствами программы, созданной прикладным программистом, происходит чтение или запись информации (символов) в этот буфер;
  3. Средствами операционной системы осуществляется "синхронизация" этого буфера ("потока данных") с файлом или устройством;
  4. При создании или открытии файла для него выделяется буфер в оперативной памяти компьютера, а после закрытия файла этот буфер очищается.

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

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