c++book1
.pdf
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
где x является int, будет интерпретироваться как:
(cerr.operator<<("x = ")).operator<<(x);
В частности, отсюда следует, что когда один оператор вывода печатает несколько элементов, они будут печататься в ожидаемом порядке: слева направо. Наличие operator<<, которая получает int, является избыточным, поскольку int может неявно преобразовываться в long. С другой стороны, int может преобразовываться также и в double. Наличие ostream::operator<<(int) позволяет избежать этой неоднозначности. Для печати символов в виде символов предоставляется функция ostream::put(char), а ostream::operator<<(int) печатает их целые значения.
8.2.2Вывод Определяемых Пользователем Типов
Рассмотрим определяемый пользователем тип:
class complex (* double re, im;
public:
complex(double r = 0, double i = 0) (* re=r; im=i; *)
friend double real(complex& a) (* returna.re; *) friend double real(complex& a) (* returna.re; *)
friend complex operator+(complex, complex); friend complex operator-(complex, complex); friend complex operator*(complex, complex); friend complex operator/(complex, complex); // ...
*);
Операцию << для нового типа complex можно определить так:
ostream& operator<<(ostream&s, complex z) (*
return s << "(" << real(z) << "," << imag(z) << ")";
*)
и использовать точно так же, как для встроенного типа:
complex x(1,2); // ...
cout << "x = " << x << "\n";
получая при этом
x = (1,2)
Определение действия вывода для определяемого пользователем типа не требует ни модификации описания класса ostream, ни доступа к структуре данных (скрытой), которую этот класс поддерживает. Очень удачно, что имеет место первое, потому что описание класса ostream находится в стандартных заголовочных файлах, к которым у
221
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
обычного пользователя нет доступа на запись. Последнее также важно, потому что обеспечивает защиту от случайной порчи структуры данных. Это также позволяет менять реализацию ostream не влияя на пользовательские программы.
8.2.3 Некоторые Подробности Разработки
Операция вывода используется, чтобы избежать той многословности, которую дало бы использование функции вывода. Но почему <<?
Возможности изобрести новый лексический символ нет (#6.2). Операция присваивания была кандидатом одновременно и на ввод, и на вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода отличалась от операции вывода. Кроме того, = не в ту сторону связывается (ассоциируется), то есть cout=a=b
означает cout=(a=b).
Делались попытки использовать операции < и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей, что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми. Помимо этого, "<" находится на большинстве клавиатур как раз на ",", и у людей получаются операторы вроде такого:
cout < x , y , z;
Для таких операторов непросто выдавать хорошие сообщения об ошибках.
Операции << и >> к такого рода проблемам не приводят, они асимметричны в том смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно низок, чтобы можно было не использовать скобки для арифметических выражений в роли операндов. Например:
cout << "a*b+c=" << a*b+c << "\n";
Естественно, при написании выражений, которые содержат операции с более низкими приоритетами, скобки использовать надо. Например:
cout << "a^b!c=" << (a^b!c) << "\n";
Операцию левого сдвига тоже можно применять в операторе вывода:
cout << "a<<b=" << (a<<b) << "\n";
В С++ нет выражений с символьными значениями, в частности, '\n' является целым (со значением 10, если используется набор символов ASCII), поэтому
cout << "x = " << x << '\n';
напечатает число 10, а не ожидаемый символ новой строки. Эту и аналогичные проблемы можно сгладить, определив несколько макросов (в которых используются стандартные имена символов ASСII):
#define sp << " " #define ht << "\t" #define nl << "\n"
222
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
Теперь предыдущий пример запишется в виде:
cout << "x = " << x nl;
Для печати символов предоставляются функции ostream::put (char) и chr(int) (см. #8.2.4). Хотя в некоторых кругах несинтаксические макросы считаются худшим видом макросов, мне они нравятся.
Рассмотрим примеры:
cout << x << " " << y << " " << z << "\n"; cout << "x = " << x << ", y = " << y << "\n";
Люди находят их трудно читаемыми из-за большого числа кавычек и того, что операция вывода внешне выглядит слишком непривычно. Здесь могут помочь приведенные выше макросы и несколько отступов:
cout << x sp << y sp << z nl; cout << "x = " << x
<<", y = " << y nl;
8.2.4Форматированный Вывод
Пока << применялась только для неформатированного вывода, и на самом деле в
реальных программах она именно для этого главным образом и применяется. Помимо этого существует также несколько форматирующих функций, создающих представление своего параметра в виде строки, которая используется для вывода. Их второй (необязательный) параметр указывает, сколько символьных позиций должно использоваться.
char* oct(long, int=0); // восьмеричное представление char* dec(long, int=0); // десятичное представление
char* hex(long, int=0); // шестнадцатиричное представление char* chr(int, int=0); // символ
char* str(char*, int=0); // строка
Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например:
cout << "dec(" << x
<<") = oct(" << oct(x,6)
<<") = hex(" << hex(x,4)
<<")";
Если x==15, то в результате получится:
dec(15) = oct( 17) = hex( f);
Можно также использовать строку в общем формате:
char* form(char* format ...);
223
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
cout<<form() эквивалентно применению стандартной функции вывода языка C printf()*. form() возвращает строку, получаемую в результате преобразования и форматирования ее параметров, которые стоят после первого управляющего параметра - строки формата format. Строка формата состоит из объектов двух типов: обычных символов, которые просто копируются в поток вывода, и спецификаций преобразования, каждая из которых влечет преобразование и печать следующего из параметров. Каждая спецификация преобразования начинается с символа %. Например:
--------------------
* Объяснение того, как применяются строки формата, - это слегка отредактированный вариант спецификации printf(). (прим. автора)
cout<<form("there were %d members present",no_of_members);
Здесь %d указывает, что no_of_members должно рассматриваться как int и печататься в виде соответствующей последовательности десятичных цифр. Если no_of_members==127, вывод будет такой:
there were 127 members present
Множество спецификаций преобразования довольно велико и обеспечивает высокую степень гибкости. После % может стоять:
- необязательный знак минус, который задает выравнивание преобразованного значения влево в указанном поле;
d необязательная строка цифр, задающая ширину поля. Если преобразованное значение имеет меньше цифр, чем ширина поля, оно будет дополняться пробелами слева (или справа,
если был задан индикатор выравнивания влево) до заполнения всей ширины поля; если ширина поля начинается с нуля, то вместо дополнения пробелами будет делаться дополнение нулями;
. необязательная точка, для отделения ширины поля от следующей строки цифр;
d необязательная строка цифр, специфицирующая точность, которая задает число цифр после десятичной точки для преобразований e и f или печатаемых символов для строки;
* в ширине поля или точности вместо строки цифр может стоять *. В этом случае ширина поля и точность задается целым параметром;
h необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру короткое целое;
l необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру длинное целое;
%указывает, что должен быть напечатан символ %, никакие параметры при этом не затрагиваются;
c символ, указывающий, какой тип преобразования должен применяться. Символы преобразования и их значения таковы:
224
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
d целый параметр преобразуется в десятичную запись;
o целый параметр преобразуется в восьмеричную запись;
x целый параметр преобразуется в шестнадцатиричную запись;
f параметр float или double преобразуется в десятичную запись вида [-]ddd.ddd, где число, задаваемое цифрами d после десятичной точки, эквивалентно спецификации точности для параметра. Если точность опущена, дается шесть цифр; если точность явно задана как 0, то не печатается десятичная точка и не печатается ни одной цифры;
e параметр float или double преобразуется в десятичную запись вида [-]d.ddde+dd, где перед десятичной точкой стоит одна цифра, а число, задаваемое цифрами после десятичной точки, эквивалентно спецификации точности для параметра; когда точность опущена, выдается шесть цифр;
g параметр float или double печатается в том из видов d, f или e, который обеспечивает полную точность при минимальной затрате места;
c печатается символьный параметр, пустые символы игнорируются;
s параметр воспринимается как строка (указатель на символ), и печатаются символы из строки до пустого символа или до тех пор, пока не будет достигнуто число символов, указанное спецификацией точности; но если точность равна нулю, печатаются все символы до пустого;
u беззнаковый целый параметр преобразуется в десятичную запись.
Несуществующая или недостаточная ширина поля никогда не приводит к обрезанию поля; дополнение поля записи имеет место только в том случае, если указанная ширина поля превышает фактическую ширину.
Вот более сложный пример:
char* src_file_name; int line;
char* line_format = "\n#line %d \"%s\"\n"; //...
cout << "int a;\n";
cout << form(line_format,line,src_file_name); cout << "int b;\n";
который печатает
int a;
#line 13 "С++/main.c" int b;
Применение form() небезопасно в смысле того, что не выполняется проверка типа. Вот, например, хорошо хорошо известный способ получить непредсказуемый вывод и/или дамп (core dump):
225
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
char x; // ...
cout<<form("bad input char: %s",x);
Правда, она дает большую гибкость в том виде, который хорошо знаком программистам на C. Потоковый вывод можно смешивать с выводом в стиле printf().
В настоящее время нет полностью удовлетворительных средств, обеспечивающих форматированный вывод типов, определяемых пользователем* В частности, вероятно нужно будет найти стандартный способ передавать функции вывода для определяемого пользователем типа информацию, которая позволит ей определить пространственные ограничения, вид заполнения, левое или правое выравнивание и т.п. такими, какими они определяются в ее вызове. Вполне осуществимый, но не идеальный подход состоит в том, чтобы снабжать определяемый пользователем тип функциями, которые порождают соответствующее строковое представление объекта, для которого они вызываются, аналогично форматирующим функциям oct(), hex() и т.д. Например:
class complex (* float re,im;
public:
// ...
char* string(char* format)
(* return form(format,re,im); *)
*); // ...
cout << z.string("(%.3f,%.3f)");
Память для хранения строк, которые возвращают form(), hex() и т.п., берется из одного статически размещаемого циклического буфера, поэтому не имеет смысла сохранять указатель, возвращаемый любой из этих функций, для дальнейшего использования. Указываемые символы будут меняться.
8.2.5 Виртуальная Функция Вывода
Иногда функция вывода должна быть virtual. Рассмотрим пример класса shape, который дает понятие фигуры (#1.18):
class shape (*
//...
public:
//...
virtual void draw(ostream& s); // рисует "this" на "s" *);
class circle : public shape (* int radius;
public:
// ...
void draw(ostream&);
*);
226
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
То есть, круг имеет все признаки фигуры и может обрабатываться как фигура, но имеет также и некоторые специальные свойства, которые должны учитываться при его обработке.
Чтобы поддерживать для таких классов стандартную парадигму вывода, операция << определяется так:
ostream& operator<<(ostream& s, shape* p) (*
p->draw(s); return s;
*)
Если next - итератор типа определенного в #7.3.3, то список фигур распечатывается например так:
while ( p = next() ) cout << p;
8.3 Файлы и Потоки
Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.
8.3.1 Инициализация Потоков Вывода ostream имеет конструкторы:
class ostream (* // ...
ostream(streambuf* s); // связывает с буфером потока ostream(int fd); // связывание для файла
ostream(int size, char* p); // связывает с вектором
*);
Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс, управляющий буферами; он описывается в #8.6, как и класс filebuf, управляющий streambuf для файла. Класс filebuf является производным от класса streambuf.
Описание стандартных потоков вывода cout и cerr, которое находится в исходных кодах библиотеки потоков ввода/вывода, выглядит так:
//описать подходящее пространство буфера char cout_buf[BUFSIZE]
//сделать "filebuf" для управления этим пространством
//связать его с UNIX'овским потоком вывода 1 (уже открытым) filebuf cout_file(1,cout_buf,BUFSIZE);
// сделать ostream, обеспечивая пользовательский интерфейс
227
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
ostream cout(&cout_file);
char cerr_buf[1];
//длина 0, то есть, небуферизованный
//UNIX'овский поток вывода 2 (уже открытый) filebuf cerr_file(2,cerr_buf,0);
ostream cerr(&cerr_file);
Примеры двух других конструкторов ostream можно найти в #8.3.3 и #8.5.
8.3.2 Закрытие Потоков Вывода
Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():
ostream::~ostream()
(*
flush(); // сброс
*)
Сбросить буфер можно также и явно. Например:
cout.flush();
8.3.3 Открытие Файлов
Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах и здесь подробно не описываются. Поскольку после включения <stream.h> становятся доступны cin, cout и cerr, во многих (если не во всех) программах не нужно держать код для открытия файлов. Вот, однако, программа, которая открывает два файла, заданные как параметры командной строки, и копирует первый во второй:
#include <stream.h>
void error(char* s, char* s2) (*
cerr << s << " " << s2 << "\n"; exit(1);
*)
main(int argc, char* argv[]) (*
if (argc != 3) error("неверное число параметров","");
filebuf f1;
if (f1.open(argv[1],input) == 0)
error("не могу открыть входной файл",argv[1]); istream from(&f1);
filebuf f2;
228
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
if (f2.open(argv[2],output) == 0)
error("не могу создать выходной файл",argv[2]); ostream to(&f2);
char ch;
while (from.get(ch)) to.put(ch);
if (!from.eof() !! to.bad())
error("случилось нечто странное","");
*)
Последовательность действий при создании ostream для именованного файла та же, что используется для стандартных потоков: (1) сначала создается буфер (здесь это делается посредством описания filebuf); (2) затем к нему подсоединяется файл (здесь это делается посредством открытия файла с помощью функции filebuf::open()); и, накрнец,
(3) создается сам ostream с filebuf в качестве параметра. Потоки ввода обрабатываются аналогично.
Файл может открываться в одной из двух мод:
enum open_mode (* input, output *);
Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан.
Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии (см. #8.4.2). При завершении программы открытые файлы неявно закрываются.
Файл можно также открыть одновременно для чтения и записи, но в тех случаях, когда это оказывается необходимо, парадигма потоков редко оказывается идеальной. Часто лучше рассматривать такой файл как вектор (гигантских размеров). Можно определить тип, который позволяет программе обрабатывать файл как вектор, см. Упражнения 8- 10.
8.3.4Копирование Потоков
Есть возможность копировать потоки. Например:
cout = cerr;
В результате этого получаются две переменные, ссылающиеся на один и тот же поток. Гавным образом это бывает полезно для того, чтобы сделать стандартное имя вроде cin ссылающимся на что-то другое (пример этого см. в #3.1.6)
8.4 Ввод
Ввод аналогичен выводу. Имеется класс istream, который предоставляет операцию >> ("взять из") для небольшого множества стандартных типов. Функция operator>> может определяться для типа, определяемого пользователем.
229
Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.
8.4.1Ввод Встроенных Типов
Класс istream определяется так:
class istream (*
//...
public:
istream& operator>>(char*); // строка istream& operator>>(char&); // символ istream& operator>>(short&); istream& operator>>(int&);
istream& operator>>(long&); istream& operator>>(float&); istream& operator>>(double&);
//...
*);
Функции ввода определяются в таком духе:
istream& istream::operator>>(char& c); (*
// пропускает пропуски
int a;
// неким образом читает символ в "a" c = a;
*)
Пропуск определяется как стандартнчй пропуск в C, через вызов isspase() в том виде, как она определена в <ctype.h> (пробел, табуляция, символ новой строки, перевод формата и возврат каретки).
В качестве альтернативы можно использовать функции get():
class istream (* // ...
istream& get(char& c); // char
istream& get(char* p, int n, int ='\n'); // строка
*);
Они обрабатывают символы пропуска так же, как остальные символы. Функция istream::get(char) читает один символ в свой параметр; другая istream::get читает не более n символов в вектор символов, начинающийся в p. Необязательный третий параметр используется для задания символа остановки (иначе, терминатора или ограничителя), то есть этот символ читаться не будет. Если будет встречен символ ограничитель, он останется как первый символ потока. По умолчанию вторая функция get будет читать самое большее n символов, но не больше чем одну строку, '\n' является ограничителем по умолчанию. Необязательный третий параметр задает символ, который читаться не будет. Например:
cin.get(buf,256,'\t');
230
