Опубликован: 10.10.2006 | Уровень: специалист | Доступ: свободно
Лекция 10:

Потоки

10.3 Ввод

Ввод во многом сходен с выводом. Есть класс istream, который реализует операцию ввода >> ("ввести из" - "input from") для небольшого набора стандартных типов. Для пользовательских типов можно определить функцию operator>>.

10.3.1 Ввод встроенных типов

Класс istream определяется следующим образом:

class istream : public virtual ios {
    //...
public:
    istream& operator>>(char*);     // строка
    istream& operator>>(char&);     // символ
    istream& operator>>(short&);
    istream& operator>>(int&);
    istream& operator>>(long&);
    istream& operator>>(float&);
    istream& operator>>(double&);
    //...
};

Функции ввода operator>> определяются так:

istream& istream::operator>>(T& tvar)
{
 // пропускаем обобщенные пробелы
 // каким-то образом читаем T в`tvar'
  return *this;
 }

Теперь можно ввести в VECTOR последовательность целых, разделяемых пробелами, с помощью функции:

int readints(Vector<int>& v)
// возвращаем число прочитанных целых
{
   for (int i = 0; i<v.size(); i++)
   {
      if (cin>>v[i]) continue;
      return i;
   }
   // слишком много целых для размера Vector
   // нужна соответствующая обработка ошибки
 }

Появление значения с типом, отличным от int, приводит к прекращению операции ввода, и цикл ввода завершается. Так, если мы вводим

1 2 3 4 5. 6 7 8.

то функция readints() прочитает пять целых чисел

1 2 3 4 5

Символ точка останется первым символом, подлежащим вводу. Под пробелом, как определено в стандарте С, понимается обобщенный пробел, т.е. пробел, табуляция, конец строки, перевод строки или возврат каретки. Проверка на обобщенный пробел возможна с помощью функции isspace() из файла <ctype.h>.

В качестве альтернативы можно использовать функции get():

class istream : public virtual ios {
    //...
    istream& get(char& c);     // символ
    istream& get(char* p, int n, char ='n');   // строка
};

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

Функция istream::get(char&) вводит один символ в свой параметр. Поэтому программу посимвольного копирования можно написать так:

main()
{
  char c;
  while (cin.get(c)) cout << c;
}

Такая запись выглядит несимметрично, и у операции >> для вывода символов есть двойник под именем put(), так что можно писать и так:

main()
{
   char c;
   while (cin.get(c)) cout.put(c);
}

Функция с тремя параметрами istream::get() вводит в символьный вектор не более n символов, начиная с адреса p. При всяком обращении к get() все символы, помещенные в буфер (если они были), завершаются 0, поэтому если второй параметр равен n, то введено не более n-1 символов. Третий параметр определяет символ, завершающий ввод. Типичное использование функции get() с тремя параметрами сводится к чтению строки в буфер заданного размера для ее дальнейшего разбора, например так:

void f()
{
    char buf[100];
    cin >> buf; // подозрительно
    cin.get(buf,100,'\n');      // надежно
    //...
 }

Операция cin>>buf подозрительна, поскольку строка из более чем 99 символов переполнит буфер. Если обнаружен завершающий символ, то он остается в потоке первым символом подлежащим вводу. Это позволяет проверять буфер на переполнение:

void f()
{
   char buf[100];

   cin.get(buf,100,'\n');   // надежно

   char c;
   if (cin.get(c) && c!='\n') {
      // входная строка больше, чем ожидалось
   }
   //...
 }

Естественно, существует версия get() для типа unsigned char.

В стандартном заголовочном файле <ctype.h> определены несколько функций, полезных для обработки при вводе:

int isalpha(char)   // 'a'..'z' 'A'..'Z'
int isupper(char)   // 'A'..'Z'
int islower(char)   // 'a'..'z'
int isdigit(char)   // '0'..'9'
int isxdigit(char)  // '0'..'9' 'a'..'f' 'A'..'F'
int isspace(char)   // ' ' '\t' возвращает конец строки
    // и перевод формата
int iscntrl(char)   // управляющий символ в диапазоне
    // (ASCII 0..31 и 127)
int ispunct(char)   // знак пунктуации, отличен от приведенных выше
int isalnum(char)   // isalpha() | isdigit()
int isprint(char)   // видимый: ascii ' '..'~'
int isgraph(char)     // isalpha() | isdigit() | ispunct()
int isascii(char c)   { return 0<=c && c<=127; }

Все они, кроме isascii(), работают с помощью простого просмотра, используя символ как индекс в таблице атрибутов символов. Поэтому вместо выражения типа

(('a'<=c && c<='z') || ('A'<=c && c<='Z')) // буква

которое не только утомительно писать, но оно может быть и ошибочным (на машине с кодировкой EBCDIC оно задает не только буквы), лучше использовать вызов стандартной функции isalpha(), который к тому же более эффективен.

В качестве примера приведем функцию eatwhite(), которая читает из потока обобщенные пробелы:

istream& eatwhite(istream& is)
{
     char c;
     while (is.get(c)) {
         if (isspace(c)==0) {
             is.putback(c);
             break;
         }
      }
      return is;
 }

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

10.3.2 Состояния потока

С каждым потоком ( istream или ostream ) связано определенное состояние. Нестандартные ситуации и ошибки обрабатываются с помощью проверки и установки состояния подходящим образом.

Узнать состояние потока можно с помощью операций над классом ios:

class ios { //ios является базовым для ostream и istream
    //...
public:
    int eof() const;     // дошли до конца файла
    int fail() const;    // следующая операция будет неудачна
    int bad() const;     // поток испорчен
    int good() const;    // следующая операция будет успешной
    //...
};

Последняя операция ввода считается успешной, если состояние задается good() или eof(). Если состояние задается good(), то последующая операция ввода может быть успешной, в противном случае она будет неудачной. Применение операции ввода к потоку в состоянии, задаваемом не good(), считается пустой операцией. Если произошла неудача при попытке чтения в переменную v, то значение v не изменилось (оно не изменится, если v имеет тип, управляемый функциями члена из istream или ostream ). Различие между состояниями, задаваемыми как fail() или как bad() уловить трудно, и оно имеет смысл только для разработчиков операций ввода. Если состояние есть fail(), то считается, что поток не поврежден, и никакие символы не пропали; о состоянии bad() ничего сказать нельзя.

Значения, обозначающие эти состояния, определены в классе ios:

class ios {
    //...
public:
    enum io_state {
        goodbit=0,
        eofbit=1,
        filebit=2,
        badbit=4,
    };
    //...
};

Истинные значения состояний зависят от реализации, и указанные значения приведены только, чтобы избежать синтаксически неправильных конструкций.

Проверять состояние потока можно следующим образом:

switch (cin.rdstate()) {
case ios::goodbit:
    // последняя операция с cin была успешной
    break;
case ios::eofbit:
    // в конце файла
    break;
 case ios::filebit:
    // некоторый анализ ошибки
    // возможно неплохой
    break;
  case ios::badbit:
    //  cin возможно испорчен
    break;
  }

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

const int _good = ios::goodbit;
const int _bad = ios::badbit;
const int _file = ios::filebit;
const int _eof = ios::eofbit;

typedef ios::io_state state_value ;

Разработчики библиотек должны заботиться о том, чтобы не добавлять новых имен к глобальному пространству именования. Если элементы перечисления входят в общий интерфейс библиотеки, они всегда должны использоваться в классе с префиксами, например, как ios::goodbit и ios::io_state.

Для переменной любого типа, для которого определены операции << и >>, цикл копирования записывается следующим образом:

while (cin>>z) cout << z << '\n';

Если поток появляется в условии, то проверяется состояние потока, и условие выполняется (т.е. результат его не 0) только для состояния good(). Как раз в приведенном выше цикле проверяется состояние потока istream, что является результатом операции cin>>z. Чтобы узнать, почему произошла неудача в цикле или условии, надо проверить состояние. Такая проверка для потока реализуется с помощью операции приведения (7.3.2).

Так, если z является символьным вектором, то в приведенном цикле читается стандартный ввод и выдается для каждой строки стандартного вывода по одному слову (т.е. последовательности символов, не являющихся обобщенными пробелами). Если z имеет тип complex, то в этом цикле с помощью операций, определенных в 10.2.2 и 10.2.3, будут копироваться комплексные числа. Шаблонную функцию копирования для потоков со значениями произвольного типа можно написать следующим образом:

complex z;
iocopy(z,cin,cout);    // копирование complex

double d;
iocopy(d,cin,cout);    // копирование double
char c;
iocopy(c,cin,cout);    // копирование char

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

10.3.3 Ввод пользовательских типов

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

istream& operator>>(istream& s, complex& a)
/*
   формат input рассчитан на 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(ios::badbit); // установим состояние
   }
   else {
       s.putback(c);
       s >> re;
   }

   if (s) a = complex(re,im);
   return s;
}

Несмотря на сжатость кода, обрабатывающего ошибки, на самом деле учитывается большая часть ошибок. Инициализация локальной переменной с нужна для того, чтобы в нее не попало случайное значение, например '(', в случае неудачной операции. Последняя проверка состояния потока гарантирует, что параметр a получит значение только при успешном вводе.

Операция, устанавливающая состояние потока, названа clear() (здесь clear - очистить), поскольку чаще всего она используется для восстановления состояния потока как good() ; значением по умолчанию для параметра ios::clear() является ios::goodbit.

Равиль Ярупов
Равиль Ярупов
Привет !
Федор Антонов
Федор Антонов
Оплата и обучение
Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва