Язык программирования C++.

17.5. Еще о библиотеке ввода и вывода.

Глава 8 познакомила вас с базовой архитектурой и наиболее часто используемой частью библиотеки ввода-вывода. В этом разделе рассматриваются три более специализированных средства, поддерживаемых библиотекой ввода-вывода: управление форматом, не форматированный ввод-вывод и произвольный доступ.

17.5.1. Форматированный ввод и вывод.

Кроме флага состояния (см. раздел 8.1.2), каждый объект iostream имеет также флаг формата, контролирующий подробности формата ввода и вывода. Флаг формата контролирует такие аспекты, как формат записи целочисленных значений, точность значений с плавающей запятой, ширина выводимого элемента и т.д.

Библиотека определяет набор перечисленных в табл. 17.17 и 17.18 манипуляторов (manipulator) (см. раздел 1.2), изменяющих флаг формата потока. Манипулятор — это функция или объект, влияющие на состояние потока и применяемые как операнд оператора ввода или вывода. Как и операторы ввода и вывода, манипулятор возвращает потоковый объект, к которому он применяется; таким образом, можно объединить манипуляторы и данные в один оператор.

Таблица 17.17. Манипуляторы, определенные в объекте iostream

boolalpha Отображать значения true и false как строки
*noboolalpha Отображать значения true и false как 0 и 1
showbase Создавать префикс, означающий базу целочисленных значений
*noshowbase Не создавать префикс базы чисел
showpoint Всегда отображать десятичную точку для значений с плавающей запятой
*noshowpoint Отображать десятичную точку, только если у значения есть дробная часть
showpos Отображать + для положительных чисел
*noshowpos Не отображать + в неотрицательных числах
uppercase Выводить 0X в шестнадцатеричной и E в экспоненциальной формах записи
*nouppercase Выводить 0x в шестнадцатеричной и е в экспоненциальной формах записи
*dec Отображать целочисленные значения с десятичной базой числа
hex Отображать целочисленные значения с шестнадцатеричной базой числа
oct Отображать целочисленные значения с восьмеричной базой числа
left Добавлять дополняющие символы справа от значения
right Добавлять дополняющие символы слева от значения
internal Добавлять дополняющие символы между знаком и значением
fixed Отображать значения с плавающей точкой в десятичном представлении
scientific Отображать значения с плавающей точкой в экспоненциальном представлении
hexfloat Отображать значения с плавающей точкой в шестнадцатеричном представлении (нововведение С++11)
defaultfloat Вернуть формат числа с плавающей точкой в десятичный (нововведение С++11)
unitbuf Сбрасывать буфер после каждой операции вывода
*nounitbuf Восстановить обычный сброс буфера
*skipws Пропускать отступы в операторах ввода
noskipws Не пропускать отступы в операторах ввода
flush Сбросить буфер объекта ostream
ends Вставить нулевой символ, а затем сбросить буфер объекта ostream
endl Вставить новую строку, а затем сбросить буфер объекта ostream

*Означает стандартное состояние потока.

Таблица 17.18. Манипуляторы, определенные в объекте iomanip

setfill(ch) Заполнить отступ символом ch
setprecision(n) Установить точность n числа с плавающей точкой
setw(w) Читать или писать значение в w символов
setbase(b) Вывод целых чисел с базой b

Ранее в программах уже использовался манипулятор endl, который "записывался" в поток вывода как будто это значение. Но манипулятор endl — не обычное значение; он выполняет операцию: выводит символ новой строки и сбрасывает буфер.

Большинство манипуляторов изменяет флаг формата.

Манипуляторы используются для двух общих категорий управления выводом: контроль представления числовых значений, а также контроль количества и расположения заполнителей. Большинство манипуляторов, изменяющих флаг формата, предоставлены парами для установки и сброса; один манипулятор устанавливает флаг формата в новое значение, а другой сбрасывает его, восстанавливая стандартное значение.

Язык программирования C++.Манипуляторы, изменяющие флаг формата потока, обычно оставляют флаг формата измененным для всего последующего ввода-вывода.

Тот факт, что манипулятор вносит постоянное изменение во флаг формата, может оказаться полезным, когда имеется ряд операций ввода-вывода, использующих одинаковое форматирование. Действительно, некоторые программы используют эту особенность манипуляторов для изменения поведения одного или нескольких правил форматирования ввода или вывода. В таких случаях факт изменения потока является желательным.

Но большинство программ (и что еще важней, разработчиков) ожидают, что состояние потока будет соответствовать стандартным библиотечным значениям. В этих случаях оставленный в нестандартном состоянии поток может привести к ошибке. В результате обычно лучше отменить изменение состояния, как только оно больше не нужно.

Контроль формата логических значений.

Хорошим примером манипулятора, изменяющего состояние формата своего объекта, является манипулятор boolalpha. По умолчанию значение типа bool выводится как 1 или 0. Значение true выводится как целое число 1, а значение false как 0. Это поведение можно переопределить, применив к потоку манипулятор boolalpha:

cout << "default bool values: " << true << " " << false

     << "\nalpha bool values: " << boolalpha

     << true << " " << false << endl;

Эта программа выводит следующее:

default bool values: 1 0

alpha bool values: true false

Как только манипулятор boolalpha "записан" в поток cout, способ вывода логических значений изменяется. Последующие операции вывода логических значений отобразят их как "true" или "false".

Чтобы отменить изменение флага формата потока cout, применяется манипулятор noboolalpha:

bool bool_val = get_status();

cout << boolalpha    // устанавливает внутреннее состояние cout

     << bool_val

     << noboolalpha; // возвращает стандартное внутреннее состояние

Здесь формат вывода логических значений изменен только для вывода значения bool_val. Как только это значение будет выведено, поток немедленно возвращается в первоначальное состояние.

Определение базы целочисленных значений.

По умолчанию целочисленные значения выводятся и читаются в десятичном формате. Используя манипуляторы hex, oct и dec, базу записи числа можно изменить на восьмеричную, шестнадцатеричную и обратно на десятичную базу:

cout << "default: " << 20 << " " << 1024 << endl;

cout << "octal: " << oct << 20 << " " << 1024 << endl;

cout << "hex: " << hex << 20 << " " << 1024 << endl;

cout << "decimal: " << dec << 20 << " " << 1024 << endl;

После компиляции и запуска на выполнение эта программа выводит следующее:

default: 20 1024

octal: 24 2000

hex: 14 400

decimal: 20 1024

Обратите внимание, как и манипулятор boolalpha, эти манипуляторы изменяют флаг формата. Они срабатывают сразу после применения и влияют на весь последующий вывод целочисленных значений, пока формат не изменит применение другого манипулятора.

Язык программирования C++.Манипуляторы hex, oct и dec влияют на вывод только целочисленных операндов, но не значений с плавающей запятой.

Индикация базы числа в выводе.

По умолчанию при выводе числа нет никакого визуального уведомления об используемой базе. Например, 20 — это действительно 20, или восьмеричное представление числа 16? Когда числа выводятся в десятичном режиме, они отображаются, как и ожидается. Если необходимо выводить восьмеричные или шестнадцатеричные значения, вероятней всего, придется использовать также манипулятор showbase. Он заставляет поток вывода использовать те же соглашения, что и при определении базы целочисленных констант.

• Предваряющий 0x означает шестнадцатеричный формат.

• Предваряющий 0 означает восьмеричный формат.

• Отсутствие любого индикатора означает десятичное число.

Здесь предыдущая программа пересмотрена для использования манипулятора showbase:

cout << showbase; // отображать базу при выводе целочисленных значений

cout << "default: " << 20 << " " << 1024 << endl;

cout << "in octal: " << oct << 20 << " " << 1024 << endl;

cout << "in hex: " << hex << 20 << " " << 1024 << endl;

cout << "in decimal: " << dec << 20 << " " << 1024 << endl;

cout << noshowbase; // возвратить состояние потока

Вывод пересмотренной программы проясняет смысл:

default: 20 1024

in octal: 024 02000

in hex: 0x14 0x400

in decimal: 20 1024

Манипулятор noshowbase возвращает поток cout в прежнее состояние, когда индикатор базы не отображается.

По умолчанию шестнадцатеричные значения выводятся в нижнем регистре с x, также в нижним регистре. Манипулятор uppercase позволяет отобразить X и шестнадцатеричные цифры a-f в верхнем регистре:

cout << uppercase << showbase << hex

     << "printed in hexadecimal: " << 20 << " " << 1024

     << nouppercase << noshowbase << dec << endl;

Этот оператор создает следующий вывод:

printed in hexadecimal: 0X14 0X400

Манипуляторы nouppercase, noshowbase и dec применяются для возвращения потока в исходное состояние.

Контроль формата значений с плавающей точкой.

Контролировать можно три аспекта вывода числа с плавающей запятой.

• Количество выводимых цифр точности.

• Выводится ли число в шестнадцатеричном формате, как фиксированное десятичное число или в экспоненциальном представлении.

• Выводится ли десятичная точка для целочисленных значений с плавающей запятой.

По умолчанию значения с плавающей запятой выводятся с шестью цифрами точности; десятичная точка не отображается при отсутствии дробной части; в зависимости от величины значения используется фиксированный десятичный формат или экспоненциальная форма. Библиотека выбирает формат, увеличивающий удобочитаемость числа. Очень большие и очень маленькие значения выводятся в экспоненциальном представлении. Другие значения выводятся в фиксированном десятичном формате.

Определение точности.

По умолчанию точность контролирует общее количество отображаемых цифр. При выводе значение с плавающей запятой округляется (а не усекается) до текущей точности. Таким образом, если текущая точность четыре, то число 3.14159 становится 3.142; если точность три, то оно выводится как 3.14.

Для изменения точности можно воспользоваться функцией-членом precision() объекта ввода-вывода или манипулятором setprecision. Функция-член precision() перегружена (см. раздел 6.4). Одна ее версия получает значение типа int и устанавливает точность в это новое значение. Она возвращает предыдущее значение точности. Другая версия не получает никаких аргументов и возвращает текущее значение точности. Манипулятор setprecision получает аргумент, который и использует для установки точности.

Язык программирования C++.Манипулятор setprecision и другие манипуляторы, получающие аргументы, определяются в заголовке iomanip.

Следующая программа иллюстрирует различные способы контроля точности при выводе значения с плавающей точкой:

// cout.precision() сообщает текущее значение точности

cout << "Precision: " << cout.precision()

     << ", Value: " << sqrt(2.0) << endl;

// cout.precision (12) запрашивает вывод 12 цифр точности

cout.precision(12);

cout << "Precision: " << cout.precision()

     << ", Value: " << sqrt(2.0) << endl;

// альтернативный способ установки точности с использованием

// манипулятора

setprecision cout << setprecision(3);

cout << "Precision: " << cout.precision()

     << ", Value: " << sqrt(2.0) << endl;

Эта программа выводит следующее:

Precision: 6, Value: 1.41421

Precision: 12, Value: 1.41421356237

Precision: 3, Value: 1.41

Программа использует библиотечную функцию sqrt(), определенную в заголовке cmath. Функция sqrt() перегружена и может быть вызвана с аргументами типа float, double или long double. Она возвращает квадратный корень своего аргумента.

Определение формы записи чисел с плавающей запятой.

Язык программирования C++.Если нет реальной необходимости контролировать представление числа с плавающей запятой (например, для вывода данных в столбик, отображения денежных данных или процентов), лучше позволить библиотеке выбирать форму записи самостоятельно.

Используя соответствующий манипулятор, можно заставить поток использовать научную, фиксированную или шестнадцатеричную форму записи. Манипулятор scientific задает использование экспоненциального представления. Манипулятор fixed задает использование фиксированных десятичных чисел.

Язык программирования C++.Новая библиотека позволяет выводить значения с плавающей точкой в шестнадцатеричном формате при помощи манипулятора hexfloat. Новая библиотека предоставляет еще один манипулятор, defaultfloat. Он возвращает поток в стандартное состояние, при котором выбор формы записи осуществляется на основании выводимого значения.

Эти манипуляторы изменяют также заданное для потока по умолчанию значение точности. После применения манипуляторов scientific, fixed или hexfloat значение точности контролирует количество цифр после десятичной точки. По умолчанию точность определяет количество цифр до и после десятичной точки. Манипуляторы fixed и scientific позволяют выводить числа, выстроенные в столбцы, с десятичной точкой в фиксированной позиции относительно дробной части :

cout << "default format: " << 100 * sqrt(2.0) << '\n'

     << "scientific: " << scientific << 100 * sqrt(2.0) << '\n'

     << "fixed decimal: " << fixed << 100 * sqrt(2.0) << '\n'

     << "hexadecimal: " << hexfloat << 100 * sqrt(2.0) << '\n'

     << "use defaults: " << defaultfloat << 100 * sqrt(2.0)

     << "\n\n";

Получается следующий вывод:

default format: 141.421

scientific: 1.414214e+002

fixed decimal: 141.421356

hexadecimal: 0x1.1ad7bcp+7

use defaults: 141.421

По умолчанию шестнадцатеричные цифры и символ е, используемый в экспоненциальном представлении, выводятся в нижнем регистре. Манипулятор uppercase позволяет выводить эти значения в верхнем регистре.

Вывод десятичной точки.

По умолчанию, когда дробная часть значения с плавающей точкой равна 0, десятичная точка не отображается. Манипулятор showpoint требует отображать десятичную точку всегда:

cout << 10.0 << endl;        // выводит 10

cout << showpoint << 10.0    // выводит 10.0000

     << noshowpoint << endl; // возвращает стандартный формат

                             // десятичной точки

Манипулятор noshowpoint восстанавливает стандартное поведение. У вывода следующих выражений будет стандартное поведение, подразумевающее отсутствие десятичной точки, если дробная часть значения с плавающей точкой отсутствует.

Дополнение вывода.

При выводе данных в столбцах зачастую необходим довольно подробный контроль над форматированием данных. Библиотека предоставляет несколько манипуляторов, обеспечивающих контроль, который может понадобиться.

• Манипулятор setw задает минимальное пространство для следующего числового или строкового значения.

• Манипулятор left выравнивает текст по левому краю вывода.

• Манипулятор right выравнивает текст по правому краю (принято по умолчанию).

• Манипулятор internal контролирует положение знака отрицательных значений. Выравнивает знак по левому краю, а значение по правому, дополняя пространство между ними пробелами.

• Манипулятор setfill позволяет задать альтернативный символ для дополнения вывода. По умолчанию принят пробел.

Язык программирования C++.Манипуляторы setw и endl не изменяют внутреннее состояние потока вывода. Они определяют только последующий вывод.

Эти манипуляторы иллюстрирует следующая программа:

int i = -16;

double d = 3.14159;

// дополняет первый столбец, обеспечивая минимум 12 позиций вывода

cout << "i: " << setw(12) << i << "next col" << '\n'

     << "d: " << setw(12) << d << "next col" << '\n';

// дополняет первый столбец и выравнивает все столбцы по левому краю

cout << left

     << "i: " << setw(12) << i << "next col" << '\n'

     << "d: " << setw(12) << d << "next col" << '\n'

     << right; // восстанавливает стандартное выравнивание

// дополняет первый столбец и выравнивают все столбцы по правому краю

cout << right

     << "i: " << setw(12) << i << "next col" << '\n'

     << "d: " << setw(12) << d << "next col" << '\n';

// дополняет первый столбец и помещает дополнение в поле

cout << internal

     << "i: " << setw(12) << i << "next col" << '\n'

     << "d: " << setw(12) << d << "next col" << '\n';

// дополняет первый столбец, используя символ # как заполнитель

cout << setfill('#')

     << "i: " << setw(12) << i << "next col" << '\n'

     << "d: " << setw(12) << d << "next col" << '\n'

     << setfill(' '); // восстанавливает стандартный символ заполнения

Вывод этой программы таков:

i:          -16next col

d:      3.14159next col

i: -16         next col

d: 3.14159     next col

i:          -16next col

d:      3.14159next col

i: -         16next col

d:      3.14159next col

i: -#########16next col

d: #####3.14159next col

Контроль формата ввода.

По умолчанию операторы ввода игнорируют символы отступа (пробел, табуляция, новая строка, новая страница и возврат каретки).

char ch;

while (cin >> ch)

 cout << ch;

Этот цикл получает следующую исходную последовательность:

a b     с

d

Он выполняется четыре раза, читая символы от а до d, пропуская промежуточные пробелы, возможные символы табуляции и новой строки. Вывод этой программы таков:

abcd

Манипулятор noskipws заставляет оператор ввода читать, не игнорируя отступ. Для возвращения к стандартному поведению применяется манипулятор skipws:

cin >> noskipws; // установить cin на чтение отступа

while (cin >> ch)

 cout << ch;

cin >> skipws; // возвратить cin к стандартному игнорированию отступа

При том же вводе этот цикл делает семь итераций, читая отступы как символы во вводе. Его вывод таков:

a b     с

d

Упражнения раздела 17.5.1.

Упражнение 17.34. Напишите программу, иллюстрирующую использование каждого манипулятора из табл. 17.17 и 17.18.

Упражнение 17.35. Напишите версию программы вывода квадратного корня, но выводящую на сей раз шестнадцатеричные цифры в верхнем регистре.

Упражнение 17.36. Измените программу из предыдущего упражнения так, чтобы различные значения с плавающей точкой выводились в столбце.

17.5.2. Не форматированные операции ввода-вывода.

До сих пор в программах использовались только операции форматированного ввода-вывода (formatted IO). Операторы ввода и вывода ( << и >>) форматируют читаемые и выводимые данные согласно их типу. Операторы ввода игнорируют отступ; операторы вывода применяют дополнение, точность и т.д.

Библиотека предоставляет также набор низкоуровневых функций не форматированного ввода-вывода (unformatted IO). Эти функции позволяют работать с потоком как с последовательностью неинтерпретируемых байтов.

Однобайтовые операции.

Некоторые из не форматированных операций имеют дело с обработкой потока по одному байту за раз. Они описаны в табл. 17.19 и читают данные, не игнорируя отступ. Например, функции не форматированного ввода-вывода get() и put() позволяют читать и записывать символы по одному:

char ch;

while (cin.get(ch))

 cout.put(ch);

Эта программа сохраняет отступ во вводе. Ее вывод идентичен вводу. Она работает так же, как и предыдущая программа, использовавшая манипулятор noskipws.

Таблица 17.19. Однобайтовые низкоуровневые функции ввода-вывода

is.get(ch) Помещает следующий байт из потока is класса istream в символьную переменную ch. Возвращает поток is
os.put(ch) Помещает символ ch в поток os класса ostream. Возвращает поток os
is.get() Возвращает следующий байт из потока is как тип int
is.putback(ch) Помещает символ ch назад в поток is; возвращает поток is
is.unget() Перемещает в поток is один байт; возвращает поток is
is.peek() Возвращает следующий байт как тип int, но не удаляет его

Возвращение во входной поток.

Иногда необходимо читать отдельные символы так, чтобы знать, к чему быть готовым. В таких случаях символы желательно возвращать в поток. Библиотека предоставляет три способа сделать это, и у каждого из них есть свои отличия.

• Функция peek() возвращает копию следующего символа во входном потоке, но не изменяет поток. Возвращенное значение остается в потоке.

• Функция unget() создает резервную копию входного потока, чтобы независимо от того, какое значение было последним возвращенным, оно все еще оставалось в потоке. Функцию unget() можно вызвать, даже не зная, какое значение было извлечено из потока последним.

• Функция putback() — это более специализированная версия функции unget(): она возвращает последнее прочитанное из потока значение, но получает аргумент, который должен совпадать с последним прочитанным значением.

Таким образом, они гарантируют возможность вернуть в поток как минимум одно значение перед следующим чтением. Следовательно, гарантированно не получится вызвать функции putback() или unget() последовательно, без промежуточной операции чтения.

Возвращение значения типа int из операций ввода.

Функция peek() и версия функции get() без аргументов возвращают прочитанный символ из входного потока как значение типа int. Этот факт может удивить; казалось бы, более естественным было бы возвращение типа char.

Причина возвращения этими функциями типа int в том, чтобы позволить им возвратить маркер конца файла. Полученный набор символов позволяет использовать каждое значение в диапазоне типа char и представлять фактические символы. Но в этом диапазоне нет никакого специального значения для представления конца файла.

Функции, возвращающие тип int, преобразуют возвращаемый символ в тип unsigned char, а затем преобразуют это значение в тип int. В результате, даже если в наборе символов будут символы, соответствующие отрицательным значениям, возвращенный этими функциями тип int будет иметь положительное значение (см. раздел 2.1.2). Библиотека использует отрицательное значение для представления конца файла, гарантируя таким образом его отличие от любого настоящего символьного значения. Чтобы не обязывать разработчиков знать фактическое возвращаемое значение, заголовок iostream определяет константу EOF, которую можно использовать для проверки, не является ли возвращенное функцией get() значение концом файла. Вот почему для содержания значения, возвращаемого этими функциями, используется переменная типа int.

int ch; // возвращаемое fromget() значение содержится в int, а не char

// цикл чтения и записи всех данных во вводе

while ((ch = cin.get()) != EOF)

 cout.put(ch);

Эта программа работает так же, как и прежняя, но здесь для чтения ввода используется функция get().

Внимание! Низкоуровневые функции подвержены ошибкам. Обычно рекомендуется использовать высокоуровневые абстракции, предоставляемые библиотекой. Функции ввода-вывода, возвращающие значение типа int, являются хорошим подтверждением правильности этой рекомендации. Обычной ошибкой программирования является присвоение значения,возвращаемого функцией get() или peek(), возвращающей тип int, переменной типа char, а не int. Это, безусловно, будет ошибкой, но компилятор ее не обнаружит. То, что произойдет в результате этой ошибки, зависит от конкретной машины и введенных данных. Например, если машина интерпретирует символ как беззнаковое целое число, приведенный ниже цикл окажется бесконечным. char ch; // применение типа char здесь приведет к катастрофе! // значение, возвращенное функцией get() объекта с in, // преобразуется из int в char, а затем сравнивается с int while ((ch = cin.get()) != EOF)  cout.put(ch); Проблема в том, что когда функция get() возвращает значение EOF, оно преобразуется в беззнаковое значение типа unsigned char. Это преобразованное значение не будет равно целочисленному значению EOF, поэтому цикл не закончится никогда. Такие ошибки обычно обнаруживаются при проверке. Но нельзя быть уверенным в том, что на тех машинах, где символы интерпретируются как знаковый топ, поведение цикла будет аналогичным. Ведь результат переполнения переменной беззнакового типа зависит от компилятора. На большинстве машин этот цикл будет работать нормально, если только во вводимых данных не встретится символ, соответствующий значению EOF. Поскольку в обычных данных такие символы маловероятны, низкоуровневые операторы ввода-вывода могут пригодиться при чтении только бинарных значений, которые не соответствуют непосредственно обычным символам и числовым значениям. На машине автора, например, цикл преждевременно завершается в случае ввода символа, значением которого является '\377'. Когда значение '\377' на машине автора преобразуется в тип signed char, получается значение -1. Если во введенных данных встретится это значение, оно будет рассматриваться как символ (преждевременного) конца файла. При чтении и записи типизированных значений такие ошибки не возникают. Поэтому по возможности следует использовать предоставляемые библиотекой высокоуровневые операторы, что гораздо безопасней.

Многобайтовые операции.

Некоторые операции не форматированного ввода-вывода работают с порциями данных за раз. Эти операции могут быть полезны, если важна скорость, но, как и другие низкоуровневые операции, они подвержены ошибкам. В частности эти операции требуют резервирования и управления символьными массивами (см. раздел 12.2), используемыми для сохранения и возвращения данных. Многобайтовые операции перечислены в табл. 17.20.

Таблица 17.20. Многобайтовые низкоуровневые операции ввода-вывода

is.get(sink, size, delim) Читает до size байтов из потока is и сохраняет их в символьном массиве, начиная с адреса, на который указывает sink. Чтение продолжается, пока не встретится символ delim, либо пока не прочитано size байтов, либо пока не кончится файл. Если параметр delim присутствует, то его значение остается во входном потоке и не читается в sink
is.getline(sink, size, delim) To же поведение, что и версии функции get() с тремя аргументами, но читает и отбрасывает delim
is.read(sink, size) Читает до size байтов в символьный массив sink. Возвращает поток is
is.gcount() Возвращает количество байтов, прочитанных из потока is при последним вызове функции не форматированного чтения
os.write(source, size) Записывает size байтов из символьного массива source в поток os
is.ignore(size, delim) Читает и игнорирует до size символов, включая delim. В отличие от других не форматированных функций, ignore() имеет аргументы по умолчанию: для size — 1 и для delim — конец файла

Функции get() и getline() имеют схожие, но не идентичные параметры. В каждом случае sink — это символьный массив, в который помещаются данные. Обе функции читают, пока не будет выполнено одно из следующих условий:

• Прочитано size - 1 символов.

• Встретился конец файла.

• Встретился символ разделения.

Эти функции отличаются обработкой разделителя: функция get() оставляет разделитель как следующий символ потока istream, а функция getline() читает и отбрасывает разделитель. В любом случае разделитель не сохраняется в массиве sink.

Язык программирования C++.Весьма распространенная ошибка: намереваться удалить разделитель из потока, но забыть сделать это.

Определение количества читаемых символов.

Некоторые из операций читают из ввода неизвестное количество байтов. Для определения количества символов, прочитанных последней операцией не форматированного ввода, можно вызвать функцию gcount(). Имеет смысл вызывать функцию gcount() перед любым вмешательством в операции не форматированного ввода. В частности, операции с единичными символами, возвращающими их в поток, также являются операциями не форматированного ввода. Если функции peek(), unget() или putback() будут вызваны перед вызовом функции gcount(), то будет возвращено значение 0.

Упражнения раздела 17.5.2.

Упражнение 17.37. Используйте не форматированную версию функции getline() для чтения файла по строке за раз. Проверьте программу на примере файла с пустыми строками, а также со строками, длинна которых больше символьного массива, переданного функции getline().

Упражнение 17.38. Дополните программу из предыдущего упражнения так, чтобы выводить каждое прочитанное слово в отдельной строке.

17.5.3. Произвольный доступ к потоку.

Некоторые из потоковых классов обеспечивают произвольный доступ к данным связанного с ними потока. Положение в потоке можно изменить так, чтобы прочитать сначала последнюю строку, затем первую и т.д. Для установки (seek) необходимой позиции и сообщения (tell) текущей позиции в потоке библиотека предоставляет пару функций.

Язык программирования C++.Произвольный доступ для чтения и записи напрямую зависит от системы. Чтобы выяснить способ применения этой возможности, следует обратиться к документации на систему.

Хотя функции seek() и tell() определены для всех потоковых классов, возможные для них действия определяются видом объекта, с которым связан поток. В большинстве систем поток, с которым связан потоковый объект cin, cout, cerr или clog, не обеспечивает возможности произвольного доступа — в конце концов, как можно перейти на десять позиций обратно, если запись осуществляется непосредственно в объект cout? Применить функции seek() и tell(), конечно, можно, но во время выполнения это приведет к ошибке и переходу потока в недопустимое состояние.

Язык программирования C++.Поскольку классы istream и ostream обычно не обеспечивают произвольного доступа, в остальной части этого раздела речь идет только о классах fstream и sstream.

Функции установки и сообщения.

Для обеспечения произвольного доступа типы ввода-вывода обладают маркером (marker), который указывает позицию следующей операции чтения или записи. Они обладают также двумя функциями: одна устанавливает (seek) маркер в новую позицию, а вторая сообщает (tell) текущую позицию маркера. Фактически в библиотеке определены две пары функций установки и сообщения, которые описаны в табл. 17.21. Одна пара функций используется потоками ввода, а вторая — потоками вывода. Версии для ввода и вывода различаются суффиксом. Суффикс g (getting) означает получение данных (чтение), а суффикс p (putting) — помещение данных (запись).

Таблица 17.21. Функции установки и сообщения

tellg() tellp() Возвращает текущую позицию маркера потока ввода ( tellg()) или потока вывода ( tellp())
seekg(pos) seekp(pos) Переустанавливает маркер потока ввода или вывода на заданный параметром pos абсолютный адрес в потоке. Значение pos обычно возвращается предыдущим вызовом в соответствующей функции tellg() или tellp()
seekp(off, from) seekg(off, from) Переустанавливает маркер потока ввода или вывода на off символов вперед или назад от значения from, которое может быть:
beg — от начала потока;
cur — от текущей позиции потока;
end — от конца потока

Вполне логично, что для класса istream, а также производных от него классов ifstream и istringstream (см. раздел 8.1) можно использовать только версии g, а для классов ostream и классов ofstream и ostringstream, производных от него, можно использовать только версии p. Классы iostream, fstream и stringstream способны читать и записывать данные в поток, поэтому для них можно использовать обе версии, g и p.

Существует только один маркер.

Тот факт, что библиотека различает версии функций seek() и tell() для чтения и записи, может ввести в заблуждение. Хотя библиотека и различает эти функции, в файле существует только один маркер, т.е. нет разных маркеров для чтения и записи.

Когда речь идет о потоке только ввода или вывода, различие не столь очевидно. В таких потоках можно использовать версии только g или p. Если попытаться вызвать функцию tellp() для объекта класса ifstream, компилятор сообщит об ошибке. Аналогично он поступит при попытке вызвать функцию seekg() для объекта класса ostringstream.

Типы fstream и stringstream допускают чтение и запись в тот же поток. У них есть один буфер для хранения подлежащих чтению и записи данных, а также один маркер, обозначающий текущую позицию в буфере. Библиотечные функции версий g и p используют тот же маркер позиции.

Язык программирования C++.Поскольку существует только один маркер, для переустановки маркера при каждом переключении между чтением и записью следует применять функцию seek().

Перемещение маркера.

Имеются две версии функции установки позиции: одна обеспечивает переход к указанной позиции в файле, а другая осуществляет смещение от текущей позиции.

// установка маркера в заданную позицию

seekg(new_position); // установить маркер чтения в позицию pos_type

seekp(new_position); // установить маркер записи в позицию pos_type

// смещение позиции на указанную дистанцию от текущей

seekg(offset, from); // установить дистанцию смещения маркера чтения

seekp(offset, from); // от from; offset имеет тип off_type

Возможные значения параметра from перечислены в табл. 17.21.

Аргументы new_position и offset этих функций имеют машинно-зависимые типы pos_type и off_type соответственно. Они определены в классах istream и ostream. Тип pos_type представляет позицию файла, а тип off_type — смещение от этой позиции. Значение типа off_type может быть положительным или отрицательным, что соответствует смещению вперед или назад.

Доступ к маркеру.

Функции tellg() и tellp() возвращают значение типа pos_type, обозначающее текущую позицию в потоке. Эти функции обычно используются для того, чтобы запомнить позицию и впоследствии вернуться к ней:

// запомнить текущую позицию записи в переменную mark

ostringstream writeStr; // поток вывода в строку

ostringstream::pos_type mark = writeStr.tellp();

// ...

if (cancelEntry)

 // возврат к отмеченной позиции

 writeStr.seekp(mark);

Чтение и запись в тот же файл.

Рассмотрим пример программы, которая читает файл и записывает в его конец новую строку, содержащую относительную позицию начала каждой строки. Предположим, например, что работать придется со следующим файлом.

Abcd

efg

hi

j

Модифицированный программой файл должен выглядеть следующим образом.

Abcd

efg

hi

j

5 9 12 14

Обратите внимание: программа не записывает смещение для первой строки, она всегда начинается с позиции 0. Обратите также внимание на то, что смещения должны также учитывать невидимый символ новой строки, завершающий каждую строку. И наконец, последнее число в выводе — смещение для строки, с которой начинается вывод. При включении этого смещения в вывод можно отличить свой вывод от первоначального содержимого файла. Можно прочитать последнее число в полученном файле и установить смещение так, чтобы получить позицию начала вывода.

Наша программа будет читать файл построчно. Для каждой строки значение счетчика будет увеличиваться на размер только что прочитанной строки. Этот счетчик содержит смещение, с которого начинается следующая строка:

int main() {

 // открыть файл для ввода и вывода, а затем перейти в его конец

 // аргументы режима файла приведены в табл. 8.4

 fstream inOut("copyOut",

  fstream::ate | fstream::in | fstream::out);

 if (!inOut) {

  cerr << "Unable to open file!" << endl;

  return EXIT_FAILURE; // EXIT_FAILURE см. p. 6.3.2

 }

 // inOut открыт в режиме ate, поэтому исходной позицией файла будет

 // его конец

 auto end_mark = inOut.tellg(); // запомнить позицию первоначального

                                // конца файла

 inOut.seekg(0, fstream::beg);  // перейти к началу файла

 size_t cnt = 0;                // счетчик количества байтов

 string line;                   // содержит каждую строку ввода

 // пока нет ошибки и исходные данные читаются

 while (inOut && inOut.tellg() != end_mark

        && getline(inOut, line)) { // и можно получить следующую строку

  cnt += line.size() + 1;          // добавить 1 для новой строки

  auto mark = inOut.tellg();       // запомнить позицию чтения

  inOut.seekp(0, fstream::end);    // установить маркер записи в конец

  inOut << cnt;                    // записать общую длину

  // вывести разделитель, если это не последняя строка

  if (mark != end_mark) inOut << " ";

  inOut.seekg(mark); // восстановить позицию чтения

 }

 inOut.seekp(0, fstream::end); // перейти к концу

 inOut << "\n"; // вывести символ новой строки в конце файла

 return 0;

}

Эта программа открывает поток fstream в режимах in, out и ate (см. табл. 8.4). Первые два режима означают, что предполагается чтение и запись в тот же файл. Режим ate означает, что начальной позицией открытого файла будет его конец. Как обычно, необходимо удостовериться, что файл открыт корректно, если это не так, следует выйти из программы (см. раздел 6.3.2).

Поскольку программа пишет в свой исходный файл, нельзя использовать конец файла как признак прекращения чтения. Цикл должен закончиться по достижении конца первоначального ввода. В результате сначала следует запомнить первоначальную позицию конца файла. Так как файл открыт в режиме ate, поток inOut уже установлен в конец. Сохраним текущую (т.е. первоначальную) позицию конца файла в переменной end_mark. Запомнив конечную позицию, маркер чтения следует установить в начало файла, чтобы можно было приступить к чтению данных.

Цикл while имеет три условия выхода: сначала проверяется допустимость потока; если это так, то проверяется, не достигнут ли конец исходных данных. Для этого текущая позиция чтения, возвращаемая функцией tellg(), сравнивается с позицией, заранее сохраненной в переменной end_mark. И наконец, если обе проверки пройдены успешно, происходит вызов функции getline(), которая читает следующую строку из файла. Если вызов функции getline() успешен, выполняется тело цикла.

Тело цикла начинается с запоминания текущей позиции в переменной mark. Она сохраняется для возвращения после записи следующего относительного смещения. Вызов функции seekp() переводит маркер записи в конец файла. Выводится значение счетчика, а затем функция seekg() возвращается к позиции, сохраненной в переменной mark. Восстановив положение маркера, можно снова проверить условие выхода из цикла while.

Каждая итерация цикла выводит смещение следующей строки. Поэтому последняя итерация цикла заботится о записи смещения последней строки. Однако в конец файла следует еще записать символ новой строки. Как и в других случаях записи, для позиционирования в конец файла перед выводом новой строки происходит вызов функции seekp().

Упражнения раздела 17.5.3.

Упражнение 17.39. Напишите собственную версию программы, представленной в этом разделе.