Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Подбельский учебник с++ / Подбельский - главы 10-12

.pdf
Скачиваний:
28
Добавлен:
22.05.2015
Размер:
1.46 Mб
Скачать

376

Язык Си++

Здесь угловые скобки являются неотъемлемым элементом определения. Список параметров шаблона должен быть заключен именно в угловые скобки.

Аналогично определяется шаблон семейства классов:

template <список_параметров_шаблона> определенив_класса

Шаблон семейства классов определяет способ построения отдельных классов подобно тому, как класс определяет правила построения и формат отдельных объектов. В определении класса, входящего в шаблон, особую роль играет имя класса. Оно является не именем отдельного класса, а параметризованным именем семейства классов.

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

Следуя авторам языка и компилятора Си++ [2, 9], рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствующий класс) при каждом определении конкретного объекта.

Следующий шаблон позволяет автоматически формировать классы векторов с указанными свойствами:

//TEMPLATE.VEC

-

шаблон векторов

 

template <class T> // Т - параметр шаблона

class Vector

 

 

 

{ Т *data;

 

// Начало одномерного массива

int size;

 

// Количество элементов в массиве

public:

 

// Конструктор класса vector

Vector(int)

-Vector()

{

delete[] data; }

// Деструктор

// Расширение действия (перегрузка) операции "[] Т£ operator[] (int i) ( return datafi]; }

// Внешнее определение конструктора класса: template <class T>

Vector -<T>: :Vector(int n) ( data = new T[n];

size = n;

);

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

Глава 10. Наследование и другие возможности классов

377

торых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов:

икя_параметриаованного_класса

<фактические_параметр11_шаблока> имя_обч*кта(параметры_конструктора);

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

V e c t o r < d o u b l e > Z ( 8 ) ;

Проиллюстрируем сказанное следующей программой:

//Р10-11.СРР - формирование классов с помощью шаблона #include "template.vec" // Шаблон классов "вектор" iinclude <iostream.h>

main ()

{ // Создаем объект класса "целочисленный вектор": Vector <int> X(5);

//Создаем объект класса "символьный вектор": Vector <char> С (5);

//Определяем компоненты векторов:

for (int i « О; i < 5; i++)

{ X[i] - i; C[i] = 'A1 + i;} for (i • 0; i < 5 ; i++)

cout « " " « X[i] « ' ' « C[i];

Результат выполнения программы:

0 A IB 2 С 3D 4E

В программе шаблон семейства классов с общим именем Vector используется для формирования двух классов с массивами целого и символьного типов. В соответствии с требованием синтаксиса имя параметризованного класса, определенное в шаблоне (в примере vector), используется в программе только с последующим конкретным фактическим параметром (аргументом), заключенным в угловые скобки. Параметром может быть имя стандартного или определенного пользователем типа. В данном примере использованы стандартные типы int и char. Использовать имя Vector без указания фактического параметра шаблона нельзя - никакое умалчиваемое значение при этом не предусматривается.

378 Язык Си++

В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для которых тип фиксирован:

//Р10-12.СРР •include <iostream.h>

template <class T, int size = 64> class row

{ T «data;

int length;

 

 

public: row()

 

 

( length - size-

 

data - new T[size];

 

~row() { delete[] data; }

 

Tfi operator

[] (int i)

 

( return data[i]; }

 

void B*in()

 

 

{ row <float,8> rf;

 

row <int,8> ri;

 

for (int i - 0; i < 8; i++)

 

{ rf[i] • i; ri[i] - i * i;

}

for (i ш 0; i < 8; i++)

 

cout « "

" « rf[i] « '

' « ri[i];

Результат выполнения программы:

00 11 24 39 4 16 5 25 6 36 7 49

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

Глава 11. ВВОД-ВЫВОД В ЯЗЫКЕ СИ++

11.1. Общие сведения о библиотеке потокового ввода-вывода

Под "программированием на языке Си++" обычно понимается "программирование в среде Си++". Дело в том, что никакая полезная программа не может быть написана на языке Си++ без привлечения библиотек, включаемых в конкретную среду (в компилятор) языка. Конкретная среда Си++, в которой разрабатывается программа, обычно обеспечивает программиста удобными средствами для работы с ее библиотеками. При этом по утверждению Б.Страуструпа: "Для использования библиотеки совсем не нужно знание методов, которые применялись для ее реализации". Однако знание правил использования средств библиотеки совершенно необходимо. Самая незаменимая из этих библиотек - библиотека ввода-вывода, так как средства вво- да-вывода непосредственно в язык Си++ (также как и в язык Си) не входят. В программах на языке Си++ можно равноправно использовать две библиотеки ввода-вывода: стандартную библиотеку функций языка Си (стандарт ANSI С) и библиотеку классов, специально созданную для языка Си++. Библиотека функций языка Си становится доступной в программе, как только в ее заголовок будет включен файл stdio.h. Для обращения к функциям требуются сведения об их прототипах и соблюдение соглашений стандарта (см. прил. 3). Подробную информацию можно получить из технической документации того компилятора, с которым вы работаете.

На протяжении всей книги в программах постоянно использовалась препроцессорная директива:

iinclude <iostream.h>

Назначение указанного в директиве заголовочного файла iostream.h - связать компилируемую программу -с одной из основных частей библиотеки ввода-вывода, построенной на основе механизма классов. Эта библиотека ввода-вывода почти стандартна, так как включена практически во все компиляторы Си++. Однако о стан-

380

Язык Си++

дарте библиотеки ввода-вывода Си++ можно говорить только неформально. Библиотека создана позже, чем появился язык, она разрабатывалась в некотором смысле независимо от создания языка Си++, не входит в формальное описание языка и написана на языке Си++.

Потоки ввод-вывода. В соответствии с названием заголовочного файла iostream.h (stream - поток; "i" - сокращение от input - ввод; "о" - сокращение от output - вывод) описанные в этом файле средства ввода-вывода обеспечивают программиста механизмами для извлечения данных из потоков и для включения (внесения) данных в потоки. Поток определяется как последовательность байтов (символов) и с точки зрения программы не зависит от тех конкретных устройств (файл на диске, принтер, клавиатура, дисплей, стример и т.п.), с которыми ведется обмен данными. При обмене с потоком часто используется вспомогательный участок основной памяти - буфер потока (рис. 11.1- буфер вывода, рис. 11.2- буфер ввода).

О с н о в н а я п а м я т ь

Прикладная программа Выводимые данные

Пересылки (включения) по командам прикладной программы

Буфер вывода

Передача при заполнении буфера или по специальной команде "пересылка буфера"

ГВнешний носитель информации

Рис.11.1.Буферизированныйвыходнойпоток

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

Глава 11. Ввод-вывод в языке Си++

381

 

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

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

Внешний источник информации

|

Передача при пустом буфере ввода

Буфер ввода

Пересылки (извлечения) по командам прикладной программы

Принимающие объекты Прикладная программа

О с н о в н а я п а м я т ь

Рис.11.2.Буферизированныйвходнойпоток

Используемые в программах потоки логически делятся на три ти-

па:

входные, из которых читается информация;

выходные, в которые вводятся данные;

двунаправленные, допускающие как чтение, так и запись.

Все потоки библиотеки ввода-вывода последовательные, т.е. в каждый момент для потока определены позиции записи и (или) чте-

382

Язык Си++

ния, и эти позиции после обмена перемещаются по потоку на длину переданной порции данных.

В соответствии с особенностями "устройства", к которому "присоединен" поток, потоки принято делить на стандартные, консольные, строковые и файловые.

Из перечисленных потоков мы не будем рассматривать только консольные потоки. Причин для этого несколько. Во-первых, консольные потоки отсутствовали в классических реализациях библиотеки потокового ввода-вывода [26]. Во-вторых, консольные потоки несовместимы с операционной средой Microsoft Windows и могут использоваться только при разработке программ, работающих под управлением MS-DOS (см., например, [9, 21, 29, 30, 31]). Консольные потоки поддерживаются классом constream и обеспечивают удобный доступ к терминалу. В них есть возможности работы с клавиатурой и средства манипуляции с участками экрана и с экраном в целом.

Если символы потока в совокупности образуют символьный массив (строку) в основной памяти, то это строковый поток. Если при использовании потока его символы размещаются на внешнем носителе данных (например, на диске или на магнитной ленте), то говорят о файловом потоке или просто о файле. Стандартные и консольные потоки соответствуют передаче данных от клавиатуры и к экрану дисплея. До сих пор во всех программах выполнялся обмен только со стандартными потоками. Рассмотрим их возможности подробнее. Однако предварительно остановимся на некоторых принципиальных особенностях потоковой библиотеки ввода-вывода языка Си++.

Иерархия классов библиотеки ввода-вывода. В отличие от стандартной библиотеки (в которой находятся средства, например, для работы со строками, или математические функции), унаследованной компиляторами языка Си++ от языка Си, библиотека ввода-вывода Си++ является не библиотекой функций, а библиотекой классов. Это первая "промышленная" библиотека классов, разработанная для распространения совместно с компиляторами. Именно эту библиотеку рекомендуют изучать, начиная знакомиться с принципами объектноориентированного программирования [28]. Одним из базовых принципов ООП является предположение о том, что объекты "знают", что нужно делать при появлении обращения (сообщения) определенного типа, т.е. для каждого типа адресованного ему обращения объект имеет соответствующий механизм обработки. Если мы используем объект cout, представляющий выходной поток, то как уже неоднократно показано на примерах, для каждого из базовых типов (int, long, double, ...) этот объект cout выбирает соответствующую проце-

Глава 11. Ввод-вывод в языке Си++

383

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

языках Си или Фортран, когда программист сам определял форму внешнего представления, библиотека классов ввода-вывода хорошо защищена.

Библиотека потоковых классов построена на основе двух базовых классов: ios и streambuf. Класс streambuf обеспечивает буферизацию данных во всех производных классах, которыми явно или неявно пользуется программист. Обращаться к его методам и данным из прикладных программ обычно не нужно. Класс streambuf обеспечивает взаимодействие создаваемых потоков с физическими устройствами. Он обеспечивает производные классы достаточно общими методами для буферизации данных. Класс ios и производные классы содержат указатель на класс streambuf, но об этом можно до времени не вспоминать. Методы и данные класса streambuf программист явно обычно не использует1. Этот класс нужен другим классам библиотеки ввода-вывода. Он доступен и программисту-пользователю для создания новых классов на основе уже существующего класса из iostream. Однако необходимость в построении таких производных классов возникает достаточно редко, и мы не будем рассматривать класс streambuf. Класс ios содержит компоненты (данные и методы), которые являются общими, как для ввода, так и для вывода.

При работе с потоковой библиотекой ввода-вывода программист обычно достаточно активно использует следующие классы:

ios

- базовый потоковый класс;

istream

- класс входных потоков;

oatream

- класс выходных потоков;

iostream

- класс двунаправленных потоков ввода-вывода;

iatrstream

- класс входных строковых потоков;

ostrstreaa

- класс выходных строковых потоков;

strstream

- класс двунаправленных строковых потоков (ввода-

 

вывода);

ifstream

- класс входных файловых потоков;

ofstream

- класс выходных файловых потоков;

f stream

класс двунаправленных файловых потоков (ввода-

 

вывода);

constream

- класс консольных выходных потоков.

384

Язык Си++

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

istream

 

ostream

 

iostream

 

istrstream

strstream

|co:

constream

ifstream

fstream

ostrstream

 

 

ofstream

Рис11.3.Упрощеннаясхемаиерархиипотоковыхклассов

Как наглядно видно из диаграммы классов (рис. П.З.), класс ios является базовым для классов ostream, istream, и опосредовано базовым для всех остальных потоковых классов. Все общие средства потоковых классов помещаются в класс ios. Например, при помощи методов и данных класса ios осуществляется управление процессом передачи символов из буфера и в буфер. При выполнении этих действий необходимы, например, сведения о нужном основании счисления (восьмеричное, десятичное, шестнадцатеричное), о точности представления вещественных чисел, и т.д. (см. флаги). Класс ios содержит эти сведения, т.е. (методы) функции и данные, относящиеся к состояниям потоков и позволяющие менять их свойства.

Потоковые классы, их данные и методы становятся видимыми и доступными в программе, если в нее включен нужный заголовочный файл:

iostream.h

- ДЛЯ классов ios, istream, ostream, stream;

strstrea.h

- для классов istrstream, ostrstream, strstream;

fstream. h

- для классов if stream, of stream, fstream;

constrea.h

- для класса constream.

Глава 11. Ввод-вывод в языке Си++

385

Так как класс ios является базовым для остальных потоковых классов, то включение в текст программы любого из заголовочных файлов s t r s t r e a . h , constrea . h или fstream.h автоматически подключает к программе файл iostream. h. Соответствующие проверки выполняются на этапе препроцессорной обработки.

В заключение перечислим отличительные особенности применения механизма потоков. Потоки обеспечивают:

буферизацию при обменах с внешними устройствами;

независимость программы от файловой системы конкретной операционной системы;

контроль типов передаваемых данных;

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

11.2. Стандартные потоки для базовых типов

Стандартные потоки ввода-вывода. У читателя может возникнуть законный вопрос: почему в программах предыдущих глав использовались потоки ввода-вывода cin, cout, cer r и ничего не требовалось знать о тех классах, к которым они относятся. Достаточно поместить в текст программы препроцессорную процедуру

#include <iostream.h>

и можно с помощью операций включения (записи) данных в поток « и извлечения (чтения) данных из потока » выполнять обмен с дисплеем и клавиатурой ЭВМ.

Объясняется это тем, что заголовочный файл iostream. h не только подключает к программе описания классов ios, istream, ostream, stream, но и содержит определения стандартных потоков ввода-вывода:

cin

- объект класса istream, связанный со стандартным буферизи-

 

рованным входным потоком (обычно клавиатура консоли);

cout

- объект класса ostream, связанный со стандартным буферизи-

 

рованным выходным потоком (обычно дисплей консоли);

cer r

- объект класса ostream, связанный со стандартным небуфери-

 

зированным выходным потоком (обычно дисплей консоли), в

 

который направляются сообщения об ошибках;

386

Язык Си++

clog

- объект класса ostream, связанный со стандартным буфери-

 

зированным выходным потоком (обычно дисплей консоли),

 

в который с буферизацией направляются сообщения об

 

ошибках.

Каждый раз при включении в программу файла iostream.h происходит формирование объектов cin, cout, cerr, clog, т.е. создаются соответствующие стандартные потоки, и программисту становятся доступными связанные с ними средства ввода-вывода. Программист может по своему усмотрению разорвать связь любого из перечисленных объектов с консолью и соединить его с тем или иным файлом, но стандартная (по умолчанию) связь устанавливается именно с клавиатурой (поток cin) и дисплеем (потоки cout, cerr, clog). В том же файле iostream.h, где описаны классы istream, ostream, для них определены оригинальные операции ввода и вывода данных. Операция ввода класса istream называется извлечением (чтением) данных из потока. Она обозначается с помощью символа операции сдвига вправо ». Операция вывода класса ostream называется вставкой или включением (или записью) данных в поток. Она обозначается с помощью символа операции сдвига влево «. Роль операции извлечения и вставки конструкции « и » играют по умолчанию только в том случае, если слева от них находятся объекты, соответственно, классов iostream И ostream:

cin » имя_об"ъекта_баэоиого_типа cout « выражвние_баэового_типа cerr « вцражение_баэоаоро_типа clog « выраженив_ба9о«ого_типа

Выполнение операции » (извлечение из потока) заключается в преобразовании последовательности символов потока в значение типизированного объекта, частным случаем которого является переменная базового типа int, long, double и т.д. При выполнении операции « (включение в поток) осуществляется обратное преобразование - типизированное значение выражения (int, float, char и т.д.) трансформируется в последовательность символов потока. Примеры применения операций включения в поток и извлечения из потока типизированных значений уже приводились многократно. Хорошо бы теперь читателю удивиться некоторым особенностям ввода-вывода с помощью этих операций.

Дело в том, что внешнее (визуальное) представление данных никак не похоже на те внутренние коды, которые используются для их хранения и обработки внутри ЭВМ. Вне ЭВМ это алфавитно-

Глава 11. Ввод-вывод в языке Си++

387

цифровые изображения (чисел или символов), внутри ЭВМ это двоичные коды - последовательности битов (двоичных разрядов) регламентированной для каждого типа длины. При выполнении программы операция вывода данных (например, на экран дисплея) предусматривает преобразование двоичных кодов в символы алфавита, изображаемые на внешнем устройстве. В операции ввода выполняется преобразование сигналов от клавишей клавиатуры в двоичные коды внутреннего представления данных. Чтобы отвлечься (абстрагироваться) от особенностей аппаратурной реализации устройств вводавывода, программист, работая на таком языке, как Си++, использует входные и выходные потоки. Поток в обоих случаях - это последовательность байтов (двоичных кодов фиксированной длины). При выводе коды потока могут изображаться на экране в виде символов принятого алфавита. При вводе по кодам из потока формируются двоичные представления вводимых данных. Сложности и вопросы появляются у программиста при преобразовании данных (кодов) из внешнего (потокового) во внутреннее представление и обратно.

Например, во внутреннем коде целое число может быть представлено двумя1 смежными байтами. Те же самые смежные байты можно рассматривать как внутренние коды двух литер (символов). (Именно так действует объединение union в язцке Си++, позволяя по-разному интерпретировать внутренние коды данных.)

Средства вывода, применяемые в языке, должны иметь возможность распознать тип выводимых данных и в указанном примере поместить в выходной поток либо код внешнего представления целого числа, либо два кода расположенных рядом символов. Для обеспечения такой возможности операция « включения в стандартный выходной поток перегружена. Существуют ее варианты для типов char, unsigned short, signed short, signed int, unsigned int, signed long, unsigned long, float double, long double, char *, void *. Все они доступны после включения в программу файла iostream.h. Отметим, что операция включения определена только для указателей двух типов. Этого вполне достаточно, так как все указатели, отличные от char *, автоматически приводятся к типу void *.

Предопределенный обмен данными со стандартными потоками. Рассмотрим процесс ввода данных с применением операции извлечения. Например, как нужно в программе воспринять последовательность -2.3е+1, набираемую на клавиатуре? Это символьная строка, которую нужно разместить в массиве типа char [ ], или экспоненциальное представление вещественного числа типа float, либо типа double? Набирая на клавиатуре последовательность цифр 1234, можно

25*

388

ЯзыкСи++

интерпретировать ее либо как целое число, либо как символьную строку, либо как значение вещественного числа. А как же правильно

еевоспринять?

Втаких языках, как Фортран или Си, программист с помощью специальных средств форматирования должен указать правила преобразования и формы представления вводимой и выводимой информации. В библиотеке потокового ввода-вывода Си++ возможность форматирования передаваемых данных также существует, но дополнительно имеется и широко используется новый механизм автоматического распознавания типов вводимых и выводимых данных. Он работает подобно механизму перегрузки функций. Потоковые объекты cin, cout, cerr, clog построены таким образом, что ввод и вывод выполняются по-разному в зависимости от типов правого операнда операций вставки « и извлечения ». В текстах программ мы уже неоднократно пользовались этим свойством объектов cin и cout. Оператор

cout « "\nl234 = " « 1234;

выведет с новой строки последовательность символов 1234 • , а затем целое число со значением 1234. И никакой подсказки от программиста не требуется! На основе анализа типа выражения, помещенного справа от операции включения «, в поток помещаются коды внешнего представления данных соответствующих типов.

Подобная ситуация и при вводе данных. По существу, операция » извлечения из потока не одна, а существуют три по-разному выполняемых операции:

для целых чисел;

для вещественных чисел;

для строк.

Все они при чтении по умолчанию игнорируют ведущие пробелы, но затем выполняются по-разному, в зависимости от типа правого операнда. Иллюстрируя сказанное, рассмотрим, как будет воспринята одна и та же последовательность символов, набираемая на клавиатуре, если справа от cin » поместить разные типы данных, но все они будут соответствовать одному и тому же участку памяти:

//Р11-01.СРР - "стандартная" перегрузка операций «, » // для базовых типов

iinclude <iostream.h>

void main()

{ union { long integer;

Глава 11. Ввод-вывод в языке Си++

389

char line[4]; float real;

}mix;

cout « "\п\пВведите целое число (mix.integer): "; cin » mix.integer;

cout « "mix.integer=" « mix.integer; cout « "\nmix.line=" « mix.line; cout « "\nmix.real=" « mix.real;

cout « "\п\пВведите строку (mix.line): "; cin » mix.line;

cout « "mix.integer — " « mix.integer; cout « "\nmix.line = " « mix.line; cout « "\nmix.real = " « mix.real;

cout « "\п\пВведите вещественное число (mix.real): "; cin » mix.real;

cout « "mix.integer = " « mix.integer; cout « "\nmix.line = " « mix.line; cout « "\nmix.real = " « mix.real;

}

Результатвыполнения программы:

Введите целое число (mix.integer): 888 <Enter> mix.integer = 888

mix.line = xv

mix.real = 1.244353e-42

Введите строку (mix.line): 888 <Enter> mix.integer = 3684408

mix.line = 888

mix.real = 5.162955e-39

Введите вещественное число (mix.real): 888 <Enter> mix.integer = 1147011072

mix. line =• mix.real = 888

Впрограмме определено объединение mix, сопоставляющее один

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

char[4], float. Элементы объединения mix.integer, mix.line, mix. real используются в качестве правых операндов операций извлечения из потока » и включения в поток «. В зависимости от типа операнда одна и та же последовательность символов, набираемая на клавиатуре (в примере это 888), воспринимается и заносится в память либо как char [4], либо как long, либо как float. При выводе в поток

390

Язык Си++

cout одно и то же внутреннее значение участка памяти, отведенного для объединения mix, воспринимается и отображается на экране поразному в зависимости от типа правого операнда. В библиотеке вво- да-вывода Си++ для обеспечения указанных возможностей используется тот же самый механизм перегрузки. Объекты cout, cin с операциями «, » "знают", как выполнять ввод-вывод значений разных типов.

Еще раз обратим внимание на результаты выполнения программы. "Правильно" выводятся значения именно тех типов, которые введены. "Неправильные" значения других типов не всегда понятны. Например, после ввода mix.real строка mix.line почему-то оказалась пустой. По-видимому, в первом байте массива char line [4] находится код нуля. Объяснение "неправильных" результатов вывода требует рассмотрения внутренних представлений, которые различны на разных ЭВМ, для разных компиляторов и даже для разных исполнений программы.

Некоторые особенности операций вставки и извлечения. Обратите

внимание, что операции » и « обеспечивают связи с потоками только в том случае, если они употребляются справа от имен потоковых объектов. В противном случае они как обычно обозначают операции сдвига. В соответствии с синтаксисом языка (см. табл. 2.4 "Приоритеты операций") операции сдвига «, » имеют невысокий приоритет. Им "предшествуют", например, все арифметические операции, преобразования типов, скобки и др. Использование операций «, » для обозначения передач данных в потоки и из потоков не изменяет их приоритета. Поэтому допустима, например, такая запись:

c o u t « 2 + 3 + 4 ;

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

Чтобы вывести в поток значение выражения, содержащего операции более низкого ранга чем «, требуется применение скобок:

cout « (а + Ь < с);

Так как условная операция ?: и операции сравнения имеют более низкий приоритет, чем операция сдвига «, то следующий оператор для отрицательного значения х

cout « х < 0 ? -х : х;

Глава 11. Ввод-вывод в языке Си++

391

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

cout « (х < 0 ? -х : х);

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

cout « (2 « 1) ;

выведет в поток (и на экран) значение 4.

Выражения, в которые входят операции » и «, должны иметь значения. В соответствии с определением, находящимся в файле iostream. h, значением выражения

cout « выражение

является ссылка на объект cout, т.е. операция включения « возвращает ссылку на тот потоковый объект, который указан слева от нее в выражении. Следовательно, к результату выполнения операции включения можно вновь применить операцию «, как и к объекту cout. Таким образом рационально применять "цепочки" операций вывода в поток. Например, так:

cout « "\nx * 2 ж » « х * 2;

С помощью скобок можно следующим образом пояснить порядок вычисления этого выражения:

(cout « "\nx * 2 = ") « х * 2;

Если значением х служит 33, то в результате выполнения любого из этих операторов с новой строки на экран будет выведено:

х * 2 - бб

Для к равного 1 после выполнения оператора

cout « "\п к * 2 ж » « (к « 1) « 11 1с « 2 = " « (к « 2) ;

результат на экране дисплея будет таким:

к * 2 = 2 k « 2 = 4

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

392 Язык Си++

от реализации компилятора. Например, после выполнения следующих операторов:

int k = 1;

cout « "\nk++ = " « к++ «

11 (к += 3) = " « (к += 3) « " к++ = " « к++;

на экране получим:

к++ = 5 (к += 3) = 5 к++ = 1

Зависимость от компилятора результатов выполнения цепочки операций включения и необходимость аккуратно учитывать приоритеты операций приводят к следующей нарушенной в этом примере рекомендации [13]: изменяемая переменная не должна появляться в цепочке вывода более одного раза.

"Цепочки" операций обмена можно формировать и при вводе данных (при их извлечении, т.е. чтении) из потока. Например, для i n t i, j , k, l; следующий оператор:

cin » i » j » k » 1;

обеспечивает ввод последовательности целых значений переменных i, j, к, 1. Элементы этой последовательности во входном потоке должны разделяться обобщенными пробельными символами (пробелами, знаками табуляции, символами перевода строк). Исходные данные можно ввести либо размещая их на экране в одну строку (извлечение из потока cin происходит только после сигнала от клавиши Enter):

4 <Enter>

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

1 <Enter> | <Enter>

3<Enter>

£<Enter>

Отметим, что при вводе "в одну строку", когда значения разделяются обобщенными пробельными символами, количество чисел, набранных на клавиатуре, может превышать количество операций извлечения ». Например, тот же результат будет получен (введен), если набрать:

1 9 .4 4 5 в 7 8 9 <Enter>

Глава 11. Ввод-вывод в языке Си++

393

При вводе и выводе значений элементов массивов необходимо явно использовать их обозначения в виде индексированных переменных. Например, попытка выполнить операторы:

float real[3] = { 10.0, 20.0, 30.0 } ; cout « real;

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

cout « real[0] « " " « real[l] « " " « real[2];

При попытке ввести значения элементов массива с помощью оператора

cin » real;

получим сообщение об ошибке на этапе компиляции. Для операторов

double e[5];

for (int i = 0; i < 5; i++) cin » e[i];

на клавиатуре необходимо набрать, например, такую последовательность значений:

0 . 0 1 0 . 0 2 0 . 0 3 0 . 0 4 0 . 0 5 < E n t e r >

При вводе-выводе целых чисел существуют ограничения на длины внешнего и внутреннего представлений. Например, если при выполнении операторов

int i; cin » i; cout « "\ni = " « i;

набрать на клавиатуре число 123456789, то результатом, выведенным на экран дисплея, будет (в конкретном случае)

i - -13035

Та же последовательность цифр 123456789, набираемая на клавиатуре для длинного целого long g, будет безболезненно воспринята и выведена операторами:

cin » g; cout « "\ng = " « g;

394

Язык Си++

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

cout « 123456789;

все будет в порядке - именно такое значение будет выведено на экран. Оператор с недопустимой константой, например, такой:

cout « 98765432100;

приведет к выводу неверного числового значения. В соответствии с ограничениями реализации компилятора ВС++ справа от знака включения « можно записывать целые константы от 0 до 4294967295

(см. табл. 2.1).

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

int N; cin

N; cout « "\nN = « N;

можно ввести восьмеричное значение 077777 и получить ответ в десятичном виде:

N = 32767

Введяшестнадцатеричноезначение0X7FFF,получим

N = -32767

И Т.Д.

Значения указателей (т.е. адреса) выводятся в стандартный поток в шестнадцатеричном виде, как мы это видели в гл. 5. Вывод числового значения, например типа int, в шестнадцатеричном или восьмеричном виде по умолчанию не выполняется. Для смены десятичного основания необходимо специальное "воздействие" на поток. Средства такого управления потоком будут рассмотрены позже, хотя в программе Р5-03.СРР мы уже использовали флаг hex, переключающий поток cout на вывод числовых значений в шестнадцатеричной форме.

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

float pi; cin » pi; cout « "pi

« pi;

Глава 11. Ввод-вывод в языке Си++

395

при вводе числового значения в виде 3.141593, или 3.141593еО, или +3.141593, или о. 3141593е+1 всегда приведут к печати:

pi = 3.141593

Если при вводе вещественного значения набрать на клавиатуре: 3.1415926535897 932385 <Enter>

то в выходной поток cout опять будет выведено

pi = 3.141593

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

float at; cin » at; cout « "\nat = " « at;

При вводе с клавиатуры значения

0.00000000000000000000001 <Enter>

на экран будет выведено:

at = 1е-23

Тот же результат будет выведен, если на клавиатуре набрать

1.0е-23.

Наличие буфера в стандартном входном потоке создает некоторые особенности. В процессе набора данных на клавиатуре они отображаются на экране, но не извлекаются из потока. Это дает возможность исправлять допущенные ошибки во вводимых данных до того, как значения будут выбраны из входного потока. Извлечение данных из потока, т.е. собственно выполнение операции cin » происходит только после нажатия клавиши Enter. При этом вся набранная строка переносится в буфер ввода, и именно из буфера ввода начинается "чтение". При извлечении числовых данных игнорируются начальные пробельные символы. Чтение начинается с первого непробельного символа и заканчивается при появлении не числового символа. При вводе целых читаются символы ' + ', ' - ' , десятичные цифры и, если число вводится в шестнадцатеричном виде (признак 0х), то буквы А, а, в, ь, с, с, D, d, E, e, F, f. Для вещественных чисел дополнительно может появиться символ Е или е, как обозначение экспоненциальной часта числа, и точка, отделяющая целую часть от дробной. Выборка из