Опубликован: 10.10.2006 | Доступ: свободный | Студентов: 6134 / 468 | Оценка: 4.26 / 3.88 | Длительность: 31:30:00
Лекция 10:

Потоки

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. (*1.5) Читая файл вещественных чисел, составлять из пар прочитанных чисел комплексные числа, записать комплексные числа.
  2. (*1.5) Определить тип name_and_address (тип_и_адрес). Определить для него << и >>. Написать программу копирования объектов потока name_and_address.
  3. (*2) Разработать несколько функций для запроса и чтения данных разных типов. Предложения: целое, вещественное число, имя файла, почтовый адрес, дата, личная информация, и т.п. Попытайтесь сделать их устойчивыми к ошибкам.
  4. (*1.5) Напишите программу, которая печатает: (1) строчные буквы, (2) все буквы, (3) все буквы и цифры, (4) все символы, входящие в идентификатор в вашей версии С++, (5) все знаки пунктуации, (6) целые значения всех управляющих символов, (7) все обобщенные пробелы, (8) целые значения всех обобщенных пробелов, и, наконец, (9) все изображаемые символы.
  5. (*4) Реализуйте стандартную библиотеку ввода-вывода С (<stdio.h>) с помощью стандартной библиотеки ввода-вывода С++ (<iostream.h>).
  6. (*4) Реализуйте стандартную библиотеку ввода-вывода С++ (<iostream.h>) с помощью стандартной библиотеки ввода-вывода С (<stdio.h>).
  7. (*4) Реализуйте библиотеки С и С++ так, чтобы их можно было использовать одновременно.
  8. (*2) Реализуйте класс, для которого операция [] перегружена так, чтобы обеспечить произвольное чтение символов из файла.
  9. (*3) Повторите упражнение 8, но добейтесь, чтобы операция [] была применима для чтения и для записи. Подсказка: пусть [] возвращает объект "дескриптор типа", для которого присваивание означает: присвоить через дескриптор файлу, а неявное приведение к типу char означает чтение файла по дескриптору.
  10. (*2) Повторите упражнение 9, позволяя операции [] индексировать объекты произвольных типов, а не только символы.
  11. (*3.5) Продумайте и реализуйте операцию форматного ввода. Используйте для задания формата строку спецификаций как в printf(). Должна быть возможность попыток применения нескольких спецификаций для одного ввода, чтобы найти требуемый формат. Класс форматного ввода должен быть производным класса istream.
  12. (*4) Придумайте (и реализуйте) лучшие форматы ввода.
  13. (**2) Определите для вывода манипулятор based с двумя параметрами: система счисления и целое значение, и печатайте целое в представлении, определяемом системой счисления. Например, based(2,9) напечатает 1001.
  14. (**2) Напишите "миниатюрную" систему ввода-вывода, которая реализует классы istream, ostream, ifstream, ofstream и предоставляет функции, такие как operator<<() и operator>>() для целых, и операции, такие как open() и close() для файлов. Используйте исключительные ситуации, а не переменные состояния, для сообщения об ошибках.
  15. (**2) Напишите манипулятор, который включает и отключает эхо символа.
Дарья Федотова
Дарья Федотова
Сергей Березовский
Сергей Березовский

В рамках проф. переподготовки по программе "Программирование"

Есть курсы, которые я уже прошел. Но войдя в курс я вижу, что они не зачтены (Язык Ассемблера и архитектура ЭВМ, Программирование на С++ для профессионалов). Это как?