Ввод Типов, Определяемых Пользователем
Ввод для пользовательского типа может определяться точно так же, как вывод, за тем исключением, что для операции ввода важно, чтобы второй параметр был ссылочного типа. Например:
istream& operator>>(istream& s, complex& a)
/*
форматы ввода для complex; "f" обозначает float:
f
( f )
( f , f )
*/
{
double re = 0, im = 0;
char c = 0;
s >> c;
if (c == '(') {
s >> re >> c;
if (c == ',') s >> im >> c;
if (c != ')') s.clear(_bad); // установить state
}
else {
s.putback(c);
s >> re;
}
if (s) a = complex(re,im);
return s;
}
Несмотря на то, что не хватает кода обработки ошибок, большую часть видов ошибок это на самом деле обрабатывать будет. Локальная переменная c инициализируется, чтобы ее значение не оказалось случайно '(' после того, как операция окончится неудачно. Завершающая проверка состояния потока гарантирует, что значение параметра a будет изменяться только в том случае, если все идет хорошо.
Операция установки состояния названа clear() (очистить), потому что она чаще всего используется для установки состояния потока заново как _good. _good является значением параметра по умолчанию и для istream::clear(), и для ostream::clear().
Над операциями ввода надо поработать еще. Было бы, в частности, замечательно, если бы можно было задавать ввод в терминах шаблона (как в языках Снобол и Икон), а потом проверять, прошла ли успешна вся операция ввода. Такие операции должны были бы, конечно, обеспечивать некоторую дополнительную буферизацию, чтобы они могли восстанавливать поток ввода в его исходное состояние после неудачной попытки распознавания.
Инициализация Потоков Ввода
Естественно, тип istream, так же как и ostream, снабжен конструктором:
class istream {
// ...
istream(streambuf* s, int sk =1, ostream* t =0);
istream(int size, char* p, int sk =1);
istream(int fd, int sk =1, ostream* t =0);
};
Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет
cout.flush(); // пишет буфер вывода
С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream. Например:
int y_or_n(ostream& to, istream& from)
/*
"to", получает отклик из "from"
*/
{
ostream* old = from.tie(&to);
for (;;) {
cout << "наберите Y или N: ";
char ch = 0;
if (!cin.get(ch)) return 0;
if (ch != '\n') { // пропускает остаток строки
char ch2 = 0;
while (cin.get(ch2) && ch2 != '\n') ;
}
switch (ch) {
case 'Y':
case 'y':
case '\n':
from.tie(old); // восстанавливает старый tie
return 1;
case 'N':
case 'n':
from.tie(old); // восстанавливает старый tie
return 0;
default:
cout << "извините, попробуйте еще раз: ";
}
}
}
Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игнорирует.
Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядывать вперед" в поток ввода.
Работа со Строками
Можно осуществлять действия, подобные вводу/выводу, над символьным вектором, прикрепляя к нему istream или ostream. Например, если вектор содержит обычную строку, завершающуюся нулем, для печати слов из этого вектора можно использовать приведенный выше копирующий цикл:
void word_per_line(char v[], int sz)
/*
печатет "v" размера "sz" по одному слову на строке
*/
{
istream ist(sz,v); // сделать istream для v
char b2[MAX]; // больше наибольшего слова
while (ist>>b2) cout << b2 << "\n";
}
Завершающий нулевой символ в этом случае интерпретируется как символ конца файла.
В помощью ostream можно отформатировать сообщения, которые не нужно печатать тотчас же:
char* p = new char[message_size];
ostream ost(message_size,p);
do_something(arguments,ost);
display(p);
Такая операция, как do_something, может писать в поток ost, передавать ost своим подоперациям и т.д. с помощью стандартных операций вывода. Нет необходимости делать проверку не переполнение, поскольку ost знает свою длину и когда он будет переполняться, он будет переходить в состояние _fail. И, наконец, display может писать сообщения в "настоящий" поток вывода. Этот метод может оказаться наиболее полезным, чтобы справляться с ситуациями, в которых окончательное отображение данных включает в себя нечто более сложное, чем работу с традиционным построчным устройством вывода. Например, текст из ost мог бы помещаться в располагающуюся где-то на экране область фиксированного размера.
Буферизация
При задании операций ввода/вывода мы никак не касались типов файлов, но ведь не все устройства можно рассматривать одинаково с точки зрения стратегии буферизации. Например, для ostream, подключенного к символьной строке, требуется буферизация другого вида, нежели для ostream, подключенного к файлу. С этими проблемами можно справиться, задавая различные буферные типы для разных потоков в момент инициализации (обратите внимание на три конструктора класса ostream). Есть только один набор операций над этими буферными типами, поэтому в функциях ostream нет кода, их различающего. Однако функции, которые обрабатывают переполнение сверху и снизу, виртуальные. Этого достаточно, чтобы справляться с необходимой в данное время стратегией буферизации. Это также служит хорошим примером применения виртуальных функций для того, чтобы сделать возможной однородную обработку логически эквивалентных средств с различной реализацией. Описание буфера потока в выглядит так:
struct streambuf { // управление буфером потока
char* base; // начало буфера
char* pptr; // следующий свободный char
char* qptr; // следующий заполненный char
char* eptr; // один из концов буфера
char alloc; // буфер, выделенный с помощью new
// Опустошает буфер:
// Возвращает EOF при ошибке и 0 в случае успеха
virtual int overflow(int c =EOF);
// Заполняет буфер
// Возвращет EOF при ошибке или конце ввода,
// иначе следующий char
virtual int underflow();
int snextc() // берет следующий char
{
return (++qptr==pptr) ? underflow() : *qptr&0377;
}
// ...
int allocate() // выделяет некоторое пространство буфера
streambuf() { /* ... */}
streambuf(char* p, int l) { /* ... */}
~streambuf() { /* ... */}
};
Обратите внимание, что здесь определяются указатели, необходимые для работы с буфером, поэтому обычные посимвольные действия можно определить (только один раз) в виде максимально эффективных inline- функций. Для каждой конкретной стратегии буферизации необходимо определять только функции переполнения overflow() и underflow(). Например:
struct filebuf : public streambuf {
int fd; // дескриптор файла
char opened; // файл открыт
int overflow(int c =EOF);
int underflow();
// ...
// Открывает файл:
// если не срабатывает, то возвращает 0,
// в случае успеха возвращает "this"
filebuf* open(char *name, open_mode om);
int close() { /* ... */ }
filebuf() { opened = 0; }
filebuf(int nfd) { /* ... */ }
filebuf(int nfd, char* p, int l) : (p,l) { /* ... */ }
~filebuf() { close(); }
};
int filebuf::underflow() // заполняет буфер из fd
{
if (!opened || allocate()==EOF) return EOF;
int count = read(fd, base, eptr-base);
if (count < 1) return EOF;
qptr = base;
pptr = base + count;
return *qptr & 0377;
}
Эффективность
Можно было бы ожидать, что раз ввод/вывод определен с помощью общедоступных средств языка, он будет менее эффективен, чем встроенное средство. На самом деле это не так. Для действий вроде "поместить символ в поток" используются inline-функции, единственные необходимые на этом уровне вызовы функций возникают из-за переполнения сверху и снизу. Для простых объектов (целое, строка и т.п.) требуется по одному вызову на каждый. Как выясняется, это не отличается от прочих средств ввода/вывода, работающих с объектами на этом уровне.
Упражнения
1. (*1.5) Считайте файл чисел с плавающей точкой, составьте из пар считанных чисел комплексные числа и выведите комплексные числа.
2. (*1.5) Определите тип name_and_address (имя_и_адрес). Определите для него << и >>. Скопируйте поток объектов name_and_address.
3. (*2) Постройте несколько функций для запроса и чтения различного вида информации. Простейший пример - функция y_or_n() в #8.4.4. Идеи: целое, число с плавающей точкой, имя файла, почтовый адрес, дата, личные данные и т.д. Постарайтесь сделать их защищенными от дурака.
4. (*1.5) Напишите программу, которая печатает (1) все буквы в нижнем регистре, (2) все буквы, (3) все буквы и цифры, (4) все символы, которые могут встречаться в идентификаторах C++ на вашей системе, (5) все символы пунктуации, (6) целые значения всех управляющих символов, (7) все символы пропуска, (8) целые значения всех символов пропуска, и (9) все печатаемые символы.
5. (*4) Реализуйте стандартную библиотеку ввода/вывода C () с помощью стандартной библиотеки ввода/вывода C++ ().
6. (*4) Реализуйте стандартную библиотеку ввода/вывода C++ () с помощью стандартной библиотеки ввода/вывода C ().
7. (*4) Реализуйте стандартные библиотеки C и C++ так, чтобы они могли использоваться одновременно.
8. (*2) Реализуйте класс, для которого [] перегружено для реализации случайного чтения символов из файла.
9. (*3) Как Упражнение 8, только сделайте, чтобы [] работало и для чтения, и для записи. Подсказка: сделайте, чтобы [] возвращало объект "дескрипторного типа", для которого присваивание означало бы присвоить файлу через дескриптор, а неявное преобразование в char означало бы чтение из файла через дескриптор.
10. (*2) Как Упражнение 9, только разрешите [] индексировать записи некоторого вида, а не символы.
11. (*3) Сделайте обобщенный вариант класса, определенного в Упражнении 10.
12. (*3.5) Разработайте и реализуйте операцию ввода по сопоставлению с образцом. Для спецификации образца используйте строки формата в духе printf. Должна быть возможность попробовать сопоставить со вводом несколько образцов для нахождения фактического формата. Можно было бы вывести класс ввода по образцу из istream.
13. (*4) Придумайте (и реализуйте) вид образцов, которые намного лучше.
[Назад] [Содержание] [Вперед]
Справочное руководство по С++
1. Введение
2. Договоренности о Лексике
2.1 Комментарии |
2.2 Идентификаторы (имена) |
2.3 Ключевые слова |
2.4 Константы |
2.5 Строки |
2.6 Характеристики аппаратного обеспечения |
3. Запись Синтаксиса
4. Имена и Типы
4.1 Область видимости |
4.2 Определения |
4.3 Компоновка |
4.4 Классы памяти |
4.5 Основные типы |
4.6 Производные типы |
5. Объекты и lvalue (адреса)
6. Преобразования
6.1 Символы и целые |
6.2 Float и double |
6.3 Плавающие и целые |
6.4 Указатели и целые |
6.5 Unsigned |
6.6 Арифметические преобразования |
6.7 Преобразования указателей |
6.8 Преобразования ссылок |
7. Выражения
7.1 Основные выражения |
7.2 Унарные операции |
7.3 Мультипликативные операции |
7.4 Аддитивные операции |
7.5 Операции сдвига |
7.6 Операции отношения |
7.7 Операции равенства |
7.8 Операция побитовое И |
7.9 Операция побитовое исключающее ИЛИ |
7.10 Операция побитовое включающее ИЛИ |
7.11 Операция логическое И |
7.12 Операция логическое ИЛИ |
7.13 Условная операция |
7.14 Операции присваивания |
7.15 Операция запятая |
7.16 Перегруженные операции |
8. Описания
8.1 Спецификаторы класса памяти |
8.2 Спецификаторы Типа |
8.3 Описатели |
8.4 Смысл описателей |
8.5 Описания классов |
8.6 Инициализация |
8.7 Имена типов |
8.8 Определение типа typedef |
8.9 Перегруженные имена функций |
8.10 Описание перечисления |
8.11 Описание Asm |
9. Операторы
9.1 Оператор выражение |
9.2 Составной оператор, или блок |
9.3 Условный оператор |
9.4 Оператор while |
9.5 Оператор do |
9.6 Оператор for |
9.7 Оператор switch |
9.8 Оператор break |
9.9 Оператор continue |
9.10 Оператор return |
9.11 Оператор goto |
9.12 Помеченные операторы |
9.13 Пустой оператор |
9.14 Оператор delete |
9.15 Оператор asm |
10. Внешние Определения
10.1 Определения функций |
10.2 Определения внешних данных |
11. Правила Области Видимости
12. Командные Строки Компилятора
12.1 Замена идентификаторов |
12.2 Включение файлов |
12.3 Условная компиляция |
12.4 Управление строкой |
13. Неявные Описания
14. Обзор Типов
14.1 Классы |
14.2 Функции |
14.3 Массивы, указатели и индексирование |
14.4 Явные преобразования указателей |
15. Константные Выражения
16. Соображения Мобильности
17. Свободная Память
18. Краткое Изложение Синтаксиса
18.1 Выражения |
18.2 Описания |
18.3 Операторы |
18.4 Внешние определения |
18.5 Препроцессор |
19. Отличия от "старого C"
19.1 Расширения |
[Назад] [Содержание] [Вперед]
Введение
Язык программирования C++ - это C*1, расширенный введением классов, inline-функций, перегруженных операций, перегруженных имен функций, константных типов, ссылок, операций управления свободной памятью, проверки параметров функций. Коротко различия между С++ и "старым С" приведены в #15. В этом руководстве описывается язык по состоянию на Июнь 1985.
Договоренности о Лексике
Есть шесть классов лексем: идентификаторы, ключевые слова, константы, строки, операторы и прочие разделители. Символы пробела, табуляции и новой строки, а также комментарии (собирательно - "белые места"), как описано ниже, игнорируются, за исключением тех случаев, когда они служат разделителями лексем. Некое пустое место необходимо для разделения идентификаторов, ключевых слов и констант, которые в противном случае окажутся соприкасающимися.
Если входной поток разобран на лексемы до данного символа, принимается, что следующая лексема содержит наиболее длинную строку символов из тех, что могут составить лексему.
Комментарии
Символы /* задают начало комментария, заканчивающегося символами */. Комментарии не могут быть вложенными. Символы // начинают комментарий, который заканчивается в конце строки, на которой они появились.
2.2 Идентификаторы (имена)
Идентификатор - последовательность букв и цифр произвольной длины; первый символ обязан быть буквой; подчерк '_' считается за букву; буквы в верхнем и нижнем регистрах являются различными.
Ключевые слова
Следующие идентификаторы зарезервированы для использования в качестве ключевых слов и не могут использоваться иным образом:
asm auto break case char
class const continue default delete
do double else enum extern
float for friend goto if
inline int long new operator
overload public register return short
sizeof static struct switch this
typedef union unsigned virtual void
while
Идентификаторы signed и volatile зарезервированы для применения в будущем.
Константы
2.4.1 Целые константы | |
2.4.2 Явно заданные длинные константы | |
2.4.3 Символьные константы | |
2.4.4 Константы с плавающей точкой | |
2.4.5 Перечислимые константы | |
2.4.6 Описанные константы |
Как описано ниже, есть несколько видов констант. В #2.6 приводится краткая сводка аппаратных характеристик, которые влияют на их размеры.
Целые константы
Целая константа, состоящая из последовательности цифр, считается восьмиричной, если она начинается с 0 (цифры ноль), и десятичной в противном случае. Цифры 8 и 9 не являются восьмиричными цифрами. Последовательность цифр, которой предшествует 0х или 0Х, воспринимается как шестнадцатеричное целое. В шестнадцатеричные цифры входят буквы от а или А до f или F, имеющие значения от 10 до 15. Десятичная константа, значение которой превышает наибольшее машинное целое со знаком, считается длинной (long); восьмеричная и шестнадцатеричная константа, значение которой превышает наибольшее машинное целое со знаком, считается long; в остальных случаях целые константы считаются int.