Урок №208. Функционал класса istream
Обновл. 15 Сен 2021 |
Оператор извлечения
Как вы уже узнали на предыдущем уроке, мы можем использовать оператор извлечения >> для считывания информации из входного потока. Одной из наиболее распространенных проблем при считывании строк из входного потока является предотвращение переполнения. Например:
Что произойдет, если пользователь введет 20 символов? Правильно, переполнение. Одним из способов решения этой проблемы является использование манипуляторов. Манипулятор — это объект, который применяется для изменения потока данных с использованием операторов извлечения ( >> ) или вставки ( ).
Мы уже работали с одним из манипуляторов — endl, который одновременно выводит символ новой строки и удаляет текущие данные из буфера. Язык C++ предоставляет еще один манипулятор — setw() (из заголовочного файла iomanip), который используется для ограничения количества символов, считываемых из потока. Для использования setw() вам нужно просто передать в качестве параметра максимальное количество символов для извлечения и вставить вызов этого манипулятора следующим образом:
Эта программа теперь прочитает только первые 11 символов из входного потока (+ один символ для нуль-терминатора). Все остальные символы останутся в потоке до следующего извлечения.
Извлечение и пробелы
Важный момент: оператор извлечения работает с «отформатированными» данными, т.е. он игнорирует все пробелы, символы табуляции и символ новой строки. Например:
Если пользователь введет следующее:
Hello! My name is Anton
То оператор извлечения пропустит все пробелы и символы новой строки. Следовательно, результат выполнения программы:
Часто пользовательский ввод все же нужен со всеми его пробелами. Для этого класс istream предоставляет множество функций. Одной из наиболее полезных является фунция get(), которая извлекает символ из входного потока. Вот вышеприведенная программа, но уже с использованием функции get():
Теперь, если мы введем следующее:
Hello! My name is Anton
Hello! My name is Anton
Функция get() также имеет строковую версию, в которой можно указать максимальное количество символов для извлечения. Например:
Если мы введем следующее:
Hello! My name is Anton
Обратите внимание, программа считывает только первые 11 символов (+ нуль-терминатор). Остальные символы остаются во входном потоке.
Один важный нюанс: функция get() не считывает символ новой строки! Например:
Если пользователь введет следующее:
И программа сразу же завершит свое выполнение! Почему так? Почему не срабатывает второй ввод данных? Дело в том, что первый get() считывает символы до символа новой строки, а затем останавливается. Второй get() видит, что во входном потоке все еще есть данные и пытается их извлечь. Но первый символ, на который он натыкается — символ новой строки, поэтому происходит второй «Стоп!».
Для решения данной проблемы класс istream предоставляет функцию getline(), которая работает точно так же, как и функция get(), но при этом может считывать символы новой строки:
Этот код работает точно так, как ожидается, даже если пользователь введет строку с символом новой строки.
Если вам нужно узнать количество символов, извлеченных последним getline(), используйте функцию gcount():
Hello! My name is Anton
24 characters were read
Специальная версия функции getline() для std::string
Еще несколько полезных функций класса istream
функция ignore() — игнорирует первый символ из потока;
функция ignore(int nCount) — игнорирует первые nCount (количество) символов из потока;
функция peek() — считывает символ из потока, при этом не удаляя его из потока;
функция unget() — возвращает последний считанный символ обратно в поток, чтобы его можно было извлечь в следующий раз;
функция putback(char ch) — помещает выбранный вами символ обратно в поток, чтобы его можно было извлечь в следующий раз.
Класс istream содержит еще множество других полезный функций и их вариаций, но это уже тема для отдельного туториала.
Поделиться в социальных сетях:
Что такое C++ Stringstream и как его использовать?
Строковый объект, созданный из строкового класса, представляет собой структуру данных списка. Список представляет собой серию персонажей, и он ценится как таковой. Строковый объект C ++ имеет множество методов. Однако в нем отсутствуют определенные операции, которые лучше всего использовать, если рассматривать его как поток. Вот где на помощь приходит stringstream. Stringstream — это поток, который можно использовать для:
— Подсчитайте количество слов в строковом объекте
— Получить частоту отдельных слов в строковом объекте
— Преобразование слова в текстовой форме в строковом объекте в число и наоборот
Перемещение символов из строкового объекта в программу C ++ является вводом и представлено объектом строкового потока. Выводится перемещение символов из программы C ++ в строковый объект. Stringstream (то есть sstream) использует классы istringstream и ostringstream. Объект, созданный из istringstream, отвечает за ввод символов в поток строк. Объект, созданный из ostringstream, отвечает за вывод символов из строкового потока в строковый объект.
В этом руководстве объясняется, что такое sstream и как его использовать. Целевой строковый объект является частью программы C ++.
Чтобы выполнить ввод, вывод или и то, и другое в одном сеансе, программа на C ++ должна начинаться с:
Создание объекта Stringstream
Известно, что строковый поток может быть объявлен и применен одновременно в одном операторе. Однако в данном руководстве это не тот подход. В этом руководстве объект строкового потока создается из класса в одном операторе и используется в другом операторе.
Stringstream может быть создан для чтения (ввода). Stringstream может быть создан для записи (вывода). Stringstream может быть реализован как для чтения и записи.
Чтобы создать объект потока для чтения, используйте:
где strm — объект потока; а «in» класса ios_base означает чтение.
Чтобы создать объект потока для записи, используйте:
где strm — объект потока; а «вне» класса ios_base означает запись.
Чтобы создать объект потока для чтения или записи, используйте:
где «ios_base :: in | ios_base :: out », предназначен для чтения или записи.
Входная операция Stringstream
Текст для ввода в строковый поток из строкового объекта может быть выполнен двумя способами: с помощью оператора вставки ( #include
using namespace std ;
stringstream strm1 ( ios_base :: in ) ;
strm1 «We are the world!» ;
string stri2 = «This is the earth!» ;
stringstream strm2 ( ios_base :: in ) ;
stringstream strm3 ( ios_base :: in ) ;
strm3. str ( «Mars is next.» ) ;
string stri4 = «What about Jupiter?» ;
stringstream strm4 ( ios_base :: in ) ;
Строковый объект может быть литералом или идентификатором. Обратите внимание, что в объявлении объекта sstream используется «stringstream», а не «sstream», хотя оба термина означают одно и то же. В директиве include следует использовать термин sstream.
Выходная операция Stringstream
Слово — это любой строковый текст, в котором нет пробела (’’). Вывод из строкового потока означает отправку строкового слова из объекта sstream в строковый объект. Для этого нужен оператор извлечения (>>). Следующая программа отправляет слово из объекта sstream в строковый объект:
Класс basic_ostream
Синтаксис
Параметры
ТС
traits_type символа.
Remarks
Большинство функций элементов, которые оператор перегрузки имеют отформатированные выходные функции. Они следуют этому шаблону:
Две другие функции-члены — это неформатированные выходные функции. Они следуют этому шаблону:
Обе группы функций вызывают SetState(badbit), если при вставке элементов возникает ошибка.
Функции элементов
Операторы
| Оператор | Описание |
|---|---|
| Оператор = | Присваивает значение указанного параметра объекта basic_ostream этому объекту. |
| станции | Записывает данные в поток. |
Требования
Заголовок: ostream>
Пространство имен: std
basic_ostream::basic_ostream
Параметры
Remarks
Пример
Дополнительные сведения о выходных потоках см. в примере для basic_ofstream::basic_ofstream.
basic_ostream::flush
Возвращаемое значение
Ссылка на объект basic_ostream.
Remarks
Если rdbuf не является пустым указателем, функция вызывает rdbuf- pubsync. Если возвращается значение –1, функция вызывает setstate(badbit). Он возвращает * this.
Пример
basic_ostream::operator
Записывает данные в поток.
Параметры
PFN
Указатель функции.
Val
Элемент, записываемый в поток.
Возвращаемое значение
Ссылка на объект basic_ostream.
Remarks
В <> заголовке ostream также определено несколько глобальных операторов вставки. Дополнительные сведения см. в разделе оператор.
Первая функция-член гарантирует, что выражение формы ostr вызывает ostr (OSTR), а затем возвращает * this. Вторая и третья функции обеспечивают аналогичное поведение других манипуляторов, таких как hex. Все остальные функции являются форматированными выходными функциями.
Извлекает элементы из strBuf, если strBuf не является пустым указателем, и вставляет их. Извлечение останавливается в конце файла или если оно создает исключение (которое создается повторно). Оно также останавливается без извлечения соответствующего элемента, если вставка завершается неудачно. Если функция не вставляет элементы или если вставка создает исключение, эта функция вызывает setstate(failbit). В любом случае функция возвращает * this.
Преобразует _Val в логическое поле и вставляет его путем вызова _Val num_put ( OutIt определяется как OutIt Elem, TR >. Функция возвращает * this.
Пример
basic_ostream::operator=
Присваивает значения указанного параметра объекта basic_ostream этому объекту.
Параметры
Remarks
basic_ostream::put
Помещает символ в поток.
Параметры
Возвращаемое значение
Ссылка на объект basic_ostream.
Remarks
Неформатированная выходная функция вставляет элемент _Ch. Он возвращает * this.
Пример
basic_ostream::seekp
Сбрасывает позицию в выходном потоке.
Параметры
_Pos
Позиция в потоке.
_Off
Смещение относительно _Way.
Возвращаемое значение
Ссылка на объект basic_ostream.
Remarks
Пример
basic_ostream::sentry
Вложенный класс описывает объект, объявление которого структурирует форматированные и неформатированные выходные функции.
Remarks
Если uncaught_exception функция Returns false и uncaught_exception & false не равны нулю, деструктор вызывает &.
basic_ostream::swap
Параметры
Remarks
Функция – член вызывает метод basic_ios:: Swap для обмена содержимым этого объекта с содержимым right.
basic_ostream::tellp
Сообщает позицию в выходном потоке.
Возвращаемое значение
Позиция в выходном потоке.
Remarks
Пример
Пример использования см. в разделе seekp.
basic_ostream::write
Помещает символы в поток.
Параметры
count
Количество символов для помещения в поток.
str
Символы для помещения в поток.
Возвращаемое значение
Ссылка на объект basic_ostream.
Remarks
Пример
Пример использования см. в разделе streamsize.
Свои потоки ввода-вывода в C++ с помощью std::streambuf
В статье на примерах объясняется, как реализовать поддержку потокового ввода-вывода из стандартной библиотеки ( ) для своих классов.
В тексте статьи будет часто встречаться слово «поток», что означает именно поток ввода-вывода ((i/o)stream), но не поток выполнения (thread). Потоки выполнения в статье не рассматриваются.
Введение
Потоки из стандартной библиотеки — мощный инструмент. Аргументом функции можно указать поток, и это обеспечивает ее универсальность: она может работать как со стандартными файлами (fstream) и консолью (cin/cout), так и с сокетами и COM-портами, если найти соответствующую библиотеку.
Однако не всегда можно найти готовую библиотеку, где подходящий функционал уже реализован, может даже вы разрабатываете собственную библиотеку со своими классами. Тогда возникает задача реализации интерфейса потоков своими силами.
Используемое окружение
16.04.4), а также стандарт c++11. Для наглядности я использовал из него ключевое слово override, чтобы пометить переопределяемые методы базового класса, однако если его убрать (а еще nullptr на NULL заменить), то должно собраться и на более старых стандартах.
Все примеры также доступны на github: streambuf_examples.
Содержание
Как устроены потоки?
Каждый класс, поддерживающий потоковый ввод-вывод, наследует классы std::istream (ввод), std::ostream (вывод) или std::iostream (ввод и вывод). Именно они обеспечивают возможность использования перегруженных операторов ‘ >’, форматирования вывода, преобразование чисел в строки и наоборот и т.д.
Однако непосредственное чтение или запись данных происходят не в нем, а в классе, наследующем std::streambuf. Сам по себе streambuf является лишь интерфейсом с набором виртуальных функций, которые надо переопределить в классе-наследнике и уже в них реализовать свою логику чтения/записи данных (именно так и сделано в классах std::filebuf и std::stringbuf для fstream и stringstream соответственно).
Кроме того streambuf реализует часть логики работы с буфером. Программисту остается лишь задать начало и конец буфера и реализовать обработчики событий его переполнения, опустошения, синхронизации и т.п.
При разработке собственных потоков наиболее сложной частью является реализация наследника std::streambuf. Производные классы от istream, ostream или iostream в простых случаях могут и вовсе отсутствовать.
Простые случаи — без буферизации
В простом случае или когда производительность не играет важной роли, буферы могут быть не нужны. Тогда достаточно переопределить всего три виртуальные функции:
Возвращаемое значение: в случае успеха, код записанного сивола, приведенный к типу int, иначе EOF.
Поведение по-умолчанию: всегда возвращает EOF.
Возвращаемое значение: в случае успеха, код считанного символа, приведенный к типу int, иначе EOF.
Поведение по-умолчанию: если буфер доступен и есть несчитанные символы, возвращает символ на текущей позиции в буфере, иначе EOF.
Возвращаемое значение: как в underflow.
Поведение по-умолчанию: вызывает underflow. Если результат успешен, сдвигает указатель буфера к следующему символу и возвращает результат вызова underflow, в случае неудачи возвращает EOF. Попытка сдвинуть указатель незаданного буфера окончится segmentation fault-ом, не забудьте переопределить это поведение, если не используете буфер!
Здесь и далее описание функций взято с cppreference.com
Пример 1 — фильтруем цифры
Пожалуй, пока хватит текста. В качестве примера разберем фильтрующий поток, который будет пропускать только символы цифр и пробелы (чтобы числа можно было как-то разделять друг от друга), сами данные будем брать из другого потока.
Результат работы программы:
Основные моменты в коде уже прокомментированы, однако дополнительно стоит отметить, что для чтения важно реализовать обе функции — uflow и underflow, поскольку underflow может вызываться до uflow и даже несколько раз подряд. Если добавить в начало этих функций отладочный вывод, это можно увидеть наглядно, например, при чтении из потока в целочисленную переменную.
Используем буферы
Как я уже ранее говорил, streambuf уже реализует в себе часть логики работы с буфером и предоставляет доступ к 6-и указателям, по 3 указателя на входной и выходной буферы. Однако streambuf не реализует выделение памяти под буферы. Эта задача возлагается на программиста вместе с инициализацией буферных указателей.
Для входного буфера указатели следующие:
Наглядная иллюстрация с сайта mr-edd.co.uk
Также для управления указателями входного буфера служат следующие фукнции:
Еще одна наглядная иллюстрация с сайта mr-edd.co.uk
Управляющие функции выходного буфера также схожи:
Пример 2 — блочный вывод
Внимательный читатель наверняка давно уже задумался: буфер буфером, но ведь его надо как-то сбрасывать и не только при переполнении, но и по требованию программиста (подобно тому, как это происходит при записи в файл).
Для этого и служит еще одна виртуальная функция int sync(). Обычно она вызывается как раз по требованию программиста, однако в примере выше мы также вызываем ее сами при переполнении буфера. Возвращаемое ею значение говорит об успешной синхронизации (0) или неудачной (-1), при неудаче поток приобретает невалидное состояние. Реализация по-умолчанию ничего не делает и просто возвращает 0 (успех).
Кстати о переполнении буфера. В примере для упрощения реализации overflow() применен небольшой трюк: фактический размер буфера всегда на 1 элемент больше, чем «думает» streambuf. Это позволяет поместить переданный функции overflow «не влезший» символ в буфер и не усложнять код его специфичной обработкой.
Вывод программы для блоков размером в 10 символов следующий:
Пример 3 — буферизированный ввод из файла
С чтением все несколько сложнее, поэтому начнем с простого. В примере ниже с помощью потока реализовано простое последовательное чтение файла. Для получения данных из файла используем средства стандартной библиотеки языка Си.
Расширенные возможности
seekoff и seekpos для перемещения по файлу
При работе с файлом может потребоваться перемещение позиции в файле в произвольное место. Как вы уже догадались, в примере выше это не реализовано: файл читается только в одном направлении, назад вернуться нельзя, только переоткрывать файл. Чтобы исправить этот существенный недостаток, нам потребуется переопределить следующие методы класса streambuf :
Пояснение: в поле pos_base хранится смещение в файле, с которого данные были загружены в буфер.
Функция пытается переместить указатель в файле на заданную позицию и заполнить весь наш буфер от начала до конца. Не очень производительно при любой операции заново заполнять буфер из файла, но в примере это сделано для упрощения реализации. Когда вы будете реализовывать вашего собственного наследника streambuf, вам наверняка будут известны тонкости работы с вашими данными, чтобы написать максимально эффективные функции позиционирования указателей.
Ну а мы идем далее.
pbackfail — возврат прочитанных символов назад
Возвращаемое значение: код возвращенного в поток символа, приведенный к типу int, в случае неудачи — EOF.
Поведение по-умолчанию: ничего не делает и возвращает EOF.
Теперь реализуем наш pbackfail :
Как я говорил ранее, в этом примере производительность будет ужасная, т.к. практически при каждом вызове pbackfail данные будут заново читаться из файла в буфер ради всего одного символа — предыдущего. Но целю этой статьи является понимание принципа работы, а не соревнование в производительности реализаций.
Пример 4 — чтение файла с позиционированием и возвратом символов
Здесь просто представлен код, в котором добавлены правки, реализованные в предыдущих разделах, а также примеры использования этого функционала, с пояснениями:
Другие возможности
Помимо возможностей, рассмотренных в статье, есть и другие. Некоторые довольно просто реализуются, другие нужны лишь в специфических случаях, поэтому подробно они не рассматриваются. Далее приведен перечень таких функций и краткое описание, зачем они нужны. Более подробное описание о них вы можете найти в официальной документации (ссылка есть в конце статьи).
Другие доступные для переопределения методы:
Насколько медленны iostreams?
Потоки ввода-вывода в стандартной библиотеке C++ просты в использовании, типобезопасны, устойчивы к утечке ресурсов, и позволяют простую обработку ошибок. Однако, за ними закрепилась репутация «медленных». Этому есть несколько причин, таких как широкое использование динамической аллокации и виртуальных функций. Вообще, потоки — одна из самых древних частей стандартной библиотеки (они начали использоваться примерно в 1988 году), и многие решения в них сейчас воспринимаются как «спорные». Тем не менее, они широко используются, особенно когда надо написать какую-то простую программу, работающую с текстовыми данными.
Вопрос производительности iostreams не праздный. В частности, с проблемой производительности консольного ввода-вывода можно столкнуться в системах спортивного программирования, где даже применив хороший алгоритм, можно не пройти по времени только из-за ввода-вывода. Я также встречался с этой проблемой при обработке научных данных в текстовом формате.
Сегодня в комментариях у посту возникло обсуждение о медленности iostreams. В частности, freopen пишет
Забавно смотреть на ваши оптимизации, расположенные по соседству со считыванием через cin 🙂
Можно заменить на getchar_unlocked() для *nix или getchar() для всех остальных.
getchar_unlocked > getchar > scanf > cin, где «>» означает быстрее.
В этом посте я развею и подтвержу некоторые мифы и дам пару рекомендаций.
Все измерения в этом посте приведены для системы Ubuntu 14.10 с компилятором GCC 4.9.1, компилировалось с ключами
Запуск проводился на ноутбуке с процессором Intel Core2 Duo P8600 (2.4 ГГц).
Постановка задачи
В спортивном программировании, как и в UNIX-way, обычно входные данные подаются на входной поток. Итак, задача:
На входной поток (stdin) поступает много неотрицательных целых чисел по одному на строке. Программа должна вывести максимальное из входных чисел.
Сформируем входные данные
В файл data мы записали 10 миллионов последовательных целых чисел, общим объёмом 76 мегабайт.
Запускать программу мы будем так
1. scanf
Решим задачу с использованием старого доброго scanf.
Время работы: 1.41 c
2. Наивный std::cin
Теперь решим задачу самым простым способом при помощи iostreams:
Время работы: 4.41 c
Ого! Потоки оказались медленнее чем scanf в 3 раза! То есть выходит, что iostream оказываются действительно никуда не годится по скорости?
3. Быстрый std::cin
На самом деле, чтобы исправить ситуацию, достаточно добавить в программу одну единственную строчку. В самом начале функции main вставим:
Все последующие варианты с использованием std::cin будут использовать эту оптимизацию.
4. Наивный std::istringstream
Помимо ввода из файла, стандартная библиотека предоставляет также классы для ввода из строки с таким же интерфейсом. Посмотрим, насколько это медленно. Будем читать из входного потока по одной строке, а затем парсить её с помощью std::istringstream :
Время работы: 7.21 c
Очень медленно!
5. Переиспользование std::istringstream
Может показаться удивительным, но самое медленное в istringstream — это его создание. А мы создаём для каждой входной строки заново. Попробуем переиспользовать один и тот же объект:
Обратите внимание, что нужны 2 вызова — clear, чтобы сбросить флаги состояния, и str, чтобы задать новый буфер, из которого будет происходить чтение.
Время работы: 2.16 c
Это другое дело. Это ожидаемо медленнее, чем чтение напрямую из std::cin (данные проходят 2 раза через классы потоков), но не катастрофично.
6. Хотим ещё быстрее! (getchar/getchar_unlocked)
7. C++11: std::stoi
Время работы: 1.04 c
Это самый быстрый стандартный способ чтения целых чисел. (А для чисел с плавающей точкой есть аналогичные функции stof/stod).
8. Бонус: Чтение большими блоками + Boost::Spirit
Время работы: 0.18 c
Это рекорд!
Результаты и советы
Время работы:
| No | Метод | GCC 4.9.1 | clang 3.5.0 + libc++ | GCC 100M* |
|---|---|---|---|---|
| 1 | scanf | 1.41 | 1.48 | |
| 2 | std::cin | 4.41 | 13.30 | |
| 3 | std::cin и std::ios::sync_with_stdio(false) | 1.33 | 13.24 | |
| 4 | std::istringstream | 7.21 | 9.16 | |
| 5 | std::istringstream с переиспользованием | 2.16 | 7.92 | |
| 6a | getchar | 0.82 | 0.84 | 9.14 |
| 6b | getchar_unlocked | 0.28 | 0.26 | 2.94 |
| 7 | std::getline + std::stoi | 1.04 | 3.53 | 10.8 |
| 8 | Большой блок + Boost::Spirit | 0.18 | 1.67 | 1.90 |
* — Измерения на файле со 100 миллионами чисел (размер файла 848 мегабайт).
Рекомендации:
Update 1. По совету Lol4t0 добавлен метод номер 7.
Update 2. В таблицу добавлены времена выполнения на clang+libc++ (версия 3.5.0, выполнялось на той же системе). Видно, что производительность потоков очень плохая, да к тому же трюк с выключением синхронизации не работает. В результате stoi оказывается в 2 раза медленнее чем scanf.
Update 3. Добавлен вариант номер 8: чтение большими блоками и разбор с помощью Boost::Spirit. И это чемпион!


