5.3.1 E/S en la Librería Estándar
"The C++ I/O library is quite complicated, but the general ideas behind its architecture are simple". Matthew H. Austern en "The Standard Librarian: Streambufs and Streambuf Iterators". Dr.Dobb's Journal.
§1 Introducción
Inicialmente las utilidades C++ de E/S derivan de las utilizadas en C (en realidad eran las mismas). En C estas utilidades están localizadas en el fichero <stdio.h>, que contiene casi un tercio del total de funciones de la Librería C Estándar, que precisamente debe su nombre a su contenido ("Standar Input Output"). Las E/S C se mantienen en C++ por compatibilidad, aunque recientemente el fichero el fichero de cabecera ha pasado a denominarse <cstdio>.
Cuando C++ pasó a ser algo más que un preprocesador, comenzó a presentar su propio tratamiento de las entradas/salidas. Como no podía ser menos, el enfoque C++ del asunto es encapsular las funcionalidades de E/S en una serie de clases, conocidas con el nombre genérico de iostreams y organizadas en una jerarquía. istreams si son flujos de entrada; ostreams si son de salida, e iostreams si son bidireccionales.
Nota: más concretamente, clases genéricas (plantillas), en las que el tipo de carácter es uno de los argumentos de la plantilla y que se agrupan en un conjunto de cabeceras propio y distinto del antiguo. Este diseño permite que puedan manejarse flujos de caracteres sencillos (tipo char 2.2.1a), o de caracteres en cualquier otro lenguaje para el que un carácter venga representado por un wchar_t ( 2.2.1a1). Incluso con juegos de objetos, similares a caracteres, definidos por el usuario [4].
En concreto, las clases C++ encargadas de manejar E/S a fichero (filestreams) utilizan la técnica denominada RAII ( 4.1.5a), de forma que la creación de un flujo, que se concreta en instanciar un objeto, puede llevar aparejada la apertura del fichero correspondiente. Una vez realizadas las operaciones E/S, aplicando los métodos correspondientes sobre el objeto, no hay que preocuparse de que el fichero quede abierto, porque es cerrado automáticamente por el destructor.
§2 Ficheros de cabecera
La colección de plantillas antes mencionada, fue oficializada en la última revisión del Estándar con el nombre de Librería de Entrada/Salida ("Input/output Library" o "IOStream Library"). Contiene los componentes necesarios para las E/S de los programas C++, y se concreta en una serie de ficheros de cabecera agrupados según la funcionalidad de los algoritmos. Son los siguientes:
Funcionalidad | Fichero/s |
Declaraciones adelantadas. Comprende la declaración de 20 clases genéricas (plantillas) y 28 typedefs. | <iosfwd> |
Descripción de clases estándar para manejo de E/S. | <iostream> |
Definición de las superclases que actúan como raíz en la librería anterior (5.3.2a). | <ios> |
Almacenamiento de flujos ("Stream buffers"). Define los tipos que controlan la capa de transporte ( 5.3.2f). | <streambuf> |
Formateo y manipulación de flujos. El fichero <istream> define entidades para controlar la entrada desde un flujo ( 5.3.2c). <ostream> define entidades para controlar la salida ( 5.3.2d). <iomanip> define los manipuladores estándar ( 5.3.3c1). |
<istream> <ostream> <iomanip> |
Flujos de cadenas ("String streams"). La cabecera <sstream> define cuatro clases genéricas y seis tipos que asocian flujos con objetos de la clase basic_string. |
<sstream> <cstdlib> |
Flujos de fichero ("File streams") La cabecera <fstream> define cuatro clases genéricas y seis tipos que asocian flujos con ficheros, facilitando la lectura y escritura de ficheros. |
<fstream> <cstdio> <cwchar> |
§3 Resumen de utilidades
Un vistazo al contenido del fichero <iosfwd> nos puede dar una visión general de las clases disponibles en esta librería, que comentaremos a continuación ( 5.3.2), pero antes de entrar en detalles ofreceremos una visión sinóptica.
§3.1 E/S en general
Existen tres clases para flujos de E/S genéricos:
-
basic_istream ( 5.3.2) para flujos de entrada
-
basic_ostream ( 5.3.2) para flujos de salida
-
basic_iostream ( 5.3.2) para flujos bidireccionales
§3.2 E/S a fichero
Existen tres clases para flujos de E/S a ficheros de disco y similares ("Named files") ficheros o dispositivos que puedan ser identificados por un nombre, como es el caso de los ficheros de disco.
Figura 4. |
-
basic_ifstream ( 5.3.2) para flujos de entrada.
-
basic_ofstream ( 5.3.2) para flujos de salida.
-
basic_fstream ( 5.3.2) para flujos bidireccionales.
El esquema de la arquitectura de uno de estos algoritmos se muestra en la figura 4. Estas clases disponen de funciones para abrir y cerrar ficheros, similares a las funciones fopen() y fclose() de la librería tradicional C, aunque son más seguras, dado que utilizan la técnica RIAA. También disponen de constructores que permiten crear un flujo y asociarlo a un fichero.
Ejemplo: escribir el contenido de una variable int en un fichero y recuperarla posteriormente, podría ser algo así:
#include <fstream>
using namespace std;
void writeint (int x) {
ofstream file ("value.dat", ios::binary|ios::trunc);
file.write (reinterpret_cast<char*>(&x), sizeof(x));
file.close(); // no es realmente necesario
}
int readint () {
ifstream file ("value.dat");
int x = 0;
file.read (reinterpret_cast<char *>(&x), sizeof(x));
return x;
}
Las diferencias principales entre los flujos estándar predefinidos stdIO ( 5.3) y los flujos de fichero (filestreams) son:
-
Los flujos de fichero deben ser conectados a un fichero antes de su uso. Los flujos estándar están abiertos desde el inicio del programa.
-
Los flujos estándar pueden ser utilizados directamente, incluso en constructores de objetos estáticos (que son ejecutados antes que se inicie la función main). Los flujos de fichero deben ser creados antes de su uso.
-
Los flujos de fichero pueden ser reposicionados en cualquier posición de un fichero, lo que no tiene sentido con los flujos predefinidos ya que estos están conectados generalmente con el terminal E/S (por defecto suele ser la pantalla) y estos dispositivos no soportan petición de posición.
Para facilitar las cosas, existen typedefs estándar: ifstream, ofstream y fstream para referirse a flujos de caracteres normales (char) y wifstream, wofstream y wfstream para flujos de fichero de caracteres anchos (wchar_t 5.3.2).
Para controlar el tráfico de caracteres de/hacia el fichero externo ("Buffering"), utilizan un tipo de buffer denominado buffer de fichero ("File buffer"), abreviadamente filebuf, que es una instancia de la clase basic_filebuf ( 5.3.2) derivada de la superclase basic_streambuf.
§3.3 E/S a memoria
Existen tres clases para flujos de E/S a memoria
Figura 5. |
-
basic_istringstream ( 5.3.2) Para flujos de entrada.
-
basic_ostringstream ( 5.3.2) Para flujos de salida.
-
basic_stringstream ( 5.3.2) Para flujos bidireccionales.
Estas clases disponen de funciones para obtener y establecer una cadena de caracteres que será utilizada como un buffer. Internamente se utiliza un buffer de flujo especial, que en este caso se denomina buffer de cadena ("String buffer"), que es una instancia de la clase basic_stringbuf ( 5.3.2). En este caso particular, el buffer de flujo y el dispositivo externo son la misma cosa. La figura 5 es una representación del funcionamiento de estos algoritmos.
§3.4 Modalidades de E/S formateadas y sin formato
En general, las utilidades STL de E/S se presentan en dos grupos que estudiaremos separadamente: utilidades para operaciones (de E/S) formateadas y utilidades para operaciones sin formato ("planas"). Para entender sus diferencias, consideremos dos sucesiones de octetos en el interior de la máquina (o en un fichero externo). El primero S1, es un grupo de 4 octetos que representan entidades char, cuyo valor podemos representar por la cadena "AEIO". El segundo S2, es igualmente un grupo de 4 octetos que representan una entidad float, que sabemos ocupa 32 bits ( 2.2.4) y cuyo valor decimal es 12.5. Sabemos que la representaciones interna de ambas cadenas es como sigue:
S1:
01000001 01000101 01001001 01001111
S2:
01000001 01001000 00000000 00000000
En estas condiciones, si queremos organizar un flujo con S1 y S2 hacie un dispositivo externo.
Por ejemplo, la pantalla o una impresora, las rutinas de salida con
formato escribirían en el dispositivo el mensaje esperado: AEIO12.5
.
En cambio, las rutinas de salida sin formato, escribirían en el dispositivo AEIO HA
. La razón es que,
en este caso, ambos dispositivos de salida están orientados a la representación de grafos (caracteres) del
idioma correspondiente (aquí suponemos inglés americano). Ambas representaciones coinciden en su primera parte, porque las
sucesiones de caracteres ASCII de S1 en las rutinas formateadas y sin formato son idénticas (no necesitan
transformación). En cambio, al llegar a S2 empiezan las diferencias.
La rutina con formato, que conoce que S2 es un float, que la salida es para "terricolas" y que ella misma es bastante lista, transforma el "valor" del los 32 bits, en su representación numérica según el sistema arábigo. En este caso, una sucesión de cuatro octetos que corresponde a los caracteres ASCII; 1, 2, . y 5 (estos son los valores entregado realmente al dispositivo de salida). En cambio, la rutina sin formato los entrega al dispositivo "tal cual" los encuentra. En este caso dos nulos y los octetos correspondientes a los caracteres H y A.
Nota: Para terminar de entenderlo (si no lo tiene claro aún), considere que la traducción de octetos (sucesiones de bits) a grafos del alfabeto latino, la realiza el adaptador gráfico de la pantalla (o impresora). La explicación de la representación interna de S2 puede encontrarla en "Formas de representación binaria de las magnitudes numéricas" ( 2.2.4a). Finalmente, para justificar porqué la representación de S2 es HA y no AH como podría parecer, considere que hemos efectuado el experimento en un sistema con procesador Intel. Como este hardware es Little-endian en lo que respecta a la representación interna ( 2.2.6a), los cuatro octetos del float están almacenados en orden inverso y, como hemos señalado, las rutinas de salida plana entregan los bytes "tal cual" están almacenados (ver el código que permite la verificación en 5.3.2a).
Tendremos ocasión de ver ( 5.3.2b) que las utilidades para E/S formateadas, son en realidad versiones sobrecargadas de los operadores >> << [1] en los iostreams. Las de entrada (>>) son denominadas extractores ("Extractor"); las de salida (<<) insertores ("Inserter"). Por su parte, las utilidades para E/S sin formato están constituidas por una colección de métodos de nombres diversos, de los que existe un amplio surtido, aunque todos comparten la característica de trasegar los bytes sin realizar transformaciones en su contenido (en realidad consideran que el flujo solo contiene "caracteres"). Por ejemplo, las operaciones de salidas formateada y plana de un flujo, myStream pueden presentar el siguiente aspecto:
myStream.write(texto, 50); // salida sin formato
myStream >> texto; // salida formateada
myStream.operator>>(texto); // sintaxis equivalente a la anterior
Es importante destacar que, estas expresiones solo son posibles, si en la clase de myStream, existen versiones de los métodos write() y operator>>(), que acepten un tipo texto como argumento.
§4 Ventajas de la librería C++ E/S
Aunque las utilidades de la librería C clásica pueden seguir utilizándose, salvo algunos casos concretos [3], es preferible utilizar las posibilidades de la nueva librería C++ (iostreams). En especial porque presentan un control de errores más robusto y pueden ser extendidas para servir a tipos definidos por el usuario. Sin embargo, las posibilidades y aplicaciones de las iostreams no acaban en la simple sustitución de las utilidades tradicionales. Se permiten también usos más avanzados como comunicación entre objetos y persistencia.
Ejemplo: en el siguiente código utilizamos las formas clásica y moderna para una salida:
int i = 25;
char name[50] = "Alonso Quijano";
fprintf(stdout, "%d %s", i, name); // C clásico
cout << i << ' ' << name << '\n'; // C++
Las dos sentencias pueden escribir correctamente la misma salida:
25 Alonso Quijano
pero si en la forma clásica se altera por error el orden de los argumentos. Por ejemplo:
fprintf(stdout, "%d %s", name, i);
El error solo puede ser detectado en "runtime", a veces con resultados catastróficos. En cambio, un error análogo en la forma C++ no produce ningún error (aunque desde luego, la salida no tiene el formato esperado), ya que existen versiones sobrecargadas del método operator<<() para los tipos básicos. Por ejemplo, la forma cout << i invoca la versión operator<<(int), mientras que la forma cout << name invoca la versión operator<<(const char*).
Otra ventaja de las iostreams es que pueden ser utilizadas con tipos definidos por el usuario. Como ejemplo consideremos un tipo Pair cuya definición es la siguiente:
struct Pair { int x; string y; }
Para utilizar las facilidades de la Librería Estándar E/S con nuestro nuevo tipo, solo es necesario sobrecargar la función-operador operator<<() para los miembros de nuestra clase. Lo que puede hacerse añadiendo la definición adecuada en la forma:
basic_ostream<char>& operator<<(basic_ostream<char>& o, const Pair& p) { return o << p.x << ' ' << p.y; }
Con esta nueva definición es posible utilizar funciones iostreams en la forma:
Pair p1(56, "Mayo"); cout << p1 << endl;
Lo anterior en formato ejecutable:
#include <iostream>
#include <string>
using namespace std;
struct Pair {
int x;
string y;
Pair (char n=0, string s="Vacio");
};
Pair::Pair(char n, string s) {
x = n;
y = s;
}
basic_ostream<char>& operator<<(basic_ostream<char>& o, const Pair& p) {
return o << p.x << ' ' << p.y;
}
void main() { // ==============
Pair p1(56, "Mayo");
cout << p1 << endl;
Pair p2;
cout << p2 << endl;
}
Salida:
56 Mayo
0 Vacio
§5 Criterios de uso de las rutinas Estándar de E/S
Además de las ventajas generales presentadas en el epígrafe anterior, existen muchas situaciones en las que las rutinas de E/S ofrecidas por la LE pueden ser de utilidad:
§5.1 Entradas/Salidas de fichero.
Aunque en el pasado reciente las interfaces alfanuméricas de usuario eran generalmente construidas utilizando lecturas/escrituras a los dispositivos estándar de entrada/salida. En la actualidad, prácticamente la totalidad de las aplicaciones utilizan interfaces gráficas estándar (proporcionadas por el SO).
Aunque este tipo de operaciones ha perdido importancia, los iostreams pueden ser utilizados aún para entradas y salidas, no solo a los dispositivos estándar, sino a cualquier otro tipo de dispositivo externo de cualquier clase que acepte la abstracción de fichero. Basándose en este principio se han construido multitud de aplicaciones y librerías para comunicaciones en red.
§5.2 E/S a memoria
Este tipo de utilidades ("In-Memory I/O") pueden realizar fácilmente operaciones de formateo y conversión de código. Recordemos que incluso en los entornos gráficos es preciso formatear el texto que debe mostrarse. Los iostreams estándar proporcionan internacionalización en memoria, lo que puede resultar de gran ayuda en ciertas operaciones de formateos de texto. Por ejemplo, los formatos numéricos dependen de convenciones culturales y la capa de formato puede realizar fácilmente este tipo de adaptaciones culturales.
§5.3 Proceso de texto internacionalizado
Puesto que las rutinas estándar E/S utilizan locales para manejar las particularidades de los flujos y los locales son extensibles, cualquier tipo de particularidad puede ser manejada fácilmente por una rutina estándar de control de flujo.
Por defecto los iostreams utilizan solamente las particularidades ("facets") relativas a formatos numéricos y de conversión de código, aunque fecha, hora y moneda son también manejadas por la LE. Cualquier otra dependencia cultural puede ser encapsulada en un "facet" específico y ser puesta a disposición de un flujo. Es posible por tanto personalizar la internacionalización de los flujos de E/S para hacer que se adapten a nuestras necesidades.
§5.4 E/S Binarias
Las E/S binarias C tradicionales sufren de ciertas limitaciones. La principal es la falta de capacidad de conversión. Por ejemplo, si se inserta un double en un flujo de salida, no podemos saber que formato se utilizará para representarlo en el dispositivo externo. No existe un forma portable de insertarlo como binario.
Los iostreams estándar son mucho más flexibles. La conversión de código realizada en la transferencia de datos internos a un dispositivo externo puede ser personalizada. Puesto que la capa de transporte delega la conversión de código a un especificador de localismos (facet), proporcionando un facet adecuado a un flujo binario es posible insertar un double en un flujo binario que resulte portable. Aunque tal implementación no resulte trivial y la LE no proporcione ningún facet preconstruido a tal efecto. Una posible alternativa sería implementar una capa completa de buffer de flujo que pueda manejar E/S binarias.
§5.5 Extender los iostreams
En cierta forma los iostreams pueden ser considerados como un marco de trabajo que puede ser extendido y personalizado. Pueden añadirse operadores de entrada y de salida para tipos definidos por el usuario, o crear nuestros propios elementos de formateo, los manipuladores. Pueden especializarse flujos completos, generalmente en conjunción con buffers de flujo especializados. Pueden suministrarse locales diferentes para representar diferentes convenciones culturales, o para contener peculiaridades con un fin particular. También pueden instanciarse clases de flujo para nuevos tipos de caracteres distintos de char o wchar_t.
[1] Sobrecarga realizada mediante los métodos operator>>() y operator<<() de los que existen versiones para todos los tipos básicos definidos en el lenguaje.
[3] Por ejemplo las funciones de la librería clásica printf() y scanf() son mucho más eficaces y concretas para ciertas tareas que las correspondientes versiones genéricas C++.
[4] Esta flexibilidad también tiene un precio. Cualquier juego de caracteres definido por el usuario debe satisfacer unas exigencias muy estrictas. El nuevo tipo debe comportarse como un carácter en todas circunstancias; además deben definirse varias clases para soportar su funcionamiento en el ambiente de las plantillas Estándar de E/S.