Лекция 10: |
оглавление |
Библиотека потокового ввода-вывода 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 для этого используются специальные методы класса 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.
Для входных строковых потоков можно использовать символьный массив с завершающим нулем, либо указав точный размер.
КОНЕЦ |