5.2. Iostreams al rescate

Estos problemas dejan claro que la E/S es una de las principales prioridades para la librería de clases estándar de C++. Como 'hello, worlod' es el primer programa que cualquiera escribe en un nuevo lenguaje, y porque la E/S es parte de virtualmente cualquier programa, la librería de E/S en C++ debe ser particularmente fácil de usar. Tembién tiene el reto mucho mayor de acomodar cualquier nueva clase. Por tanto, estas restricciones requieren que esta librería de clases fundamentales tengan un diseño realmente inspirado. Además de ganar en abstracción y claridad en su trabajo con las E/S y el formateo, en este capítulo verá lo potente que puede llegar a ser esta librería de C++.

5.2.1. Insertadores y extractores

Un stream es un objeto que transporta y formatea carácteres de un ancho fijo. Puede tener un stream de entrada (por medio de los descendientes de la clase istream), o un stream de salida (con objetos derivados de ostream), o un stream que hace las dos cosas simultáneamente (con objetos derivados de iostream). La librería iostream provee tipos diferentes de estas clases: ifstream, ofstream y fstream para ficheros, y istringstream, ostringstream, y stringstream para comunicarese con la clase string del estándar C++. Todas estas clases stream tiene prácticamente la misma interfaz, por lo que usted puede usar streams de manera uniforme, aunque esté trabajando con un fichero, la E/S estándar, una región de la memoria, o un objeto string. La única interfaz que aprenderá también funciona para extensiones añadidas para soportar nuevas clases. Algunas funciones implementan sus comandos de formateo, y algunas funciones leen y escriben caracteres sin formatear.

Las clases stream mencionadas antes son actualmente especializaciones de plantillas, muchas como la clase estándar string son especializaciones de la plantilla basic_string. Las clases básicas en la jerarquia de herencias son mostradas en la siguiente figura: [11]

La clase ios_base declara todo aquello que es común a todos los stream, independientemente del tipo de carácteres que maneja el stream. Estas declaraciones son principalmente constantes y funciones para manejarlas, algunas de ella las verá a durante este capítulo. El resto de clases son plantillas que tienen un tipo de caracter subyacente como parámetro. La clase istream, por ejemplo, está definida a continuación:

typedef basic_istream<char< istream;

Todas las clases mencionadas antes estan definidas de manera similar. También hay definiciones de tipo para todas las clases de stream usando wchar_t (la anchura de este tipo de carácteres se discute en el Capítulo 3) en lugar de char. Miraremos esto al final de este capítulo. La plantilla basic_ios define funciones comunes para la entrada y la salida, pero depende del tipo de carácter subyacente (no vamos a usarlo mucho). La plantilla basic_istream define funciones genéricas para la entrada y basic_ostream hace lo mismo para la salida. Las clases para ficheros y streams de strings introducidas después añaden funcionalidad para sus tipos especificos de stream.

En la librería de iostream, se han sobrecargado dos operadores para simplificar el uso de iostreams. El operador << se denomina frecuentemente instertador para iostreams, y el operador >> se denomina frecuentemente extractor.

Los extractores analizan la información esperada por su objeto destino de acuerdo con su tipo. Para ver un ejemplo de esto, puede usar el objeto cin, que es el equivalente de iostream de stdin en C, esto es, entrada estándar redireccionable. Este objeto viene predefinido cuando usted incluye la cabecera <iostream>.

int i;
  cin >> i;

  float f;
  cin >> f;

  char c;
  cin >> c;

  char buf[100];
  cin >> buf;

Existe un operador sobrecargado >> para cada tipo fundamental de dato. Usted también puede sobrecargar los suyos, como verá más adelante.

Para recuperar el contenido de las variables, puede usar el objeto cout (correspondiente con la salida estándar; también existe un objeto cerr correspondiente con la salida de error estándar) con el insertador <<:

cout << "i = ";
  cout << i;
  cout << "\n";
  cout << "f = ";
  cout << f;
  cout << "\n";
  cout << "c = ";
  cout << c;
  cout << "\n";
  cout << "buf = ";
  cout << buf;
  cout << "\n";

Esto es tedioso y no parece ser un gran avance sobre printf(), aparte de la mejora en la comprobación de tipos. Afortunadamente, los insertadores y extractores sobrecargados están diseñados para ser encadenados dentro de expresiones más complejas que son mucho más fáciles de escribir (y leer):

cout << "i = " << i << endl;
  cout << "f = " << f << endl;
  cout << "c = " << c << endl;
  cout << "buf = " << buf << endl;

Definir insertadores y extractores para sus propias clases es simplemente una cuestion de sobrecargar los operadores asociados para hacer el trabajo correcto, de la siguente manera:

Hacer del primer parámetro una referencia no constante al stream (istream para la entrada, ostream para la salida).

Realizar la operación de insertar/extraer datos hacia/desde el stream (procesando los componentes del objeto).

Retornar una referencia al stream

El stream no debe ser constante porque el procesado de los datos del stream cambian el estado del stream. Retornando el stream, usted permite el encadenado de operaciones en una sentencia individual, como se mostró antes.

Como ejemplo, considere como representar la salida de un objeto Date en formato MM-DD-AAAA . El siguiente insertador hace este trabajo:

ostream& operator<<(ostream& os, const Date& d) {
  char fillc = os.fill('0');
  os << setw(2) << d.getMonth() << '-'
     << setw(2) << d.getDay() << '-'
     << setw(4) << setfill(fillc) << d.getYear();
  return os;
}

Esta función no puede ser miembro de la clase Date por que el operando de la izquierda << debe ser el stream de salida. La función miembro fill() de ostream cambia el carácter de relleno usado cuando la anchura del campo de salida, determinada por el manipulador setw(), es mayor que el necesitado por los datos. Usamos un caracter '0' ya que los meses anteriores a Octubre mostrarán un cero en primer lugar, como '09' para Septiembre. La funcion fill() también retorna el caracter de relleno anterior (que por defecto es un espacio en blanco) para que podamos recuperarlo después con el manipulador setfill(). Discutiremos los manipuladores en profundidad más adelante en este capítulo.

Los extractores requieren algo más cuidado porque las cosas pueden ir mal con los datos de entrada. La manera de avisar sobre errores en el stream es activar el bit de error del stream, como se muestra a continuación:

istream& operator>>(istream& is, Date& d) {
  is >> d.month;
  char dash;
  is >> dash;
  if(dash != '-')
    is.setstate(ios::failbit);
  is >> d.day;
  is >> dash;
  if(dash != '-')
    is.setstate(ios::failbit);
  is >> d.year;
  return is;
}

Cuando se activa el bit de error en un stream, todas las operaciones posteriores serán ignoradas hasta que el stream sea devuelto a un estado correcto (explicado brevemente). Esto es porque el código de arriba continua extrayendo incluso is ios::failbit está activado. Esta implementación es poco estricta ya que permite espacios en blanco entre los numeros y guiones en la cadena de la fecha (por que el operador >> ignora los espacios en blanco por defecto cuado lee tipos fundamentales). La cadena de fecha a continuación es válida para este extractor:

"08-10-2003"
"8-10-2003"
"08 - 10 - 2003"

Pero estas no:

"A-10-2003" // No alpha characters allowed
"08%10/2003" // Only dashes allowed as a delimiter

Discutiremos los estados de los stream en mayor profundidad en la sección 'Manejar errores de stream' después en este capítulo.



[11] Explicadas en profundidad en el capítulo 5.