Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


5.3.3b  E/S con formato

§1  Sinopsis

En capítulos anteriores hemos indicado que las rutinas STL dedicadas al manejo de Entradas/Salidas, se dividen en dos grandes grupos según se refieran a operaciones formateadas y no formateadas (planas).  En el apartado 5.3.1 explicamos las diferencias entre ambos tipos de operación y dedicamos el capítulo anterior ( 5.3.3a) a las herramientas disponibles controlar flujos no formateados.  En este capítulo centraremos la atención sobre las operaciones de E/S con formato.

§2  Insertores y extractores

Las E/S formateadas son controladas por versiones sobrecargadas de los operadores >> << para manejo de bits ( 4.9.3), que aquí tienen un significado totalmente distinto [1]. Recordemos que los operadores de entrada (>>) sen denominan extractores, y los de salida (<<) insertores [2]. Así pues, si stream es un objeto derivado de una clase iostream, las expresiones

stream << objeto;
stream.operator<<(objeto);

son formas sintácticas equivalentes que representan un insertor. Es decir, el operador que introduce o "inserta" en el flujo controlado por el objeto stream una secuencia representada por objeto.  De forma análoga, las expresiones

stream >> objeto;
stream.operator>>(objeto);

son también formas equivalentes de representar un extractor.  El operador que lee o "extrae", del flujo controlado por el objeto stream, una secuencia que es transformada en objeto.  El caso más socorrido es cuando stream es uno de los flujos estándar ( 5.3):

std::cout << "Introduzca su saldo";
std::cin >> number;

La primera sentencia hace aparecer el mensaje en la pantalla; la segunda almacena el flujo proveniente del teclado en el Lvalue number.

Es importante observar que la secuencia de bits insertada (o extraída) no tiene porqué corresponder con la máscara de bits del operando derecho de la expresión. Esta posible falta de correspondencia es precisamente la característica principal de las operaciones de E/S formateadas. Siguiendo con el ejemplo anterior, si escribimos:

float number;
std::cin >> number;
std::cout << number;

La segunda línea es un extractor que lee datos del flujo controlado por cin (el teclado) y los introduce en el objeto number. (puede considerarlo como un flujo de bits de sentido cin  -> number). Como este Rvalue es un float y la operación es formateada, el extractor es capaz de sustituir el dato introducido. Por ejemplo 12.5, por una máscara de bits tal como 01000001 01001000 00000000 00000000 que es colocada en el Lvalue de number.

La tercera sentencia es un insertor que inserta en el flujo controlado por cout (la pantalla) el Rvalue indicado por number (puede considerarlo como un flujo de bits de sentido number ->  cout). Como en este flujo es también formateado, el operador es capaz por sí mismo de traducir la secuencia 01000001 01001000 00000000 00000000 en una sucesión de bits correspondientes a los caracteres 1, 2, . y 5, de forma que el resultado observado es 12.5.

§3  Operaciones encadenadas

Los flujos estándar permiten inserciones y extracciones encadenadas del tipo

std::cout << "Hola mundo " << "te encuentro cansado\n";

La razón de esta sintaxis hay que buscarla en que las funciones-operador de inserción/extracción  operator<<() y operator>>() están definidas de forma que siempre devuelven una referencia al flujo correspondiente .  De esta forma, la expresión

std::cout << "Hola mundo ";

que en realidad equivale a la invocación

std::cout.operator>>("Hola mundo ");

devuelve un objeto, que supondremos c, de tipo std::cout&.  En estas condiciones, como la evaluación se realiza de izquierda a derecha, la expresión inicial es equivalente a:

c.operator>>("te encuentro cansado\n");

c << "te encuentro cansado\n";

donde c es un alias de std::cout, de forma que la sentencia funciona correctamente.

§4  Mecánica de funcionamiento

A veces es útil conocer los detalles del "cómo" y el "porqué", así que expondremos brevemente algunos detalles del mecanismo de los flujos E/S, empezando por los estándar.

§4.1  Caracteres en blanco

Para empezar, señalaremos que las operaciones de entrada están influidas por la presencia de determinados caracteres, denominados espacios en blanco o  "whitespaces" que son eliminados automáticamente (ignorados).  Whitespaces son todos los caracteres c que devuelvan true en la invocación isspace(c), utilizando la versión de esta función contenida en la librería tradicional C. Su resultado depende del localismo utilizado, pero por defecto, se consideran "espacios" los caracteres siguientes:  el espacio (0x20), la tabulación horizontal (0x09), nueva línea (0x0A), tabulación vertial (0x0B), salto de formato (form-feed 0x0C) y retorno de carro (0x0D).

Nota:  Este comportamiento por defecto, de ignorar los "whitespaces" iniciales, puede ser modificado limpiando (poner a 0) el bit skipws en la máscara fmtflags del flujo ( 5.3.2a)


§4.2  En expresiones del tipo 

int x;

char name[50];

char c;

...

std::cin >> x;        // L.1

std::cin >> name;     // L.2

std::cin >> c;        // L.3

std::cout << x << "; " << name << "; " << c << std::endl;

el ejecutable explora la secuencia de caracteres recibidos (generalmente del teclado), comenzando por eliminar los "whitespaces" iniciales [3]. A continuación, se extraen los caracteres que pueden corresponder con el tipo esperado, hasta que se encuentra el primero que no concuerda, o un nuevo whitespace (estos últimos no son extraídos y permanecen en el buffer de entrada).  Finalmente, realiza las conversiones de formato pertinentes sobre los caracteres extraídos y coloca el resultado en el Lvalue correspondiente.

En el caso anterior, introduciendo la secuencia:

  12 Hola.

c

se obtiene

12; Hola.; c

Pero si la secuencia de entrada es

12 Hol a. c

Se obtiene

12; Hol; a

Tema relacionado: Ver el comentario al especificador width() de ancho de campo ( 5.3.2a3)

§5  Extractores e insertores predefinidos

El hecho de que puedan escribirse expresiones como:

stream << objeto;

stream >> objeto;

exige la existencia de versiones de los métodos stream.operator>>(x) y stream.operator>>(x) que acepten un tipo objeto como argumento. Para facilitar las cosas, las clases iostreams incluyen una completa colección de versiones sobrecargadas de los operadores  >>  <<  para los tipos básicos del lenguaje: bool, char, int, long, float, Etc. El resultado es que ambos operadores pueden ser utlizados "tal cual" con los tipos básicos. El mecanismo de resolución de sobrecarga ( 4.4.1a) es el encargado de seleccionar la versión adecuada en cada caso concreto. Por ejemplo, cuando el compilador encuentra las sentencias

int x;

char* name = "                   ";

char c;

...

cin >> x;        // L.1

cin >> name;     // L.2

cin >> c;        // L.3


En L.1 realmente utiliza una invocación al método std::cin.operator>>(int&); en L.2 utiliza la versión std::cin.operator>>(void*&), y en L.3 utiliza la invocación std::cin.oprator>>(char&), donde cin es una instancia de la clase basic_istream.  Observe que en todos los casos se utiliza una referencia como argumento, a través de la cual es posible alterar el objeto.

En el caso de L.2, el prototipo acepta una referencia a puntero genérico, con lo que puede recibir un puntero de cualquier tipo (en nuestro caso un puntero-a-carácter).

 Además de las versiones sobrecargadas para los tipos básicos, existen algunas más sofisticadas. Por ejemplo, la tercera de la lista del siguiente epígrafe:

std::istream& operator>> (ios_base& (* pf)(ios_base&))

En este caso la función-operador devuelve, como siempre, una referencia a un stream, pero su argumento es un puntero-a-función aceptando una referencia a un iostream y devolviendo una referencia al mismo tipo. En el próximo capítulo tendremos ocasión de ver la utilidad de estas extrañas definiciones.


  Es importante resaltar que, la totalidad de operadores predefinidos, devuelven una referencia al flujo. Esta condición es suficiente y necesaria para que puedan ser utilizados en operaciones encadenadas .

§5.1  Extractores

Están definidos en la clase basic_istream . Son versiones sobrecargadas del operador >> definidas mediante funciones-operador operator>>(). Las hay de dos clases: como funciones-miembro de basic_istream ( 5.3.2c) y como funciones genéricas (plantillas) externas. Los métodos son los siguientes:


basic_istream<charT,traits>& operator>>
   (basic_istream<charT,traits>& (* pf)(basic_istream<charT, traits>&))
basic_istream<charT,traits>& operator>>
   (basic_ios<charT,traits>& (* pf)(basic_ios<charT, traits>&))
basic_istream<charT,traits>& operator>> (ios_base& (* pf)(ios_base&))
basic_istream<charT,traits>& operator>>(bool& n);
basic_istream<charT,traits>& operator>>(short& n);
basic_istream<charT,traits>& operator>>(unsigned short& n);
basic_istream<charT,traits>& operator>>(int& n);
basic_istream<charT,traits>& operator>>(unsigned int& n);
basic_istream<charT,traits>& operator>>(long& n);
basic_istream<charT,traits>& operator>>(unsigned long& n);
basic_istream<charT,traits>& operator>>(float& f);
basic_istream<charT,traits>& operator>>(double& f);
basic_istream<charT,traits>& operator>>(long double& f);
basic_istream<charT,traits>& operator>>(void*& p);
basic_istream<charT,traits>& operator>>(basic_streambuf<char_type,traits>* sb);

Además de los anteriores, se dispone de seis funciones-operador externas para extracción de caracteres:

template<class charT, class traits> basic_istream<charT,traits>&

   operator>>(basic_istream<charT,traits>&, charT&);
template<class traits> basic_istream<char, traits>&

   operator>>(basic_istream<char,traits>&, unsigned char&);
template<class traits> basic_istream<char, traits>&

   operator>>(basic_istream<char,traits>&, signed char&);
template<class charT, class traits> basic_istream<charT,traits>&

   operator>>(basic_istream<charT,traits>&, charT*);
template<class traits> basic_istream<char, traits>&

   operator>>(basic_istream<char,traits>&, unsigned char*);
template<class traits> basic_istream<char, traits>&

   operator>>(basic_istream<char,traits>&, signed char*);

§5.2  Insertores

Están definidos en la clase basic_ostream. Son versiones sobrecargadas del operador << definidas mediante funciones-operador operator<<(). Las hay de dos clases: como funciones-miembro de basic_ostream ( 5.3.2d) y como funciones genéricas (plantillas) externas. Los métodos se destinan a salidas de valores diversos, especialmente numéricos. Son los siguientes:

basic_ostream<charT,traits>& operator<<
   (basic_ostream<charT,traits>& (* pf)(basic_ostream<charT,traits>&));
basic_ostream<charT,traits>& operator<<
   (basic_ios<charT,traits>& (* pf)(basic_ios<charT,traits>&));
basic_ostream<charT,traits>& operator<<(ios_base& (* pf)(ios_base&));
basic_ostream<charT,traits>& operator<<(bool n);
basic_ostream<charT,traits>& operator<<(short n);
basic_ostream<charT,traits>& operator<<(unsigned short n);
basic_ostream<charT,traits>& operator<<(int n);
basic_ostream<charT,traits>& operator<<(unsigned int n);
basic_ostream<charT,traits>& operator<<(long n);
basic_ostream<charT,traits>& operator<<(unsigned long n);
basic_ostream<charT,traits>& operator<<(float f);
basic_ostream<charT,traits>& operator<<(double f);
basic_ostream<charT,traits>& operator<<(long double f);
basic_ostream<charT,traits>& operator<<(const void* p);
basic_ostream<charT,traits>& operator<<(basic_streambuf<char_type,traits>* sb);

Además de los anteriores, existen varias funciones-operador definidas como funciones genéricas (plantillas) externas:

template<class charT, class traits> basic_ostream<charT,traits>&

   operator<<(basic_ostream<charT,traits>&, charT);
template<class charT, class traits> basic_ostream<charT,traits>&

   operator<<(basic_ostream<charT,traits>&, char);

template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, char);
template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, signed char);
template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, unsigned char)
template<class charT, class traits> basic_ostream<charT,traits>&

   operator<<(basic_ostream<charT,traits>&, const charT*);
template<class charT, class traits> basic_ostream<charT,traits>&

   operator<<(basic_ostream<charT,traits>&, const char*);
template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, const char*);
template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, const signed char*);
template<class traits> basic_ostream<char,traits>&

   operator<<(basic_ostream<char,traits>&, const unsigned char*);


Como puede verse, la práctica totalidad de los operadores de desplazamiento << y >> ("shift operators") de los tipos preconstruidos, son funciones miembro de las respectivas clases iostream, (basic_istream y basic_ostream), aunque existen excepciones.  Los operadores de desplazamiento para los tipos carácter como char y wchar_t son funciones genéricas ( 4.12.1) en el espacio de las librerías estándar ::std ( 4.1.11c2) definidas como sigue:

 

// operator>>()

template<class charT, class traits>

   basic_istream<charT, traits>& 
   basic_istream<charT, traits>::operator>>(type& x) {   // Leer x
     return *this;      // devolver referencia al objeto ( 4.11.6)
   }

 

// operator<<()

template<class charT, class traits>
   basic_ostream<charT, traits>&
   basic_ostream<charT, traits>::operator<<(type x) {    // escribir x
   return *this;        // devolver referencia al objeto
}

 §6  Extractores e insertores de usuario

Para que los flujos estándar puedan ser utilizados directamente con objetos de tipo abstracto (tipos definidos en el programa 2.2), es preciso escribir versiones propias del operador, que puedan manejar los nuevos tipos.  Para ilustrarlo con un caso concreto, supongamos la socorrida clase de los complejos.

Tenemos una clase Complex para manejar complejos, que naturalmente tienen una parte real y una imaginaria, y queremos utilizar el flujo estándar de salida cout, con elementos c de nuestra clase, de forma que, al utilizar la expresión.

std::cout << c;        // L.1

obtengamos directamente el valor del argumento c en la forma  a + ib, siendo a y b las partes real e imaginaria respectivamente. También deseamos poder utilizar el extractor cin con elementos c, de forma que al escribir:

std::cin >> c;         // L.2

podamos introducir sucesivamente dos cantidades numéricas, que se almacenen en los miembros a y b del complejo c.

Para simplificar supondremos un diseño esquelético de la clase (desprovista de todo lo que a los autores C++, les gusta denominar "azúcar sintáctico"):

struct Complex {
    float r;        // parte real
    float i;        // parte imaginaria
    Complex(float r=0, float i=0)

        : r(r), i(i) {}    // constructor por defecto
};


La expresión L.1 requiere la definición de un insertor específico para objetos Complex, que puede tener el siguiente aspecto:

std::ostream& operator<< (std::ostream& stream, Complex complejo) {
    stream << complejo.r << "+i" << complejo.i;
    return stream;
}

La función es de lo más sencillo, pero merece un par de puntualizaciones: en primer lugar, es una función del mismo espacio que Complex, no es un método ni una función-operador de dicha clase (volveremos sobre esto más adelante).  Recibe dos argumentos, ambos por referencia: un flujo y un objeto de la clase Complex.  Devuelve una referencia a un objeto tipo ostream del espacio std (como debe ser) y esta referencia es precisamente el "stream" pasado como primer argumento.

El cuerpo de la función no merece más puntualización.  Organiza la inserción sucesiva de los miembros real e imaginario del complejo pasado como argumento, con la grafía requerida. Finalmente devuelve el "stream" recibido como primer argumento.  Observe que, en la inserción de los miembros real e imaginario, se utilizan versiones std::stream<<(float), que suponemos ya están disponibles en en el objeto "stream".  Cosa que es cierta desde luego, puesto que la utilizaremos con std::cout, que es un tipo ostream .

En estas condiciones, ante una sentencia como L.1, el compilador busca una función-operador que responda a la "firma" requerida, y aquí caben dos opciones ( 4.9.18):  un método std::cout.operator<<(c) que acepte un tipo Complex como argumento [4], o una función externa operator<<(cout, c), que acepte el tipo de cout y un Complex.  Como el cuerpo de la clase basic_ostream no es extensible y sería suicida por nuestra parte retocar los ficheros de cabecera del compilador, la elección adoptada es la segunda.

Por su parte, la expresión L.2 exige la definición de un extractor específico para objetos Complex cuyo diseño puede ser el siguiente:

std::istream& operator>> (std::istream& stream, Complex& complejo) {
   float real;

   float imag;
   if (stream >> real >> imag) {
       complejo = Complex(real, imag);
   }
   return stream;
}

Aquí caben prácticamente las mismas observaciones que en el caso anterior; el valor devuelto y los argumentos son idénticos y el cuerpo de la clase es también de lo más sencillo:  organiza la extracción sucesiva de dos float y los asigna a los miembros real e imaginario del complejo recibido en el segundo argumento.

Las condiciones respecto al compilador son también análogas. Ante una expresión como L.2, caben dos alternativas: una función-operador std::cin.operator>>(c) que reciba un tipo Complex como argumento, o una función externa capáz:  operator>>(cin, c) como la adoptada en nuestra solución.

§7  Recapitulación

La solución propuesta representa desde luego un mínimo, que puede perfeccionarse en función de las necesidades. Aunque no suele ser necesario con los flujos estándar, pueden incluirse sentencias de comprobación de que las operaciones stream<< y stream>> del interior se efectúen sin incidencias ( 5.3.2b).  Tales comprobaciones serían desde luego obligatorias si en lugar de E/S de flujos estándar fuesen "streams" conectados con otros dispositivos.  Por ejemplo, ficheros de disco o comunicaciones.

También podríamos refinar el sistema de entrada de datos del extractor. Por ejemplo, en lugar de introducir dos números separados por "whitespaces" [3], podría realizarse un análisis de la secuencia de entrada y aceptar varias posibilidades alternativas en cuanto a formato en que se proporcionan los datos: x y(x, y); x,y, etc. También podría programarse que la ausencia de un miembro en la secuencia tuviese un significado concreto. Por ejemplo,  x= x+j0; ,x= 0+jx.


  Tema relacionado:  La sobrecarga de extractores e insertores se realiza generalmente para operaciones de modificación de formato en los flujos de E/S.  En este caso reciben un nombre específico, al que dedicamos el próximo capítulo: (Manipuladores 5.3.3c).

  Inicio.


[1]  El creador del lenguaje nos informa que el hecho de elegir precisamente estos y no otros operadores para definir las nuevas funcionalidades, fue porque representaban la opción menos mala de las posibles Stroustrup TC++PL  §21.2.  No era aconsejable introducir un nuevo símbolo en el lenguaje (para definir un nuevo operador) y los existentes tenían características y usos muy definidos, o hubiesen presentado inconvenientes notacionales y de análisis sintáctico.

[2]  Auque los métodos que controlan las E/S no-formateadas realizan también operaciones de "extraer" e "insertar" caracteres en los flujos que controlan, sin embargo estos nombres (insertores y extractores) se utilizan solamente para los operadores de E/S formateadas.  Observe que en el primer caso, el operador "inserta" caracteres en un flujo, y en el segundo "extrae" caracteres del mismo (ver figura 2 5.3) para colocarlos en algún Lvalue.

[3]  Como se ha señalado el comportamiento por defecto, ignorar los whitespaces, puede ser controlado a través del bit skipws en la máscara fmtflags del flujo, pero también puede ser forzada explícitamente mediante un manipulador:

cin >> ws;      // eliminar espacios iniciales

[4]  Como cout es un ostream, lo anterior supone que la búsqueda del método requerido se realizará en la jerarquía encabezada por ios_base.