Лекция 10:

к оглавлению
оглавление

Библиотека iostream

Библиотека потокового ввода-вывода iostream может быть использована в качестве альтернативы известной стандартной библиотеки ввода-вывода языка С – stdio. Для нас библиотека iostream интересна как прекрасный пример объектно-ориентированного проектирования, так как содержит многие характерные приемы и конструкции.

В основе ООП подхода, реализуемого средствами iostream, лежит предположение о том, что объекты обладают знанием того, какие действия следует предпринимать при вводе и выводе.

При пользовании библиотекой iostream ошибки, связанные с "перепутыванием" типов данных, исключены. Если вы используете в операции ввода-вывода переменную типа unsigned long, то вызывается подпрограмма, ответственная именно за этот тип.

Библиотека stdio поддерживает средства языка С, позволяющее использовать переменное число параметров. Но такая гибкость дается не даром – на этапе компиляции проверка соответствия между спецификацией формата, как в функциях printf() и scanf(), не выполняется.

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

Библиотека iostream более медленная, чем stdio, но это небольшая плата за надежность и расширяемость, базирующиеся на возможностях объектно-ориентированных средств вывода.

Для начала рассмотрим основы применения iostream, то есть, как читать и писать данные встроенных в С++ типов. Затем обсудим, как строить собственные операции потокового ввода и вывода (операторы внесения и извлечения) для разработанных вами типов данных.

[к началу]

Простое внесение

Начнем с простой программы, обращающейся к стандартным средствам языка С, а затем преобразуем применяя для ввода-вывода средства библиотеки iostream. (Это был наш первый пример программы на С++)

Вариант на стандартном С Вариант на С++
#include <stdio.h>
int i;
char buff[80];
printf ("Введите число:");
scanf("%d", &i);
printf ("Введите символьную строку:");
gets (buff);
printf ("Вы ввели число: %d\n Вы ввели строку: %s\n", i, buff);
#include <iostream.h>
int i;
char buff[80];
cout << "Введите число и символьную строку:";
cin >> i >> buff;
cout << "Вы ввели число:" << i << "\n" << "Вы ввели строку:" << buff << "\n";

Оставим пока в стороне вопросы ввода (к этому мы еще вернемся), и остановимся на выводе информации.

Сообщения, выводимые программой, будут иметь вид:

Вы ввели число: 12
Вы ввели строку: My string

При этом, применяя стандартной библиотечной функций printf(), мы использовали в качестве одного из параметров этой функции строку формата, которая содержит два спецификатора: %d, %s, которые предписывают оператору printf() осуществить вывод целого числа и строки. Но все вы прекрасно знаете, что такая конструкция весьма чувствительна к ошибкам. Стоит только неправильно указать спецификатор типа или пропустить какой-либо из аргументов, то на выходе получается бессмыслица, а на поиск ее причин и исправление ошибки уйдет время.

И посмотрите, как все те же операции могут быть реализованы с применением iostream.

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

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

(((((cout<<"Вы ввели число:")<<i)<<"\n Вы ввели строку:")<< buff)<<"\n");

Объект cout – это предопределенный объект класса iostream, который используется для вывода. Корме него существуют еще
cin – стандартный поток ввода
cerr – поток для вывода сообщений об ошибках.

Когда компилятор разбирает приведенное выше выражение, он начинает с самого высокого уровня вложенности скобочной структуры и на этом уровне находит

cout<<"Вы ввели число:"

В процессе анализа этого выражения компилятор пытается отыскать функцию – operator<<(), имеющую в качестве левого операнда объект класса ostream, а в качестве правого – целое. Описание переопределяемой функции operator << (ostream &, char*) содержится в заголовочном файле iostream.h. Здесь компилятор С++ преобразует исходное выражение в более пригодное для дальнейшей обработки. В результате получается следующее

operator<<(cout, "Вы ввели число:")

Когда функция operator << (ostream &, char*) будет выполнена, она выведет свой аргумент (строку) и примет значение объекта cout (ее первый аргумент). "Вы ввели число:" выведется на экран, и подвыражением, находящимся на самом глубоком уровне вложенности, станет

cout << i

Теперь компилятор ищет функцию operator <<(ostream &, int). Мы уже говорили, что iostream содержит функции операторы для всех встроенных типов. Процесс продолжается до тех пор, пока выражение внесения не будет сведено к набору вызовов функции operatop << ().

В итоге, выполнение этой сточки приведет к последовательному вызову нескольких функций–операторов

функции левый операнд правый операнд возвращаемое значение
operator (ostream &, char*) cout "Вы ввели число:" cout
operator (ostream &, int) cout i cout
operator (ostream &, char*) cout "\n Вы ввели строку:" cout
operator (ostream &, char*) cout buff cout
operator (ostream &, char*) cout "\n" cout

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

[к началу]

Выражение извлечения

Как и внесение, извлечение выполняется в С++ переопределяемыми функциями–операторами, обращения к которым подставляются компилятором в зависимости от типов данных, используемых в выражении. Вернемся к нашему примеру.

Выражение извлечения в данной программе – это выражение, которое использует cin, предопределенный объект оператора iostream.

cin >> i >> buff;

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

А что будет в случае ошибочного выражения? Конечно, ошибочные данные – это всегда ошибочные данные, и ни один язык не сможет их исправить. Но средствами языка можно создать программу, устойчивую к разрушительному действию ошибочных данных. Пусть, к примеру, наша программа ожидает на вводе строку "12 строчечка".

Если на вход поступит "строчечка 12", то программа будет в большом затруднении при попытке интерпретировать "строчечка" как число. Однако библиотека iostream, в отличие от scanf(), производит контроль ошибок после ввода каждого значения. Кроме того, iostream может быть расширена введением операторов для новых типов.

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

[к началу]

Создание собственных функций внесения и извлечения

Мы уже говорили, что операторы >> и << можно перегружать, причем, вы сами можете создать свои операторы извлечения и внесения для создаваемых вами типов.

В общем виде, операция внесения имеет следующую форму

ostream& operator << (ostream& stream, имя_класса& obj)
{
stream << ... // вывод элементов объекта obj
// в поток stream, используя готовые функции внесения
return stream;
}

Аналогичным образом может быть определена функция извлечения

istream& operator << (istream& stream, имя_класса& obj)
{
stream >> ... // ввод элементов объекта obj
// из потока stream, используя готовые функции внесения
return stream;
}

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

Эти функции не могут быть методами класса, для работы с которым они создаются. Если бы это было так, то левым аргументом, при вызове операции << (или >>), должен стоять объект, генерирующий вызов этой функции.

cout << "Моя строка"; а сout.operator << ("Моя строка"); // ошибка

Получается, что функция внесения, определяемая для объектов вашего класса, должна быть методом объекта cout.

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

class _3d
{
  double x, y, z;
 public:
  _3d ();
  _3d (double initX, double initY, double initZ);
  double mod () {return sqrt (x*x + y*y +z*z);}
  double projection (_3d r) {return (x*r.x + y*r.y + z*r.z) / mod();}
  _3d& operator + (_3d& b);
  _3d& operator = (_3d& b);
  friend ostream& operator << (ostream& stream, _3d& obj);
  friend istream& operator >> (istream& stream, _3d& obj);
};

ostream& operator << (ostream& stream, _3d& obj)
{
 stream << "x=" << obj.x<< "y=" << obj.y << "z=" << obj.z;
 return stream;
}

istream& operator << (istream& stream, _3d& obj)
{
 stream >> obj.x >> obj.y >> obj.z;
 return stream;
}

main()
{
 _3d vecA;
 cin >> vecA;
 cout << "My vector: " << vecA << "\n";
}

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

[к началу]

Функции библиотеки iostream

Основное отличие профессионального программиста от любителя заключается в том, что он проверяет все входные данные. В библиотеке iostream для этого используются специальные методы класса ios и двух его наследников istream и ostream. Всего в этих классах находится порядка 25 методов, позволяющих получить информацию о состоянии объектов и управлять их поведением. Вот некоторые из них.

имя функции действие
int good () возвращает 1, если ошибок не обнаружено
int eof () возвращает 1, если поток находится в состоянии "коней файла"
int fail () возвращает 1, если обнаружена восстановимая ошибка ввода-вывода (обычно, ошибка преобразования данных)
int bad () возвращает 1, если обнаружена невосстановимая ошибка ввода-вывода
int clear () сбрасывает состояние ошибки ввода-вывода
int precision (int i) устанавливает точность вывода чисел с плавающей точкой
int width (int i) устанавливает ширину поля вывода

Как теперь можно изменить нашу программу, усилив ее проверкой на ошибки при вводе.

#include <iostream.h>
int i;
char buff[80];
do{
 if (cin.fail()) cin.clear(); // сброс состояния ошибки
 cout << "Введите число и символьную строку:";
 cin >> i;
 if (cin.fail())
 {
  cout << "Нужно ввести число";
  continue;
 }
 cin >> buff;
 if (cin.fail())
 {
  cout << "Нужно ввести строку";
  continue;
 }
} while (cin.fail()&&!cin.bad());
if (!cin.bad())
{
 cout << "Вы ввели число:" << i << "\n"
 << "Вы ввели строку:" << buff << "\n";
}

Обратите внимание на условие повторения цикла. Условие повторение цикла означает, что нужно повторить цикл, если произошла восстановимая ошибка (как правило, это ошибка преобразования), но только в том случае, если есть возможность восстановления. Единственная ошибка, которая может вызвать аварийное завершение – это переполнение 80-символьного буфера для ввода строки. Можно решить и эту проблему, указав объекту cin размер буфера, используя метод width() класса ios.

cin.width(sizeof(buff));
cin >> buff;

[к началу]

Манипуляторы ввода –вывода

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

В прошлый раз мы накладывали ограничение на размер буфера cin, применяя метод width():

cin.width(sizeof(buff));
cin >> buff;

Теперь, используя манипулятор setw(), можно определить размер буфера так

cin >> setw (sizeof (buff)) >> buff;

Существует два вида манипуляторов ввода-вывода: параметризованные и непараметризованные. Вот список стандартных манипуляторов библиотеки iostream()

манипулятор i/o описание
dec o устанавливает базу для преобразования чисел к 10-,
oct o 8-
hex o и 16-ичной форме
ws i пропуск начальных пробелов
ends o выводит нулевой символ
endl o Выводит признак конца строки
setfill (int ch) o Устанавливает символ-заполнитель
setprecision(int n) o Устанавливает точность вывода чисел с плав точкой (знаков после запятой)
setbase (int b) o устанавливает ширину поля для последующей операции
setbase (int b) o устанавливает базу для преобразования числовых значений
setiosflags (int f) i/o устанавливает отдельные флаги потока
resetiosflags (int f) i/o сбрасывает отдельные флаги потока

Вот некоторые из флагов потока
флаг описание
left выравнивание по левому краю
right выравнивание по правому краю
dec, oct, hex устанавливают базу для ввода-вывода
showbase выводить показатель базы
showpoint выводить десятичную точку для чисел с плав точкой
uppercase 16-ричные большие буквы
showpos показать "+" для положительных целых чисел
scientific установить экспон форму для чисел с плав точкой
fixed установить формат чисел с фиксированной плав точкой

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

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

ios& manipul (ios&);

Если вы пишите манипулятор только для вывода, ios замените на ostream, если для ввода – на istream.

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

Вот пример манипулятора для вывода с выравниванием по правому краю:

ostream & right(ostream &)
{
 s << resetiosflags(ios::left) << setiosflags(ios::right);
 return s;
}

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

Таким образом, выражение
cout << setw(20) << right<< 1234 << endl;
означает следующее. Манипулятор setw(20) установит поле вывода шириной 20 символов, манипулятор right установит выравнивание по правой границе поля вывода, выведется число, а манипулятор endl вызовет переход к следующей строке.

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

[к началу]

Файловые и строковые потоки

Одной из самых привлекательных возможностей библиотеки iostream является то, что она одинаково совершенно работает с различными источниками данных для ввода и вывода: клавиатурой и экраном, файлами и строками. Главное, что вы должны сделать, чтобы использовать строку или файл с библиотекой iostream,- это создать объект определенного типа.

Первостепенная задача при создании таких объектов - это организация буфера и связывание его с потоком.

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

filebuf mybuff; // создаем буферный объект
// увязывем этот буфер с файлом output для вывода
mybuff.open("output", ios::out);
ostream mycout(&mybuff); // новый потоковый объект
mycout <<12<<ends;
mybuff.close();

Аналогичные операции можно проделать при реализации извлечения из файла-буфера. Но можно поступить и по-другому, используя буферизированные файловые потоки трех видов

ifstream – для ввода,
ofstream – для вывода,
fstream – для ввода и вывода.

Например,

int i;
// сразу создается буферизованный потоковый объект, связанный // с файлом input
ifstream mycout("input");
mycin >> i;

Если в своей программе собираетесь использовать файловые потоки, то не забудьте внести в нее заголовочный файл fstream.h.

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

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

char mybuff[128];
ostrstream mycout (mybuff, sizeof(mybuff));
mycout << 123 << ends;

При этом mybuff примет значение "123".

Аналогично для ввода,

int i;
char mybuff[128]= "123";
istrstream mycin (mybuff, sizeof(mybuff));
mycin >> i;

После этого i примет значение 123.

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

[к началу]
назад КОНЕЦ вперед