Tabla de contenidos
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.
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:
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.
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).
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.
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.