STREAMAS(1)

LIBRERIA DE CASES DE E/S DEL C++

En los capítulos anteriores se viene realizando la entrada y salida a través de la consola empleando los operadores sobrecargados >> y << de C++. Aunque Turbo C++ admite todo el rico conjunto de funciones de E/S de C, los últimos capítulos los han ignorado en favor de los operadores de E/S de C++. Esto se ha hecho por una razón principal: usar el método de E/S de C++ ayuda a pensar de forma orientada a objetos, y a comprender el valor de la filosofía de "un interfaz, múltiples métodos". En este capítulo vamos a aprender más acerca del sistema de E/S de C++, incluyendo la forma en que se sobrecargan los operadores << y >> para que sea posible leer o escribir objetos de las clases que nosotros mismos diseñemos. El sistema de E/S de C++ es muy vasto, y no es posible tratar todas las funciones y todas las características, pero en este capítulo vamos a presentarle las funciones y características más importantes y de uso más frecuente. Vamos a empezar viendo rápidamente por qué define C++ su propio sistema de E/S.

SISTEMA DE ENTRADA Y SALIDA DEL C++

Si ya ha programado en otros lenguajes, sabrá que C tiene uno de los sistemas de entrada más flexibles, y aun siendo potentes. (De hecho, quizá podría decirse que

el sistema de E/S de C no tiene paralelo entre los lenguajes estructurados del mundo.) Dada la potencia de las funciones de C, quizá se pregunte por qué define C++ sus propias funciones de E/S, que (como se verá) en su mayoría duplican aquellas que ya están contenidas en C. La respuesta es que el sistema de E/S de C no ofrece apoyo alguno para los objetos definidos por el usuario. En C, por ejemplo, si se crea la estructura

 

struct mi_estructura
{

  • int contador;
    char s[80];
    double saldo;
}client;

no hay manera de extender o adaptar el sistema de E/S de C para que la reconozca y pueda llevar a cabo operaciones de E/S directamente con una variable de mi_estructura. No se puede crear un indicador de formato nuevo que esté definido por datos del tipo mi_estructura, para utilizarlo en llamadas a printf(). Por ejemplo, lo que sigue simplemente no va a funcionar:

printf("%mi_estructura", client);

Como prinf() sólo puede reconocer los tipos incorporados, no hay manera de extender sus capacidades con respecto a los nuevos tipos de datos.
Sin embargo, es posible sobrecargar los operadores << y >> utilizando la aproximación de C++ a la E/S para que reconozcan las clases que se crean. Esto incluye tanto a las operaciones de E/S en la consola que se han estado utilizando durante los cuatro últimos capítulos como a la E/S en archivos. (Como se verá, la E/S por consola y en archivos están relacionadas entre sí en C++ tal como lo están en C, y realmente son las dos caras de una misma moneda.)
Aunque no hay ninguna operación de E/S que se pueda hacer con el sistema de E/S de C++ y que no se pueda llevar a cabo utilizando el de C, el hecho de que el sistema de C++ pueda ser informado acerca de los tipos definidos por el usuario incrementa mucho su flexibilidad, y ayuda a evitar los errores. Para ver la forma de hacer esto, considere esta llamada a prinf

printf("%d%s", "Hola", 10);

En esta llamada, la cadena y el entero están invertidos en la lista de argumentos; el %d se hará corresponder con Hola y el %s con el 10. Sin embargo, técnicamente, esto no es un error en C. (Cabría pensar que en alguna situación muy poco corriente podría ser necesario usar una llamada a printf() como la que se muestra. Después de todo, C se ha diseñado para que permitiese hacer todo lo que se puede hacer en ensamblador.) Sin embargo, lo más probable es que esta llamada a printf() sea, realmente, un error. En pocas palabras, C no puede ofrecer una comprobación estricta de tipos cuando se invoca a printf(). Sin embargo, en C++ las operaciones de E/S para todos los tipos incorporados están definidas respecto de << y >>, de tal forma que no hay manera de que pudiese ocurrir una inversión como la que se ha mostrado en llamada a printf(). En lugar de suceder esto, se determina automáticamente la operación correcta en función del tipo de operando. Esta característica se puede extender también a los tipos definidos por el usuario. (Por cierto, sigue siendo posible hacer que se genere en C++ algo parecido a esa extraña llamada a printf(), utilizando una refundición de tipos. Y además, siempre podemos utilizar printf().)

consola y en archivos están relacionadas entre sí en C++ tal como lo están en C, y realmente son las dos caras de una misma moneda.)
Aunque no hay ninguna operación de E/S que se pueda hacer con el sistema de E/S de C++ y que no se pueda llevar a cabo utilizando el de C, el hecho de que el sistema de C++ pueda ser informado acerca de los tipos definidos por el usuario incrementa mucho su flexibilidad, y ayuda a evitar los errores. Para ver la forma de hacer esto, considere esta llamada a prinf

printf("%d%s", "Hola", 10);

En esta llamada, la cadena y el entero están invertidos en la lista de argumentos; el %d se hará corresponder con Hola y el %s con el 10. Sin embargo, técnicamente, esto no es un error en C. (Cabría pensar que en alguna situación muy poco corriente podría ser necesario usar una llamada a printf() como la que se muestra. Después de todo, C se ha diseñado para que permitiese hacer todo lo que se puede hacer en ensamblador.) Sin embargo, lo más probable es que esta llamada a printf() sea, realmente, un error. En pocas palabras, C no puede ofrecer una comprobación estricta de tipos cuando se invoca a printf(). Sin embargo, en C++ las operaciones de E/S para todos los tipos incorporados están definidas respecto de << y >>, de tal forma que no hay manera de que pudiese ocurrir una inversión como la que se ha mostrado en llamada a printf(). En lugar de suceder esto, se determina automáticamente la operación correcta en función del tipo de operando. Esta característica se puede extender también a los tipos definidos por el usuario. (Por cierto, sigue siendo posible hacer que se genere en C++ algo parecido a esa extraña llamada a printf(), utilizando una refundición de tipos. Y además, siempre podemos utilizar printf().)

STREAMAS (FLUJOS) EN C++

Le resultará agradable saber que los sistemas de E/S de C y de C++ tienen una cosa importante en común: los dos operan con streams (flujos), que ya hemos estudiado en capítulos anteriores. El tratamiento dado no se repetirá aquí. (Véanse los detalles en el Capítulo 10.) El hecho de que los streams de C y de C++ sean similares significa que lo que ya se sabe acerca de los streams es completamente aplicable a C++. Además, y lo que quizá es más importante, se pueden mezclar (con unas pocas excepciones) operaciones de E/S en C y en C++ en un mismo programa. Por tanto, se puede empezar a trasladar programas existentes en C en dirección a C++ sin tener que transformar todas las operaciones de E/S nada más empezar.

LOS STREAMS PREDEFINIDOS EN C++

Al igual que C, C++ contiene varios streams predefinidos que se abren automáticamente cuando el programa en C++ empieza a ejecutarse. Se trata de cin, cout, cerr y clog. Como sabe, cin es el stream asociado a la entrada estandard, y cout es el stream asociado a la salida estandard. El stream cerr está asociado a la salida estandard, y también lo está clog. La diferencia entre cerr y clog es que cerr no tiene buffer; por tanto, cualquier salida se saca inmediatamente. Por el contrario, clog tiene un buffer, y sólo se vuelca la salida cuando está lleno un buffer. Por defecto, los streams estándar de C++ están asociados a la consola, pero se puede reorientar hacia otros dispositivos o archivos a través del programa. Además, pueden ser reorientados por el sistema operativo.

CLASES (CLASS) DE STREAMS EN C++

El sistema de E/S de Turbo C++ está definido mediante una jerarquía de clases que están relacionadas con streams. Estas definiciones se encuentran en el archivo de encabezado IOSTREAM.H. La clase de nivel más bajo se llama streambuf y proporciona las operaciones básicas en un stream, pero admite formatos. La clase siguiente dentro de la jerarquía se llama ios. La clase ios proporciona soporte básico para la E/S con formato. También se utiliza para derivar tres clases, que se pueden utilizar para crear streams. Se trata de istream, ostream e iostream. Utilizando istream se puede crear un stream de entrada; utilizando ostream se puede crear un stream de salida y utilizando iostream se puede crear un stream con capacidad tanto de entrada como de salida

CREADCION DE INSERTADORES Y EXTRACTORES PROPIOS

Hasta este momento, cuando un programa necesitaba leer o escribir los datos asociados a una clase, se creaban funciones miembro especiales cuyo solo propósito era el de leer o escribir los datos de esa clase. Aunque no hay nada incorrecto en esta aproximación, C++ ofrece una forma mucho mejor de llevar a cabo operaciones de E/S para clases, sobrecargando los operadores << y >>. En el lenguaje de C++, el operador << se suele denominar operador de inserción porque inserta caracteres en un stream. Analogamente, el operador >> se llama operador de extracción porque extrae caracteres de un stream. Las funciones de operador que sobrecargan los operadores de inserción y de extracción se llaman generalmente insertadores y extractores, respectivamente. Los operadores de inserción y de extracción ya están sobrecargados (en IOSTREAM.H) para que sean capaces de llevar a cabo E/S en streams para cualquiera de los tipos incorporados de C++. Sin embargo, como se indicaba al principio del capítulo, es posible definir estos operadores con respecto a las clases que crea uno mismo. Veremos la forma de hacerlo en esta misma sección.

Creación de insertadores

Una de las características más agradables de C++ es la facilidad con que se construyen insertadores para las clases que crea uno mismo. Como primer ejemplo, vamos a crear un insertador para la clase tres_d, como se indica a continuación:

 

class tres_d
{

  • public:
    int x, y, z; // coordenadas 3-d
    tres_d(int a, int b, int c)

  • x=a; y=b, z=c;
 
};

Para crear una función de insertador para un objeto del tipo tres_d, es preciso definir una operación de inserción con respecto a él. Para hacer esto, es preciso sobrecargar el operador <<, según se indica a continuación:

 

// Mostrar coordenadas X, Y, Z
stream &operator<<(ostream &stream, tres_d obj)
{

  • stream << obj.x << ", ";
    stream << obj.y << ", ";
    stream << obj.z << "\n";
    return stream; // proporciona el stream
  • }
 

Examinemos cuidadosamente esta función, porque muchas de sus características son comunes para todas las funciones de insertador. En primer lugar, obsérvese que se declara de tal modo que proporciona una referencia de un objeto del tipo ostream. Esto es necesario para permitir que se pongan uno junto a otro varios insertadores de este tipo. A continuación, la función tiene dos parámetros. El primero es la referencia del stream, que aparece al lado izquierdo del operador <<. El segundo parámetro es el objeto que aparece en el lado derecho. Dentro de la función, se sacan los tres valores que contiene un objeto del tipo tres_d, y se proporciona el stream. Véase un pequeño programa que demuestra el insertador:

#include <iostream.h>
class tres_d
{

  • public: int x, y, z; // coordenadas 3-d
    tres_d(int a, int b, int c)

  • x=a; y=b, z=c;
 
};
// Muestra coordenadas X, Y, Z el insertador de tres_d
ostream &operator<<(ostream &stream, tres_d obj)
{

  • stream << obj.x << ", ";
    stream << obj.y << ", ";
    stream << obj.z << "\n";
    return stream; // proporciona el stream
  • }
 
main(void
{

  • tres_d a(1, 2, 3), b(3, 4, 5), c(5, 6, 7);
    cout << a<< b << c;
    return O;
  • }
 

Si se elimina el código que pertenece exclusivamente a la clase tres_d, lo que queda es el esqueleto de una función de insertador, según se muestra a continuación:

 

ostream &operator<<(ostream &stream, tipo_clase obj)
{

  • // aquí va el código específico del tipo
    return stream; // proporciona el stream
  • }
 

Lo que hace una función de insertador es, dentro de unos límites muy amplios, cosa nuestra nada más. Sólo hay que asegurarse de que se proporciona el stream. Quizá se pregunte por qué no se ha codificado la función en la forma que se indica a continuación:

// Versión limitada no utilizarla.
ostream &operator<<(ostream &stream, tres_d obj)
{

  • cout ~< obj.x << ", ";
    cout << obj.y << ", ";
    cout << obj.z << "\n";
    return stream; // proporciona el stream
  • }
 

En esta versión, el stream cout se ha mencionado específicamente en la función. Recuerde sin embargo que el operador << se puede aplicar a cualquier stream. Por tanto, es preciso utilizar el stream que se haya pasado a la función para que ésta funcione correctamente en todos los casos.
En el programa anterior, la función de insertador sobrecargada no es un miembro de tres_d. De hecho, ni la función del insertador ni la del extractor pueden ser funciones miembro de una clase. La razón de esto es que cuando una función operator es miembro de una clase, el operando de la izquierda (que se pasa implícitamente mediante el operador this) se supone que es un objeto de la clase que generó la llamada a la función operator. No hay manera de cambiar esto. Sin embargo, cuan do se sobrecarga un insertador, el argumento de la izquierda es un stream y el argumento de la derecha es un objeto de esa clase. Por tanto, los insertadores sobrecargados tienen que ser funciones que no sean miembros.
El hecho de que los insertadores no deban ser miembros de la clase sobre la cual tiene que operar plantea una cuestión muy grave: ¿Cómo puede acceder a los elementos privados de la clase un insertador sobrecargado? En el programa anterior, las variables x, y y z se hacían public para que el insertador pudiese acceder a ellas. Pero la ocultación de datos es una parte importante de la POO, y obligar a todos los datos a que sean public es una incongruencia grave. Sin embargo, hay una solución: un insertador puede ser un friend de la clase. Un friend tiene acceso a los datos privados de la clase para la cual esté definido.
Para ver un ejemplo de esto, la clase tres_d y el programa se han rehecho, declarando como friend el insertador sobrecargado.

 

#include <iostream.h>
class tres_d
{

  • int x, y, z; // coordenadas 3-d ahora son privadas
    public:
    tres_d(int a, int b, int c)
    {

  • x=a; y=b, z=c;
    }

friend ostream &operator<<(ostream &stream, tres_d obj);
};
// Muestra coordenadas X, Y, Z en el insertador de tres_d
ostream &operator<<(ostream &stream, tres_d obj)
{

  • stream << obj.x << ", ";
    stream << obj.y << ", ";
    stream << obj.z << "\n";
    return stream; // proporciona el stream
  • }
 
main(void)
{

  • tres_d a(1, 2, 3), b(3, 4, S), c(5, 6, 7);
    cout << a << b << c
    return O;
  • }
 

Observe que las variables x, y, z son ahora private en tres_d, pero sigue siendo posible acceder a ellas desde el insertador. Al hacer que los insertadores (y extractores) sean friends de las clases para las cuales están definidos, se mantiene el principio de ocultación de información que propugna la POO.