5: Iostreams

Tabla de contenidos

5.1. ¿Por que iostream?
5.2. Iostreams al rescate
5.3. Manejo errores de stream
5.4. Iostreams de fichero
5.5. Almacenamiento de iostream
5.6. Buscar en iostreams
5.7. Iostreams de string
5.8. Formateo de stream de salida
5.9. Manipuladores
5.10.
5.11.
5.12.
5.13.

Puedes hacer mucho más con el problema general de E/S que simplemente coger el E/S estándar y convertirlo en una clase.

¿No seria genial si pudiera hacer que todos los 'receptáculos' -E/S estándar, ficheros, e incluso boques de memoria- parecieran iguales de manera que solo tuviera que recordar una interficie? Esta es la idea que hay detrás de los iostreams. Son mucho más sencillos, seguros, y a veces incluso más eficientes que el conjunto de funciones de la libreria estándar de C stdio.

Las clases de iostream son generalmente la primera parte de la libreria de C++ que los nuevos programadores de C++ parender a usar. En este capítulo se discute sobre las mejoras que representan los iostream sobre las funciones de stdio de C y explora el comprotamiento de los ficheros y streams de strings además de los streams de consola.

5.1. ¿Por que iostream?

Se debe estar preguntando que hay de malo en la buena y vieja librería de C. ¿Por que no 'incrustar' la libreria de C en una clase y ya está? A veces esta solución es totalmente válida. Por ejemplo, suponga que quiere estar seguro que un fichero representado por un puntero de stdio FILE siempre es abierto de forma segura y cerrado correctamente sin tener que confiar en que el usuario se acuerde de llamar a la función close(). El siguiente programa es este intento:

//: C04:FileClass.h
// stdio files wrapped.
#ifndef FILECLASS_H
#define FILECLASS_H
#include <cstdio>
#include <stdexcept>

class FileClass {
  std::FILE* f;
public:
  struct FileClassError : std::runtime_error {
    FileClassError(const char* msg)
    : std::runtime_error(msg) {}
  };
  FileClass(const char* fname, const char* mode = "r");
  ~FileClass();
  std::FILE* fp();
};
#endif // FILECLASS_H ///:~

Listado 5.1. C04/FileClass.h


Cuando trabaja con ficheros E/S en C, usted trabaja con punteros desnudos a una struct de FILE, pero esta clase envuelve los punteros y garantiza que es correctamente inicializada y destruida usando el constructor y el destructor. El segundo parámetro del constructor es el modo del fichero, que por defecto es 'r' para 'leer'

Para pedir el valor del puntero para usarlo en las funciones de fichero de E/S, use la función de acceso fp(). Aquí están las definiciones de las funciones miembro:

//: C04:FileClass.cpp {O}
// FileClass Implementation.
#include "FileClass.h"
#include <cstdlib>
#include <cstdio>
using namespace std;

FileClass::FileClass(const char* fname, const char* mode) {
  if((f = fopen(fname, mode)) == 0)
    throw FileClassError("Error opening file");
}

FileClass::~FileClass() { fclose(f); }

FILE* FileClass::fp() { return f; } ///:~

Listado 5.2. C04/FileClass.cpp


El constructor llama a fopen(), tal como se haría normalmente, pero además se asegura que el resultado no es cero, que indica un error al abrir el fichero. Si el fichero no se abre correctamente, se lanza una excepción.

El destructor cierra el fichero, y la función de acceso fp() retorna f. Este es un ejemplo de uso de FileClass:

//: C04:FileClassTest.cpp
//{L} FileClass
#include <cstdlib>
#include <iostream>
#include "FileClass.h"
using namespace std;

int main() {
  try {
    FileClass f("FileClassTest.cpp");
    const int BSIZE = 100;
    char buf[BSIZE];
    while(fgets(buf, BSIZE, f.fp()))
      fputs(buf, stdout);
  } catch(FileClass::FileClassError& e) {
    cout << e.what() << endl;
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
} // File automatically closed by destructor
///:~

Listado 5.3. C04/FileClassTest.cpp


Se crea el objeto FileClass y se usa en llamadas a funciones E/S de fichero normal de C, llamando a fp(). Cuando haya acabado con ella, simplemente olvídese; el fichero será cerrado por el destructor al final del ámbito de la variable.

Incluso teniendo en cuenta que FILE es un puntero privado, no es particularmente seguro porque fp() lo recupera. Ya que el único efecto que parece estar garantizado es la inicialización y la liberación, ¿por que no hacerlo público o usar una struct en su lugar? Nótese que mientras se puede obtener una copia de f usando fp(), no se puede asignar a f -que está completamente bajo el control de la clase. Después de capturar el puntero retornado por fp(), el programador cliente todavía puede asignar a la estructura elementos o incluso cerrarlo, con lo que la seguridad esta en la garantía de un puntero a FILE válido mas que en el correcto contenido de la estructura.

Si quiere completa seguridad, tiene que evitar que el usuario acceda directamente al puntero FILE. Cada una de las versiones de las funciones normales de E/S a ficheros deben ser mostradas como miembros de clase para que todo lo que se pueda hacer desde el acercamiento de C esté disponible en la clase de C++.

//: C04:Fullwrap.h
// Completely hidden file IO.
#ifndef FULLWRAP_H
#define FULLWRAP_H
#include <cstddef>
#include <cstdio>
#undef getc
#undef putc
#undef ungetc
using std::size_t;
using std::fpos_t;

class File {
  std::FILE* f;
  std::FILE* F(); // Produces checked pointer to f
public:
  File(); // Create object but don't open file
  File(const char* path, const char* mode = "r");
  ~File();
  int open(const char* path, const char* mode = "r");
  int reopen(const char* path, const char* mode);
  int getc();
  int ungetc(int c);
  int putc(int c);
  int puts(const char* s);
  char* gets(char* s, int n);
  int printf(const char* format, ...);
  size_t read(void* ptr, size_t size, size_t n);
  size_t write(const void* ptr, size_t size, size_t n);
  int eof();
  int close();
  int flush();
  int seek(long offset, int whence);
  int getpos(fpos_t* pos);
  int setpos(const fpos_t* pos);
  long tell();
  void rewind();
  void setbuf(char* buf);
  int setvbuf(char* buf, int type, size_t sz);
  int error();
  void clearErr();
};
#endif // FULLWRAP_H ///:~

Listado 5.4. C04/Fullwrap.h


Esta clase contiene casi todas las funciones de E/S de fichero de <cstdio>. (vfprintf() no esta; se implementa en la función miembro printf() )

El fichero tiene el mismo constructor que en el ejemplo anterior, y también tiene un constructor por defecto. El constructor por defecto es importante si se crea un array de objetos File o se usa un objeto File como miembro de otra clase donde la inicialización no se realiza en el contructor, sino cierto tiempo después de que el objeto envolvente se cree.

El constructor por defecto pone a cero el puntero a FILE privado f. Pero ahora , antes de cualquier referencia a f, el valor debe ser comprobado para asegurarse que no es cero. Esto se consigue con F(), que es privado porque está pensado para ser usado solamente por otras funciones miembro. (No queremos dar acceso directo a usuarios a la estructura de FILE subyacente en esta clase).

Este acercamiento no es terrible en ningún sentido. Es bastante funcional, y se puede imaginar haciendo clases similares para la E/S estándar (consola) y para los formateos en el core (leer/escribir un trozo de la memoria en vez de un fichero o la consola).

Este bloque de código es el interprete en tiempo de ejecución usado para las listas variables de argumentos. Este es el código que analiza el formato de su cadena en tiempo de ejecución y recoge e interpreta argumentos desde una lista variable de argumentos. Es un problema por cuatro razones:

  1. Incluso si solo se usa una fracción de la funcionalidad del interprete, se carga todo en el ejecutable. Luego si quiere usar un printf("%c", 'x'); , usted tendrá todo el paquete, incluido las partes que imprimen números en coma flotante y cadenas. No hay una opción estándar para reducir el la cantidad de espacio usado por el programa.

  2. Como la interpretación pasa en tiempo de ejecución, no se puede evitar un empeoramiento del rendimiento. Esto es frustrante por que toda la información está allí, en el formato de la cadena, en tiempo de compilación, pero no se evalua hasta la ejecución. Por otro lado, si se pudieran analizar los argumentos en el formateo de la cadena durante la compilación, se podrían hacer llamadas directas a funciones que tuvieran el potencial de ser mucho más rápidas que un interprete en tiempo de ejecución (aunque la familia de funciones de printf() acostumbran a estar bastante bien optimizadas).

  3. Como el formateo de la cadena no se evalua hasta la ejecución, no se hace una comprobación de errores al compilar. Probalblemente está familiarizado con este problema si ha intentado buscar errores que provienen del uso de un número o tipo de argumentos incorrecto en una sentencia printf(). C++ ayuda mucho a encontrar rápidamente errores durante la compilación y hacerle la vida más fácil. Parece una tonteria desechar la seguridad en los tipos de datos para la libreria de E/S, especialmente cuando usamos intensivamente las E/S.

  4. Para C++, el más crucial de los problemas es que la familia de funciones de printf() no es particularmente extensible. Esta realmente diseñada para manejar solo los tipos básicos de datos en C (char, int, float, double, wchar_t, char*, wchar_t*, y void*) y sus variaciones. Debe estar pensando que cada vez que añade una nueva clase, puede añadir funciones sobrecargadas printf() y scanf() (y sus variaciones para ficheros y strings), pero recuerde: las funciones sobrecargadas deben tener diferentes tipos de listas de argumentos, y la familia de funciones de printf() esconde esa información en la cadena formateada y su lista variable de argumentos. Para un lenguage como C++, cuya virtud es que se pueden añadir fácilmente nuevos tipos de datos, esta es una restricción inaceptable.