Потоки
10.5.3 Буферизация
Все операции ввода-вывода были определены без всякой связи с типом файла, но нельзя одинаково работать со всеми устройствами без учета алгоритма буферизации. Очевидно, что потоку ostream, привязанному к строке символов, нужен не такой буфер, как ostream, привязанному к файлу. Такие вопросы решаются созданием во время инициализации разных буферов для потоков разных типов. Но существует только один набор операций над этими типами буферов, поэтому в ostream нет функций, код которых учитывает различие буферов. Однако, функции, следящие за переполнением и обращением к пустому буферу, являются виртуальными. Это хороший пример применения виртуальных функций для единообразной работы с эквивалентными логически, но различно реализованными структурами, и они вполне справляются с требуемыми алгоритмами буферизации. Описание буфера потока в файле <iostream.h> может выглядеть следующим образом:
class streambuf { // управление буфером потока protected: char* base; // начало буфера char* pptr; // следующий свободный байт char* gptr; // следующий заполненный байт char* eptr; // один из указателей на конец буфера char alloc; // буфер, размещенный с помощью "new" //... // Опустошить буфер: // Вернуть EOF при ошибке, 0 - удача virtual int overflow(int c = EOF); // Заполнить буфер: // Вернуть EOF в случае ошибки или конца входного потока, // иначе вернуть очередной символ virtual int underflow(); //... public: streambuf(); streambuf(char* p, int l); virtual ~streambuf(); int snextc() // получить очередной символ { return (++gptr==pptr) ? underflow() : *gptr&0377; } int allocate(); // отвести память под буфер //... };
Подробности реализации класса streambuf приведены здесь только для полноты представления. Не предполагается, что есть общедоступные реализации, использующие именно эти имена. Обратите внимание на определенные здесь указатели, управляющие буфером; с их помощью простые посимвольные операции с потоком можно определить максимально эффективно (и причем однократно) как функции-подстановки. Только функции overflow() и underflow() требует своей реализации для каждого алгоритма буферизации, например:
class filebuf : public streambuf { protected: int fd; // дескриптор файла char opened; // признак открытия файла public: filebuf() { opened = 0; } filebuf(int nfd, char* p, int l) : streambuf(p,l) { /* ... */ } ~filebuf() { close(); } int overflow(int c=EOF); int underflow(); filebuf* open(char *name, ios::open_mode om); int close() { /* ... */ } //... }; int filebuf::underflow() // заполнить буфер из "fd" { if (!opened || allocate()==EOF) return EOF; int count = read(fd, base, eptr-base); if (count < 1) return EOF; gptr = base; pptr = base + count; return *gptr & 0377; // &0377 предотвращает размножение знака }
За дальнейшими подробностями обратитесь к руководству по реализации класса streambuf.
10.6 Ввод-вывод в С
Поскольку текст программ на С и на С++ часто путают, то путают иногда и потоковый ввод-вывод С++ и функции ввода-вывода семейства printf для языка С. Далее, т.к. С-функции можно вызывать из программы на С++, то многие предпочитают использовать более знакомые функции ввода-вывода С.
По этой причине здесь будет дана основа функций ввода-вывода С. Обычно операции ввода-вывода на С и на С++ могут идти по очереди на уровне строк. Перемешивание их на уровне посимвольного ввода-вывода возможно для некоторых реализаций, но такая программа может быть непереносимой. Некоторые реализации потоковой библиотеки С++ при допущении ввода-вывода на С требуют вызова статической функции-члена ios::sync_with_stdio().
В общем, потоковые функции вывода имеют перед стандартной функцией С printf() то преимущество, что потоковые функции обладают определенной типовой надежностью и единообразно определяют вывод объектов предопределенного и пользовательского типов.
Основная функция вывода С есть
int printf(const char* format, ...)
и она выводит произвольную последовательность параметров в формате, задаваемом строкой форматирования format. Строка форматирования состоит из объектов двух типов: простые символы, которые просто копируются в выходной поток, и спецификации преобразований, каждая из которых преобразует и печатает очередной параметр. Каждая спецификация преобразования начинается с символа %, например
printf("there were %d members present.",no_of_members);
Здесь %d указывает, что no_of_members следует считать целым и печатать как соответствующую последовательность десятичных цифр. Если no_of_members==127, то будет напечатано
there were 127 members present.
Набор спецификаций преобразований достаточно большой и обеспечивает большую гибкость печати. За символом % может следовать:
- - необязательный знак минус, задающий выравнивание влево в указанном поле для преобразованного значения;
- d необязательная строка цифр, задающая ширину поля; если в преобразованном значении меньше символов, чем ширина строки, то оно дополнится до ширины поля пробелами слева (или справа, если дана спецификация выравнивания влево); если строка ширины поля начинается с нуля, то дополнение будет проводится нулями, а не пробелами;
- . необязательный символ точка служит для отделения ширины поля от последующей строки цифр;
- d необязательная строка цифр, задающая точность, которая определяет число цифр после десятичной точки для значений в спецификациях e или f, или же задает максимальное число печатаемых символов строки;
- * для задания ширины поля или точности может использоваться * вместо строки цифр. В этом случае должен быть параметр целого типа, который содержит значение ширины поля или точности;
- h необязательный символ h указывает, что последующая спецификация d, o, x или u относится к параметру типа короткое целое;
- l необязательный символ l указывает, что последующая спецификация d, o, x или u относится к параметру типа длинное целое;
- % обозначает, что нужно напечатать сам символ %; параметр не нужен;
-
c символ, указывающий тип требуемого преобразования.Символы преобразования и их смысл следующие:
- d Целый параметр выдается в десятичной записи;
- o Целый параметр выдается в восьмеричной записи;
- x Целый параметр выдается в шестнадцатеричной записи;
- f Вещественный или с двойной точностью параметр выдается в десятичной записи вида [-]ddd.ddd, где число цифр после точки равно спецификации точности для параметра. Если точность не задана, печатается шесть цифр; если явно задана точность 0, точка и цифры после нее не печатаются;
- e Вещественный или с двойной точностью параметр выдается в десятичной записи вида [-]d.ddde+dd; здесь одна цифра перед точкой, а число цифр после точки равно спецификации точности для параметра; если она не задана печатается шесть цифр;
- g Вещественный или с двойной точностью параметр печатается по той спецификации d, f или e, которая дает большую точность при меньшей ширине поля;
- c Символьный параметр печатается. Нулевые символы игнорируются;
- s Параметр считается строкой (символьный указатель), и печатаются символы из строки до нулевого символа или до достижения числа символов, равного спецификации точности; но, если точность равна 0 или не указана, печатаются все символы до нулевого;
- p Параметр считается указателем и его вид на печати зависит от реализации;
- u Беззнаковый целый параметр печатается в десятичной записи. Несуществующее поле или поле с шириной, меньшей реальной, приведет к усечению поля. Дополнение пробелами происходит, если только спецификация ширины поля больше реальной ширины.
Ниже приведен более сложный пример:
char* src_file_name; int line; char* line_format = "\n#line %d \"%s\"\n"; main() { line = 13; src_file_name = "C++/main.c"; printf("int a;\n"); printf(line_format,line,src_file_name); printf("int b;\n"); }
в котором печатается
int a; #line 13 "C++/main.c" int b;
Использование printf() ненадежно в том смысле, что нет никакого контроля типов. Так, ниже приведен известный способ получения неожиданного результата - печати мусорного значения или чего похуже:
char x; // ... printf("bad input char: %s",x);
Однако, эти функции обеспечивают большую гибкость и знакомы программирующим на С.
Как обычно, getchar() позволяет знакомым способом читать символы из входного потока:
int i;: while ((i=getchar())!=EOF) { // символьный ввод C // используем i }
Обратите внимание: чтобы было законным сравнение с величиной EOF типа int при проверке на конец файла, результат getchar() надо помещать в переменную типа int, а не char.
За подробностями о вводе-выводе на С отсылаем к вашему руководству по С или книге Кернигана и Ритчи "Язык программирования С".
10.7 Упражнения
- (*1.5) Читая файл вещественных чисел, составлять из пар прочитанных чисел комплексные числа, записать комплексные числа.
- (*1.5) Определить тип name_and_address (тип_и_адрес). Определить для него << и >>. Написать программу копирования объектов потока name_and_address.
- (*2) Разработать несколько функций для запроса и чтения данных разных типов. Предложения: целое, вещественное число, имя файла, почтовый адрес, дата, личная информация, и т.п. Попытайтесь сделать их устойчивыми к ошибкам.
- (*1.5) Напишите программу, которая печатает: (1) строчные буквы, (2) все буквы, (3) все буквы и цифры, (4) все символы, входящие в идентификатор в вашей версии С++, (5) все знаки пунктуации, (6) целые значения всех управляющих символов, (7) все обобщенные пробелы, (8) целые значения всех обобщенных пробелов, и, наконец, (9) все изображаемые символы.
- (*4) Реализуйте стандартную библиотеку ввода-вывода С (<stdio.h>) с помощью стандартной библиотеки ввода-вывода С++ (<iostream.h>).
- (*4) Реализуйте стандартную библиотеку ввода-вывода С++ (<iostream.h>) с помощью стандартной библиотеки ввода-вывода С (<stdio.h>).
- (*4) Реализуйте библиотеки С и С++ так, чтобы их можно было использовать одновременно.
- (*2) Реализуйте класс, для которого операция [] перегружена так, чтобы обеспечить произвольное чтение символов из файла.
- (*3) Повторите упражнение 8, но добейтесь, чтобы операция [] была применима для чтения и для записи. Подсказка: пусть [] возвращает объект "дескриптор типа", для которого присваивание означает: присвоить через дескриптор файлу, а неявное приведение к типу char означает чтение файла по дескриптору.
- (*2) Повторите упражнение 9, позволяя операции [] индексировать объекты произвольных типов, а не только символы.
- (*3.5) Продумайте и реализуйте операцию форматного ввода. Используйте для задания формата строку спецификаций как в printf(). Должна быть возможность попыток применения нескольких спецификаций для одного ввода, чтобы найти требуемый формат. Класс форматного ввода должен быть производным класса istream.
- (*4) Придумайте (и реализуйте) лучшие форматы ввода.
- (**2) Определите для вывода манипулятор based с двумя параметрами: система счисления и целое значение, и печатайте целое в представлении, определяемом системой счисления. Например, based(2,9) напечатает 1001.
- (**2) Напишите "миниатюрную" систему ввода-вывода, которая реализует классы istream, ostream, ifstream, ofstream и предоставляет функции, такие как operator<<() и operator>>() для целых, и операции, такие как open() и close() для файлов. Используйте исключительные ситуации, а не переменные состояния, для сообщения об ошибках.
- (**2) Напишите манипулятор, который включает и отключает эхо символа.