Классы и потоки ввода-вывода
Ввод-вывод в С++, также как и в С, рассматривается как поток данных, управляемый с помощью функций ввода-вывода. В С++ для поддержки ввода-вывода данных используется целая иерархия классов.
Основополагающим базовым классом является класс ios. Производными от него являются классы istream – поддерживает базовые операции ввода,
ostream – поддерживает базовые операции вывода. Двунаправленный поток поддерживается классом iostream, производным от istream и ostream, что можно представить схемой:
ios
____________|_________
| |
istream ostream
|_____________________|
|
iostream
Объявления этих классов ввода-вывода помещено в заголовочный файл, который нужно подключить к программе строкой:
#include<iostream.h>
Начав с определения потока как абстракции для моделирования прохождения потока данных от источника к приемнику, класс iostream обеспечивает иерархию классов для управления буферированным и непосредственным вводом-выводом для устройств и файлов. Пользователь может создавать собственные классы ввода-вывода на базе стандартных классов.
Стандартный ввод-вывод
С++ поддерживает четыре предопределенных потоковых объекта:
1. cin – стандартный ввод, как правило с клавиатуры, аналог stdin в С (объект класса istream).
2. cout – стандартный вывод, как правило на экран, аналог stdout в C (объект класса ostream).
3. cerr– стандартный небуферизованный вывод ошибок, как правило на экран, аналог stderr в С (объект класса ostream).
4. clog– буферизованный вывод ошибок, нет аналога в С (объект класса ostream).
Можно переназначать стандартные потоки на другие устройства и файлы.
Для каждого потока "перегружены" (переопределены) два оператора:
>>– оператор извлечения (чтения) из потока (для ввода с клавиатуры: "считать из");
<<– оператор записи (вставки) в поток (для вывода на экран: "вывести на").
Замещающие функции способны воспринимать аргументы любых основных типов данных (char, char*(string), int, long, float, double) и могут быть расширены для того, чтобы воспринимать аргументы типа class.
Ввод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
istream cin;
Класс iostream определяет оператор ввода >> для набора стандартных типов:
class istream
{ // …
public: // общедоступные функции
istream & operator >> (char&); // символ
istream & operator >> (char*); // символьная строка
istream & operator >> (short&); // короткий целый
istream & operator >> (int&); // целый
istream & operator >> (long&); // длинный целый
istream & operator >> (float&); // действительный
istream & operator >> (double&); // двойной действительный
}
Операция >> (извлечения, чтения) заменяет функцию ввода scanf() и лучше защищает от ошибок. Левый операнд – это объект типа класса istream. Правый операнд может быть любого типа, для которого определен ввод потока. По умолчанию оператор >> опускает пробельные символы, а затем считывает символы, соответствующие типу вводимого данного. Функции преобразования и форматирования данных зависят от их типов.
Для ввода-вывода можно строить цепочки операций слева направо. Например, так можно ввести несколько данных в С++:
int i; float f;
cin >> I >> f;
Для всех числовых типов в случае, когда первый непробельный символ не является цифрой, или знаком, или точкой (для вещественных чисел) поток входит в состояние ошибки и вплоть до сброса состояния ошибки любой дальнейший ввод запрещен.
Для целых типов short, int, long действие операции >> по умолчанию заключается в пропуске пробельных символов и преобразовании целого значения путем чтения символов ввода до символа, который не является допустимым для данного типа.
Для типов с плавающей точкой float иdouble действие операции >> состоит в пропуске пробельных символов и преобразовании значения с плавающей точкой до символа, который не может быть частью числа с плавающей точкой.
Вывод для встроенных типов
Файл заголовков iostream.h содержит объявление объекта вида:
ostream cout;
Класс ostream определяет оператор вывода << для встроенных типов:
class ostream
{ // …
public: // общедоступные функции
ostream & operator << (char); // символ
ostream & operator << (char*); // символьная строка
ostream & operator << (int i); // целый
ostream & operator << (long);// длинный целый
ostream & operator << (double); // двойной действительный
ostream & put(char);// символ
}
Операция <<, называемая вставкой, заменяет функцию вывода printf(). Левый операнд – это объект cout типа класса ostream. Правый операнд может иметь любой тип, для которого определен вывод потоком для встроенных типов.
Функция-оператор << возвращает обращение по адресу, для которого она была вызвана, так что можно применить другой оператор ostream, т.е. можно строить цепочки операций вывода в порядке слева направо.
Например:
int i, k=234; double d ; char ch='A';
cout<<”Значение k=” << k <<’\n’; // в конце символ перевода строки
cin>> i >> d; // ввод данных i, d
cout<<”i=”<<i<<” d=”<<d<<’\n’; // вывод значений i, d
cout<<”ch=”<<ch; // вывод символа A
Пример 1.
Потоки ввода-вывода.
#include<iostream.h> // подключение библиотек ввода-вывода
void main()
{ int i;
char str[ ]=”Пример вывода в С++\n”;
cout<<”Вывод строки”<<str;
cout<<”Введите целое и длинное целое число: “;
long l; // переменная описывается перед первым использованием
cin>>i>>l; //ввод данных
cout<<”i=”<<i<<” l=”<<l<<’\n;
cout<<”Введите строку: ”;
cin>>str;
cout<<”Введена строка: ”<<str;
}
Замечание. Можно также использовать любые библиотечные функции ввода-вывода языка С (printf(), scanf() и др.). Кроме того, в другом контексте преопределенные операторы << и >> задают операции сдвига.
Пример 2.
Встроенные типы вставок (вывода данных). Целые типы преобразуются по правилам (по умолчанию) для printf (если эти правила не изменены путем установки флагов ios).
#include<iostream.h> // подключение библиотек ввода-вывода
#include<stdio.h> // для printf
#include<conio.h> // для консольных функций
void main()
{ int i=5; int *pi=&i; long l=12345678; double d=345.6789; char ch='A';
clrscr(); // чистка экрана результатов
cout<<”Вывод с помощью функции printf():\n”);
printf(“i=%d адрес i pi=%#x l=%ld d=%.4f ch=%c\n”, i,pi,l,d,ch);
cout<<”Вывод с помощью потока cout:\n”;
cout << ”i=”<< i << ” адрес i=” << &I << ” l =” << l << ” d=” <<d;
cout<<” ch=”<<ch<<'\n';
getch(); // задержка экрана результатов
}
Результаты программы:
Вывод с помощью функции printf():
i=5 адрес i pi=0xfff4 l=12345678 d=345.6789 ch=A
Вывод с помощью потока cout:
i=5 адрес i=0x8f61fff4 l=12345678 d=345.6789 ch=A
Управление вводом-выводом
Символьные извлечения.
Для типа char действие операции >> состоит в пропуске пробельных символов и чтения следующего (непробельного) символа. Если требуется прочесть следующий символ (любой), то можно использовать одну из функций-элементов get() класса istream:
сhar ch;
cin.get(ch); // ch устанавливается на следующий символ потока,
// даже если это пробельный символ.
Следующий вариант get позволяет управлять числом извлекаемых символов, их размещением и оконечным символом:
get(char* buf, int max, int term=’\n’).
Эта функция считывает символы из входного потока в символьный массив buf до тех пор, пока не будет считано max -1символов, либо пока не встретится символ, заданный term, в зависимости от того, что будет раньше. Завершающий пустой символ (нуль-байт – ‘\0’) добавляется автоматически. По умолчанию оконечным символом (терминатором), который не требуется задавать, является ‘\n’. Сам терминатор в массив buf не считывается и из входного потока istream не удаляется. Массив buf должен иметь размер не менее max символов.
Пример 3.
Ввод строки цифр и преобразование их в целое число типа int с помощью функции atoi(s) и вывод числа в 10-ом, 16-ом, 8-ом форматах с использованием идентификаторов dec, hex, oct, вставленных в поток вывода для управления форматом выходного потока, endlаналог'\n'. (см. Манипуляторы).
#include<iostream.h> // подключение библиотек ввода-вывода
#include<conio.h> // для консольных функций
#include<stdlib.h> // для функции atoi()
const int size=35; // размер буфера строки
void main()
{ clrscr();
int value; // переменная для числа
char s[size]; // буфер строки
cout<<"Value= ";
cin.get(s, size,'\n'); // ввод строки числа
value=atoi(s); // преобразование строки в число
cout<<"Decimal="<<dec<<value<<" Hexadecimal=0x"<<hex<<value
<<" Octal=0"<<oct<<value<<endl;
getch();
}
Результаты программы:
Value=123
Decimal=123 Hexadecimal=0x7b Octal=0173
Замечание. Для ввода строки вместо функции cin.get(s, size,'\n');можно использовать cin>>s. Этот оператор сработает, но если пользователь введет более 34 символов, оператор ввода продолжит запись за пределами буфера s, при этом, возможно, данные и код, располагающиеся за буфером, будут уничтожены. Такая операция может привести к краху системы!
Пример 4.
Использование функции get для безопасного чтения строк.
#include<iostream.h>
void main()
{ char s[35]; // буфер строки
char c; // переменная символа
cout<<"Введите строку не более 34 символов:\n";
cin.get(s, 35); // пропущен 3-й аргумент по умолчанию '\n'
cout<<"Вы ввели строку: "<<s<<endl;
if(cin.get(c) && c!='\n') // если следующий символ не '\n',то вывод
cout<<"Достигнута максимальная длина строки\n";
}
При вводе строки длиной более 34 символов она будет усечена и ввод будет безопасным. Одна проблема все же остается. Символ '\n' или другой символ, завершающий ввод, остается в потоке и должен быть прочитан еще одним оператором cin.get(), как показано последним оператором if. Если cin.get()не читает символ '\n',ввод будет усечен. Этот факт необходимо учитывать при написании программ.
Проблему непрочитанного символа '\n' можно решить с помощью метода cin.getline()с тремя параметрами (совпадет с get()), при этом символ разделитель '\n' также помещается в принимающую строку символов (s).
Для ввода неформатированных и непреобразованных двоичных данных используется функция cin.read((char*)&x, sizeof(x)).
Форматирование ввода/вывода.
Форматирование ввода и вывода определяется флагами состояния формата, перечисленными в классе ios, по умолчанию. Их можно изменить с помощью специальных функций.
Ширина вывода.
По умолчанию вставки выводят минимальное число символов, которыми может быть представлен операнд правой части. Для изменения используются функции:
int ios::width(int w); // устанавливает поле шириной w символов и возвращает предыдущую ширину,
int ios::width(); // возвращает предыдущую ширину, не внося изменений.
По умолчанию width=0, то есть вывод выполняется без заполнителей. При w≠0, если длина числа меньше w используются заполнители, иначе выводится фактическое число без усечения.
Например:
int i=123;
int oldw=cout.width(6);
cout<< i; // вывод будет 123 (перед числом 3 пробела), затем width=0
cout.width(oldw); // восстановление предыдущей ширины width=6
После каждой форматной вставки ширина обнуляется, например:
int i, j;
cout.width(4);
cout<< i <<" "<< j <<;
Здесь i будет выведено четырьмя символами, однако пробел и j будут иметь минимально необходимое число символов.
Заполнители и дополнение вправо и влево.
Символ-заполнитель и направление дополнения зависят от установок внутренних флагов, отвечающих за эти параметры.
По умолчанию символом-заполнителем является пробел. Изменить данное умолчание позволяет функция fill:
int i=123;
cout.fill ("*");
cout.width(6);
cout<< i; // на экран будет выведено ***123
По умолчанию устанавливается выравнивание по правому краю (дополнение символами-заполнителями). Это можно изменить функциями setf и unsetf:
int i=56;
cout.width(6);
cout.fill ("#");
cout.setf (ios::left, ios::adjustfield);
cout<< i; // на экране: 56####
Второй аргумент сообщает setf, какие биты флага должны быть установлены. Первый аргумент сообщает, в какие именно значения устанавливаются эти биты.
Можно также использовать манипуляторы setfill, setiosflags, resetiosflags.
Манипуляторы.
Это специальные операции (похожие на функции) для более простого изменения ширины и других параметров форматирования. Для их использования программа должна иметь строку:
#include<iomanip.h>
Манипуляторы принимают в качестве аргумента ссылку на поток и возвращают ссылку на тот же поток. Поэтому манипуляторы могут объединяться в цепочку вставок (или извлечений из потока) для того, чтобы изменять состояние потока в виде побочного эффекта, без фактического выполнения каких-либо вставок или извлечений. В таблице ниже в столбце синтаксис задано направление потока: входной – ins >>, выходной – outs <<.
Таблица
Манипулятор Синтаксис Действие
Установка флага форматирования с преобразованиями:
dec outs << dec 10-ым ins >> dec
hex outs << hex 16-ым ins >> hex
oct outs << oct 8-ым ins >> oct
ws ins >> ws Извлечение пробелов
endl outs << endl Вставка символа новой строки
и очистка потока
ends outs << ends Вставка конечного нулевого
символа в строку
flush outs << flush Очистка ostream
setbase(int) outs << setbase(n) Установка системы счисления
(0, 8, 10, 16). 0 это по умолча-
нию 10-ая с.с. при выводе и
правила С для литералов це-
лых чисел при вводе
resetiosflags(long) ins>>resetiosflags(1) Очистка битов в ins или outs
outs<<resetiosflags(1) аргументом 1
setiosflags(long) ins >> setiosflags(1) Установка битов в ins или outs
outs << setiosflags(1) аргументом 1
setfill(int) ins >> setfill(n) Установка символа
outs >> setfill(n) заполнителя в n
setprecision(int) ins >> setprecision(n) Установка точности представле-
outs>>setprecision(n) ния чисел с плавающей точкой,
равной n разрядам
setw(int) ins >> setw(n) Установка ширины поля
outs << setw(n) в значение n
Пример 5.
Применение функций и манипуляторов форматирования вывода в программе.
#include<iostream.h>
#include<conio.h>
#include<iomanip.h>
void main()
{ clrscr();
int i=36, j=45;
cout<<"Вывод с установленной шириной поля 5 числа i:\n";
int oldw=cout.width(5); // установка ширины поля вывода
cout<< i <<'\n';
cout<<"После каждой вставки формата ширина поля=0:\n";
cout<< " oldw=" << oldw << '\n';
cout<< i << '\n';
cout<< "Установка поля 5 влияет только на 1-ю переменную:\n";
cout.width(5);
cout<< i << " " << j << '\n';
cout<< "Манипулятор setw(5) упрощает вывод переменных:\n";
cout<< setw(5) << i << setw(5) << j << '\n';
cout<< "Манипуляторы dec, hex, oct изменяют сист. счисл. \n";
cout<< "(оставляя эти изменения в силе) — вывод i:\n";
cout<< dec << i << " " << hex << i << " " << oct << i <<endl;
cout<< "Манипулятор endl аналог '\n' очищает поток"<<endl;
cout<< "i= " << i <<endl;
cout<< "Заполнитель пробел можно заменить на *"<<endl;
cout.fill ( '*' ); // задание символа заполнения функцией fill
cout<< setw(6) << i << endl; // установка ширины поля 6 числа i
cout<< "Выравнивание можно изменить функцией setf:"<<endl;
cout.width(6);
cout.setf(ios::left, ios::adjustfield);
cout<< "i= " << i <<endl;
getch();
}
Вывод чисел с плавающей точкой.
Большие и малые числа выводятся в экспоненциальном формате (е, Е), а не фиксированном. Например, число 1234567.8 печатается 1.2345678Е+07.
Числа с нулевой дробной частью печатаются как целые. Например, число 95.0 печатается 95.
Для вывода десятичных чисел в фиксированном формате можно использовать следующие операторы:
cout.setf(ios::fixed, ios::floated);
cout.setf(ios::showpoint);
Первый вызов функции setf обеспечивает печать чисел в фиксированном, а не экспоненциальном представлении. Второй вызов задает печать десятичной точки, даже если дробная часть числа равна 0.
Управление числом десятичных позиций при выводе выполняется с помощью манипулятора установки точности:
cout<< setprecision(2) << x;
например, вместо числа 16.38567 печатается 16.39.
Пример 6.
Если отсутствует переназначение, то ввод с клавиатуры отображается на экран. Следующая программа копирует cin в cout.
#include<iostream.h>
void main()
{ char ch;
while (cin >> ch)
cout << ch;
}
Обратите внимание, что cin >> ch может рассматриваться как булевское выражение. Этот эффект стал возможен благодаря определениям в классе ios. А именно, такие выражения как (cout) или (cin >> ch) рассматриваются как указатель на переменную, значение которой определяется кодом ошибки потока. Нулевой указатель (трактуемый как "ложь") индицирует ошибку в потоке, а ненулевой указатель ("истина") означает ее отсутствие. Можно использовать операцию отрицания (!), в этом случае (!cout) будет "истина" при возникновении ошибки в потоке cout и "ложь", если они отсутствуют:
if (!cout) errmsg("Output error!");
cin — это входной поток, подключенный к стандартному выводу. Он может правильно обрабатывать все стандартные типы данных. Как известно, в С вывод подсказки (приглашения) без символа новой строки ('\n') в стандартный выходной поток stdout требует обращения к fflush(stdout), чтобы эта подсказка могла появиться. В С++ обращение к cin автоматически очищает cout.
Рассмотрим примеры простых программ на С++ с отличиями от С-программ.
Пример 7.
Применение констант и встроенных функций (inline).
#include<iostream.h>
const float pi=3.14159;
inline float area (const float r) { return pi*(r)*(r); }
void main()
{ float radius;
cout << "Введите радиус круга: ";
cin >> radius;
cout << "Площадь круга= " << area (radius) << '\n';
}
Идентификатор константы ведет себя как обычная переменная (т.е. ее областью действия является блок, в котором она определена) за исключением того, что она не может находиться в левой части оператора присваивания. Директива #define является морально устаревшей для С++.
Ключевое слово inline указывают компилятору, что, если возможно, следует вставлять код в том месте, где он встретил обращение. Это эквивалентно макрорасширению и используется там, где необходимо ликвидировать накладные расходы, связанные с вызовом функции. Рекомендуется применять встроенную функцию, составленную из одной-двух строк.
Ссылки и указатели как параметры функции.
По умолчанию С и С++ передают аргументы функции, используя вызов по значению, чем создается копия аргумента, которая может быть изменена в функции, но исходное значение аргумента не меняется. Для изменения значения аргументов параметры функции объявляются как указатели (*параметр).
Пример 8.
Переставить значения двух аргументов в функции с указателями.
void swap1 (int *a, int *b)
{ int tmp;
tmp=*a; *a=*b; *b=tmp;
}
Для вызова функции необходимо задать адреса аргументов:
swap1(&i, &j); // i, j — аргументы функции
В С++ можно использовать в функции ссылки (&) на изменяемые параметры, а в теле функции и при вызове применяются имена параметров и аргументов:
void swap2 (int &a, int &b)
{ int tmp=a; a=b; b=tmp; }
Вызов функции:
swap2 (i, j);