5.5.2a Problemas y precauciones con Directorios y Ficheros
§1 Sinopsis
En lo que se refiere al manejo de ficheros y directorios en la librería clásica C++ (heredada de C), habría que señalar dos dificultades principales: (1) La escasez de funciones proporcionadas por la Librería Estándar. Por ejemplo, es imposible calcular el tamaño y atributos de un fichero utilizando exclusivamente las funciones estándar. (2) La escasa o nula información que proporcionan en caso de error. Por ejemplo, la función fwrite, devuelve el número de ítems leídos, y en caso de error un número menor, pero sin indicación del tipo de error o del número real de bytes leídos. Por su parte rewind no devuelve señal alguna del resultado de la operación [1].
Nota: por supuesto, las operaciones de red pertenecen a una galaxia ajena al lenguaje. En caso de necesitarlas hay que acudir a llamadas al Sistema o a librerías de terceros (esperemos que en un futuro no lejano sean incorporadas al Estándar algunas utilidades con este propósito).
Las funciones complementarias proporcionadas por las plataformas de desarrollo
son en consecuencia muy abundantes, pero con el inconveniente de su falta de
portabilidad. A título orientativo, la selección de funciones de la
página anterior, que recoge prácticamente todas las utilidades relacionadas con el manejo de ficheros y directorios de los compiladores
C++ Borland y MS, contiene 86 funciones, de las cuales solo 25 pertenecen al
C++ Estándar.
§2 Lecturas/escrituras binarias y de texto
Como comentábamos en la página anterior, las operaciones de lectura/escrituras relacionadas con ficheros abiertos en modo texto, implican ciertas conversiones de código; de forma que los ficheros origen y destino no son iguales. Por ejemplo, la imagen guardada en el disco y la obtenida en memoria después de una operación de lectura. Además la respuesta proporcionada por la función fread no tiene en cuenta la forma de lectura utilizada, lo que puede dar lugar a situaciones desconcertantes.
Ejemplo:
Supongamos que necesitamos leer el contenido de un fichero fichero.nnn en memoria. El primer problema es calcular el tamaño a leer, para lo que debemos utilizar una función no estándar (observe las secuencias de escape \\ utilizadas en la definición del fichero).
Fase-1 Apertura:
FILE* handle = fopen("D:\\path-name\\fichero.nnn", "r+b");
if (handle == NULL)
std::cout << "Error de apertura!!" << std::endl;
long size = filelength(_fileno(handle));
El uso de funciones estándar para este propósito obliga a un método indirecto utilizando dos funciones:
int res = fseek(handle, 0, SEEK_END);
// ir al final del fichero
long int pos = ftell(handle);
// ver que posición es esta
cout << "Longitud del fichero: " << pos << " Bytes" <<
endl;
Fase-2 Lectura: a continuación puede procederse a la lectura
propiamente dicha.
char buffer[MAXSIZE]; // buffer de lectura
size_t result = fread(buffer, 1, size, handle);
if (result != size)
std::cout << "Error de lectura!!" << std::endl;
else
std::cout << "Ok. Leidos " << size << " bytes" << std::endl;
Observe
que MAXSIZE es una constante cuyo valor debe ser suficiente para
albergar el máximo tamaño de fichero previsto. Recuerde que el tamaño
de la matriz debe ser un valor resuelto en tiempo de compilación (
4.3.1). Por consiguiente no
puede utilizarse una expresión del tipo
char buffer[size]; // Error!!
Recuerde también que si se realizan otras operaciones con el fichero después de la 1ª fase, es necesario volver a reposicionar el fp (file-pointer) al comienzo antes de proceder a la 2ª:
fseek(handle, 0L, SEEK_SET);
Suponiendo que el fichero ha sido felizmente abierto, el resultado es
satisfactorio, por lo que decidimos utilizar la rutina de forma más o menos "oficial" en nuestras operaciones de lectura. Sin embargo,
días más tarde, la rutina muestra errores persistentes sin que sea posible
determinar la causa. Después de unas horas de pruebas infructuosas
(probando todo lo demás, ya que estamos seguros que la rutina SI funciona),
descubrimos que es un fichero abierto en modo
texto. En este caso,
aunque el tamaño a leer está correctamente indicado y el fichero
correctamente abierto, descubrimos que el conteo de caracteres leídos se
realiza después de la conversión, con lo
que si el fichero tiene más de una línea de texto, es decir, si tiene al
menos un CR-LF, el tamaño leído no
coincide con el valor size, y la
rutina devuelve error. Para mayor consternación, a pesar del error, el
buffer contiene efectivamente el texto leído, pero cerca del final el
contenido es incorrecto. Este trozo tanto mayor cuanto mayor sea el
fichero leído (más líneas contenga [2] ).
En estos casos (lecturas en modo texto), la única forma de leer correctamente el contenido del fichero es utilizar algo como:
long fsize = 0;
char cread;
do {
*(buffer+fsize) = cread = fgetc(handle);
++fsize;
} while ( !feof(handle) && cread != EOF );
Aunque el sistema realiza un "buffering" automático, y en realidad el bucle anterior no significa un acceso a disco por cada carácter leído, es evidente que el procedimiento estándar para leer un simple fichero es de lo más rudimentario.
Como ejemplo de lo anteriormente expuesto, se incluye un código que permite leer correctamente el contenido de un fichero de texto:
#include <iostream>
#include <stdio.h> // para operaciones con ficheros
#define MAXSIZE 64000 // un valor suficientemente
grande
char buffer[MAXSIZE]; // Buffer de
lectura
FILE* handle = fopen("D:\\path-name\\fichero.sss", "rt");
if (handle == NULL) {
std::cout << "Error de apertura fichero!!" <<
std::endl;
return;
}
int res = fseek(handle, 0, SEEK_SET); // ir al inicio del fichero
long fsize = 0;
char cread;
do {
*(buffer + fsize) = cread = fgetc(handle);
++fsize;
} while ( !feof(handle) && cread != EOF );
buffer[fsize] = '\0';
--fsize;
// eliminar incremento del
último bucle
std::cout << "El fichero es de " << fsize << " bytes" <<
std::endl;
std::cout << buffer << std::end; // mostrar
contenido
fclose(handle); // No olvide cerrar el fichero!!
...
§3 Escritura
Igual que ocurre con las operaciones de lectura, las escrituras sucesivas se realizan secuencialmente. Cada nueva escritura se realiza a continuación de la anterior. Cuando se alcanza el final de fichero actual, cada escritura aumenta correspondientemente el tamaño del fichero resultante. Pero si se utilizan sentencias de posicionado previas (por ejemplo fseek), debe recordar que es posible iniciar una nueva escritura más allá del límite actual del fichero, y que consecuentemente, esta operación extenderá el tamaño del fichero por encima de la cantidad de caracteres realmente escritos.
Ejemplo:
FILE* handle = fopen("D:\\path-name\\fichero.nnn", "w+b");
// tamaño actual del fichero 0 bytes.
char buffer[] = "AEIOU"; // buffer de escritura
fwrite(buffer, strlen(buffer), 1, handle);
// tamaño actual del fichero 5 bytes.
fseek(handle, 100L, SEEK_SET);
fwrite(buffer, strlen(buffer), 1, handle);
// tamaño actual del fichero 105 bytes.
[1] Para terminar de arreglarlo, la documentación de MS Visual C++ 6.0 nos informa que la rutina fseek puede comportarse erróneamente en el final de un fichero de texto.
[2] Para dar una idea de las magnitudes involucradas en el proceso, indiquemos que un fichero de texto, guardado como .txt con el WordPad de Windows, que aparecía como un fichero de disco de 65.579 bytes, se transformó en un buffer de 64.385 bytes después de leído en modo texto.