AnteriorPosterior

8. Ficheros

  Curso: Introducción a C++

8 - Ficheros

8.1. Escritura en un fichero de texto

Para manejar ficheros, siempre deberemos realizar tres operaciones básicas:

  • Abrir el fichero.
  • Leer datos de él o escribir datos en él.
  • Cerrar el fichero.

Además tendremos que comprobar los posibles errores. Por ejemplo, puede ocurrir que intentemos abrir un fichero que realmente no exista, o que queramos escribir en un dispositivo que sea sólo de lectura.

Vamos a ver un ejemplo, que cree un fichero de texto y escriba algo en él:

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.01:
// Escritura en un fichero de texto
 
#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    ofstream fichero("prueba.txt");
    fichero << "Esto es una línea" << endl;
    fichero << "Esto es otra";
    fichero << " y esto es continuación de la anterior" << endl;
    fichero.close();
 
    return 0;
}
 

Hay varias cosas que comentar sobre este programa:

  • Deberemos incluir "fstream" para poder usar los "flujos de datos de ficheros" (cuyo manejo será muy parecido al de la consola, en la que escribíamos con "cout" y leíamos con "cin".
  • Un fichero de escritura será un "ofstream", abreviatura de "output file stream". En su uso más básico, bastará con indicar el nombre que tendrá el fichero dentro de nuestro programa (por ejemplo "fichero") y, entre paréntesis y comillas, el nombre físico que tendrá en nuestro disco o dispositivo de almacenamiento que estemos usando (por ejemplo, "prueba.txt").
  • Para escribir datos en el fichero usando "<<", de forma muy similar a como usábamos "cout" para enviar texto a pantalla.
  • Finalmente, debemos cerrar el fichero con ".close()".

Ejercicios propuestos:

  • (8.1.1) Crea un programa que vaya leyendo las palabras que el usuario teclee y las guarde en un fichero de texto llamado “registroDeUsuario.txt”. Terminará cuando la palabras introducida sea "fin" (esa frase no deberá guardarse en el fichero).

 

8.2. Lectura de un fichero de texto

Si queremos leer de un fichero, los pasos son muy parecidos, excepto que el fichero será un "ifstream" (input file stream) y que leeremos con ">>", de forma parecida a como leíamos desde teclado:

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.02: 
// Lectura de una palabra de un fichero de texto
 
#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    ifstream fichero("prueba.txt");
    string linea;
 
    fichero >> linea;
 
    cout << "Se ha leido: " << endl;
    cout << linea << endl;
 
    fichero.close();
 
    return 0;
}
 

Ejercicios propuestos:

  • (8.2.1) Crea un programa que muestre las tres primeras palabras que contenga un fichero de texto.

 

8.3. Leer toda una línea, incluyendo espacios

Hasta ahora, hemos leído datos con "cin", pero cuando se trataba de texto, la lectura terminaba en el primer espacio. La alternativa es leer con "getline", que se puede aplicar tanto a "cin" como a un fichero: "getline(cin, texto);", de modo que un programa que pida al usuario el nombre de un fichero y muestre su primera línea completa sería así:

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.03: 
// Lectura de una frase de un fichero de texto
 
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
 
int main()
{
    cout << "Dime el nombre del fichero: ";
    string nombre;
    getline(cin, nombre);
 
    ifstream fichero(nombre.c_str());
    string linea;
 
    getline(fichero,linea);
    cout << "Se ha leido: " << endl;
    cout << linea << endl;
 
    fichero.close();
 
    return 0;
}
 

La única complicación adicional es que cuando abrimos el fichero "ifstream", se da por sentado que el primer dato es una cadena "al estilo de C", no un string. No es un problema si es un nombre prefijado, como ocurría en los dos ejemplos anteriores, pero sí es un poco más desconcertando cuando el nombre no es fijo, como en este caso. Pero basta con obtener una cadena "al estilo de C" a partir del string introducido por el usuario: "ifstream fichero(nombre.c_str());"

Ejercicios propuestos:

  • (8.3.1) Crea un programa que muestre las tres primeras líneas que contenga un fichero de texto, cuyo nombre deberá introducir el usuario.

 

8.4. Lectura hasta el final del fichero

Normalmente no querremos leer sólo una frase del fichero, sino procesar todo su contenido. Para ayudarnos, tenemos una forma de saber si ya hemos llegado al final del fichero. Es “.eof()”, que será verdadero si el fichero ha terminado (EOF es la abreviatura de End Of File, fin de fichero).

De igual modo, podemos comprobar si ha habido algún error (por ejemplo, hemos intentado abrir un fichero que no existe) usando ".fail()".

Por tanto, nuestro programa que avise si el fichero no existe y que muestre todo su contenido, deberá repetirse mientras que no se acabe el fichero, así:

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.04:
// Lectura hasta el final de un fichero de texto
 
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;
 
int main()
{
    string nombre;
    string linea;
 
    cout << "Dime el nombre del fichero: ";    
    getline(cin, nombre);
 
    ifstream fichero(nombre.c_str());
    if( fichero.fail() )
    {
        cout << "No existe el fichero!" << endl;
        exit(1);
    }
 
    while (! fichero.eof()) 
    {
        getline(fichero,linea);
        if (! fichero.eof()) 
            cout << linea << endl;
    }
    fichero.close();
 
    return 0;
}
 

Esa será la estructura básica de casi cualquier programa que deba leer un fichero completo, de principio a fin: abrir, comprobar que se ha podido acceder correctamente, leer con “while ( !fichero.eof())” y cerrar.

Ejercicios propuestos:

  • (8.4.1) Un programa que pida al usuario que teclee frases, y las almacene en el fichero “frases.txt”. Acabará cuando el usuario pulse Intro sin teclear nada. Después deberá mostrar el contenido del fichero.
  • (8.4.2) Un programa que pregunte un nombre de fichero y muestre en pantalla el contenido de ese fichero, haciendo una pausa después de cada 25 líneas, para que dé tiempo a leerlo. Cuando el usuario pulse intro, se mostrarán las siguientes 25 líneas, y así hasta que termine el fichero.

 

8.5. Ficheros binarios

Hasta ahora nos hemos centrado en los ficheros de texto, que son sencillos de crear y de leer. Pero también podemos manejar ficheros que contengan información de cualquier tipo. Se les conoce como "ficheros finarios".

Abrir un fichero binario es casi igual de fácil que uno de texto: basta añadir ", ifstream::binary" cunado se abre:

ifstream fichero(nombre.c_str(), ifstream::binary);

Podemos leer un byte usando ".get()":

char dato = fichero.get();

Si el dato no está en la siguiente posición del fichero, podemos saltar a cualquier posición con ".seekg(posicion, origen)":

fichero.seekg(0, fichero.beg);

(Los posibles puntos de origen para el salto son el principio del fichero (".beg"), la posición actual (".cur") y el final del fichero (".end")).

También podemos saber en qué posición nos encontramos con ".tellg()":

miFichero.seekg (0, miFichero.end);
int longitudFichero = miFichero.tellg();

(Como se ve en ese ejemplo, podemos usar ".tellg()" para saber el tamaño del fichero, si antes tenemos la precaución de situarnos al final del fichero).

Con eso tenemos suficiente para hacer un ejercicio más complicado, un primer problema real. Vamos a abrir un fichero que sea una imagen en formato BMP y a mostrar en pantalla si está comprimido o no. Para eso necesitamos antes saber cómo se guarda la información en un fichero BMP, pero esto es algo fácil de localizar en Internet, y se podría resumir así:

FICHEROS .BMP
Un fichero BMP está compuesto por las siguientes partes: una cabecera de fichero, una cabecera del bitmap, una tabla de colores y los bytes que definirán la imagen.

En concreto, los datos que forman la cabecera de fiochero y la cabecera de bitmap son los siguientes:

TIPO DE INFORMACIÓN

POSICIÓN EN EL ARCHIVO

Tipo de fichero (letras BM)

0-1

Tamaño del archivo

2-5

Reservado

6-7

Reservado

8-9

Inicio de los datos de la imagen

10-13

Tamaño de la cabecera de bitmap

14-17

Anchura (píxeles)

18-21

Altura (píxeles)

22-25

Número de planos

26-27

Tamaño de cada punto

28-29

Compresión (0=no comprimido)

30-33

Tamaño de la imagen

34-37

Resolución horizontal

38-41

Resolución vertical

42-45

Tamaño de la tabla de color

46-49

Contador de colores importantes

50-53


Con esta información nos basta para nuestro propósito: la compresión se indica en la posición 30 del fichero, ocupa 4 bytes (lo mismo que un “int” en los sistemas operativos de 32 bits), y si es un 0 querrá decir que la imagen no está comprimida. Nos bastaría con mirar el primero de esos 4 bytes, y lo podríamos conseguir así:

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.05:
// Información sobre un fichero BMP (1)
 
#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    string nombre;
    char datos[256];
    int compresion;
 
    cout << "Comprobador de imágenes BMP" << endl;
    cout << "Dime el nombre del fichero: ";
    getline(cin, nombre);
 
    ifstream fichero(nombre.c_str(), ifstream::binary);
 
    if ( fichero.fail() )
        cout << "No encontrado" << endl;
    else 
    {
        fichero.seekg(30, fichero.beg);
        fichero.read(datos,1);
        fichero.close();
 
        compression = datos[0];
        if (compresion == 0) 
            cout << "Sin compresión";
        else
            cout << "BMP Comprimido";
    }
 
    return 0;
}
 

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.05:
// Información sobre un fichero BMP (1)
 
#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    string nombre;
    char datos[256];
    int compresion;
 
    cout << "Comprobador de imágenes BMP" << endl;
    cout << "Dime el nombre del fichero: ";
    getline(cin, nombre);
 
    ifstream fichero(nombre.c_str(), ifstream::binary);
 
    if ( fichero.fail() )
        cout << "No encontrado" << endl;
    else 
    {
        fichero.seekg(30, fichero.beg);
        fichero.read(datos,1);
        fichero.close();
 
        compression = datos[0];
        if (compresion == 0) 
            cout << "Sin compresión";
        else
            cout << "BMP Comprimido";
    }
 
    return 0;
}
 

Ejercicios propuestos:

  • (8.5.1) Mejorar el ejemplo 08.05, para que compruebe si realmente se trata de un fichero BMP (el primer carácter debe ser 'B' y el segundo debe ser 'M'.).

 

8.6. Lectura por bloques en ficheros binarios

En un fichero binario, no sólo podemos leer byte a byte. También podemos leer todo un bloque de golpe, lo que en general resultará más rápido. En este caso, utilizamos “.read(datos,cantidad)”, donde "datos" será un array de caracteres, del tamaño que nosotros decidamos. Así, podríamos leer los 54 bytes de cabecera de un fichero BMP todos juntos, y acceder a cada uno de ellos después de cerrar el fichero. (De todos modos, este programa tiene alguna complicación adicional, que comentaremos tras el fuente de ejemplo):

// Introducción a C++, Nacho Cabanes
// Ejemplo 08.06:
// Información sobre un fichero BMP (2)
 
#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    int tamanyoCabecera = 54;
    char * cabecera = new char[tamanyoCabecera];
 
    string nombre;
    unsigned int compresion, ancho, alto;
    char marca1, marca2;
 
    cout << "Comprobador de imágenes BMP" << endl;
    cout << "Dime el nombre del fichero: ";
    getline(cin, nombre);
 
    ifstream fichero(nombre.c_str(), ifstream::binary);
    if ( fichero.fail() )
        cout << "No encontrado" << endl;
    else 
    {
        marca1 = fichero.get();  // Leo los dos primeros bytes
        marca2 = fichero.get();
        if ((marca1 =='B') && (marca2 =='M'))  // Si son BM
        {
            cout << "Marca del fichero: " 
                << marca1 << marca2 << endl;
 
            // Leo toda la cabecera
            fichero.seekg(0, fichero.beg);    
            fichero.read(cabecera, tamanyoCabecera);
            fichero.close();
 
            // Y calculo y muestro datos individuales
            ancho = (unsigned char) cabecera[18] + 
                (unsigned char) (cabecera[19])*256 +
                (unsigned char) (cabecera[20])*256*256 + 
                (unsigned char) (cabecera[21])*256*256*256;
            cout << "Ancho: " << ancho << endl;
 
            alto = (unsigned char) cabecera[22] + 
                (unsigned char) (cabecera[23])*256 +
                (unsigned char) (cabecera[24])*256*256 + 
                (unsigned char) (cabecera[25])*256*256*256; 
            cout << "Alto: " << alto << endl;    
 
            compresion = cabecera[30];
            switch (compresion) 
            {
                case 0: cout << "Sin compresión"; break;
                case 1: cout << "Compresión RLE 8 bits"; break;
                case 2: cout << "Compresión RLE 4 bits"; break;
            }
            cout << endl;
        } 
        else  // Si la marca no es BM
            cout << "No parece un fichero BMP" << endl;  
    }
 
    return 0;
}
 

Los detalles "avanzados" que muestra este ejemplo son:

  • El ancho y el alto no son datos de un único byte. Son número enteros de 4 bytes. En este caso, el convenio más usado (lo que se conoce como "little endian") es que los datos se guardan "al revés": el primer dato que leeremos será el byte menos significativo del número final, el segundo dato será el siguiente byte menos significativo, y así sucesivamente, hasta llegar al último byte, que es el más significativo. Por eso, el dato se debe calcular haciendo "ancho = byte1 + byte2*256 + byte3*256*256 + byte4*256*256*256;
  • El tipo de datos "char" puede almacenar números negativos. De hecho, si guardamos el número "224" como dato de tipo "char" y luego lo volvemos a leer, se nos dirá que el dato era "-32". Por eso, antes de realizar la operación anterior, le decimos que tome cada dato como "unsigned char" (sin signo, considerando siempre que se trata de un número positivo). Más adelante hablaremos de "unsigned" y de otros modificadores.

Si queremos guardar bloques de información en un fichero binario, la estructura será básicamente la misma, salvo que usaremos "ofstream" en vez de "ifstream", y guardaremos con "write", que es similar a "read". Pero eso queda para los ejercicios...

Ejercicios propuestos:

  • (8.6.1) Hacer un programa que muestre información sobre una imagen en formato GIF (se deberá localizar en Internet los detalles sobre dicho formato): versión, ancho de la imagen (en píxeles), alto de la imagen y cantidad de colores.
  • (8.6.2) Crear un programa que muestre información sobre una imagen en formato PCX: ancho de la imagen (en píxeles), alto de la imagen y cantidad de colores.
  • (8.6.3) Diseñar un programa que duplique un fichero existente, creando un array para almacenar todo su contenido, y volcando este array a un nuevo fichero.

 

Actualizado el: 17-09-2013 14:13

AnteriorPosterior